なぜ Span / Memory か
Span<T> と Memory<T> は、配列や文字列の一部をコピーせずに扱うための型です。高スループットが求められる Web API やバッチ処理で、GC 圧を下げる決定打になります。
パターン 1: 文字列分割をコピーしない
string.Split は部分文字列の配列を返しますが、ログパースなど大量に走る処理では毎回 allocation が発生します。ReadOnlySpan<char>.Split(.NET 8 以降、.NET 10 / C# 14 の暗黙スパン変換でさらに書きやすくなった)を使うと無割当で処理できます。
ReadOnlySpan<char> line = "2024-01-15 12:34:56 INFO Started";
foreach (var range in line.Split(' '))
{
var token = line[range];
// token を使った処理(allocation なし)
}
パターン 2: UTF-8 を直接パースする
HTTP リクエストから受け取ったバイトは ReadOnlySpan<byte> です。Encoding.UTF8.GetString で文字列化する前に直接パースできれば、もう 1 段階アロケーションを削れます。
public static bool TryParseInt(ReadOnlySpan<byte> utf8, out int value) =>
System.Buffers.Text.Utf8Parser.TryParse(utf8, out value, out _);
パターン 3: 非同期と Memory の使い分け
Span<T> は ref struct のため async メソッドをまたげません。非同期処理内で持ち回るには Memory<T> を使います。
public async Task ProcessAsync(ReadOnlyMemory<byte> payload, CancellationToken ct)
{
await _pipe.WriteAsync(payload, ct);
// 同期的にスパン化して使う
ParseHeader(payload.Span);
}
やりがちな落とし穴
- クラスのフィールドに
Spanを持てない —Memoryで持ち、メソッド内でSpan化する。 ArrayPoolから借りたバッファは必ず返す —try/finallyでReturnを忘れない。- 範囲外アクセスは
IndexOutOfRangeException— 最初の単体テストで必ず境界をつく。
計測 → 適用の順で
Span/Memory は強力ですが、可読性は下がります。プロファイラで allocation が実際にボトルネックになっていることを確認してから投入するのが鉄則です。
