Span<T> / Memory<T> で書くゼロコピー C# — 現場で効くパターン集
C# Tips

Span<T> / Memory<T> で書くゼロコピー C# — 現場で効くパターン集

公開: 更新: 約1分で読めます

なぜ 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);
}
string.Split と ReadOnlySpan<char>.Split のアロケーション量・スループット比較
ログパースのような高頻度処理では Span 版に切り替えるだけで GC 圧がほぼゼロになる。

やりがちな落とし穴

  • クラスのフィールドに Span を持てないMemory で持ち、メソッド内で Span 化する。
  • ArrayPool から借りたバッファは必ず返すtry/finallyReturn を忘れない。
  • 範囲外アクセスは IndexOutOfRangeException — 最初の単体テストで必ず境界をつく。

計測 → 適用の順で

Span/Memory は強力ですが、可読性は下がります。プロファイラで allocation が実際にボトルネックになっていることを確認してから投入するのが鉄則です。