はじめに
C#のメモリ最適化において重要な役割を果たすSpan<T>
とMemory<T>
について、その内部実装から最適化手法まで深掘りしていきます。
目次
- SpanとMemoryの基本
- 内部実装の詳細
- メモリ安全性の仕組み
- パフォーマンス最適化手法
- 実践的なユースケース
1. SpanとMemoryの基本
1.1 概要
Span<T>
は連続したメモリ領域への参照を表すref-like構造体です。スタック上でのみ使用可能で、ヒープに保存できないという特徴があります。一方、Memory<T>
はヒープに保存可能な参照型として設計されています。
スタック(Stack)
- プログラムの実行時に自動的に管理される高速なメモリ領域
- サイズが固定で、メモリの割り当てと解放が高速
- 関数呼び出しやローカル変数を格納
- LIFO(Last In First Out:後入れ先出し)方式
- 値型(int, structなど)はデフォルトでここに格納
ヒープ(Heap)
- プログラマが明示的に管理できる柔軟なメモリ領域
- 動的にサイズを変更可能
- 参照型(class, string など)のオブジェクトを格納
- メモリの割り当てと解放はガベージコレクタが管理
- スタックより遅いが、より大きなデータを扱える
1.2 基本的な違い
C#
// Span<T>の例
Span<byte> stackSpan = stackalloc byte[100]; // スタック上に確保
Span<byte> heapSpan = new byte[100].AsSpan(); // ヒープ上のデータを参照
// Memory<T>の例
Memory<byte> memory = new byte[100]; // ヒープ上に確保
2. 内部実装の詳細
2.1 Spanの内部構造
C#
public readonly ref struct Span<T>
{
private readonly ref T _reference;
private readonly int _length;
}
特筆すべき点は以下の通りです:
-
ref struct
として定義されており、ヒープに置けない -
readonly
フィールドを持つが、内容は変更可能 - ポインタのようなものですが、GCに追跡される
2.2 Memoryの内部構造
C#
public readonly struct Memory<T>
{
private readonly object _object;
private readonly int _index;
private readonly int _length;
}
Memoryの特徴:
- 通常の
struct
として定義 - オブジェクト参照を保持できる
- 非同期操作での使用が可能
3. メモリ安全性の仕組み
3.1 型システムによる制約
C#
public class Example
{
// これはコンパイルエラー - Span<T>はフィールドに使えない
private Span<byte> _span;
// これはOK
private Memory<byte> _memory;
public void Method()
{
// ローカル変数としてのSpan<T>は問題なし
Span<byte> localSpan = stackalloc byte[100];
}
}
3.2 ライフタイム管理
C#
public class SafetyExample
{
public Memory<byte> CreateMemory()
{
return new byte[100];
}
public Span<byte> UnsafeMethod()
{
// 警告: スタックメモリへの参照を返すことはできない
Span<byte> localSpan = stackalloc byte[100];
return localSpan; // コンパイルエラー
}
}
4. パフォーマンス最適化手法
4.1 スライシングの最適化
C#
public class SlicingOptimization
{
public void Traditional(byte[] array)
{
// 従来の方法 - 新しい配列が作成される
var slice = new byte[10];
Array.Copy(array, 0, slice, 0, 10);
}
public void Optimized(byte[] array)
{
// Span<T>を使用 - 新しいメモリ割り当てなし
Span<byte> span = array.AsSpan();
Span<byte> slice = span.Slice(0, 10);
}
}
4.2 スタックアロケーションの活用
C#
public class StackAllocationOptimization
{
public void ProcessLargeArray(byte[] largeArray)
{
// スタック上に一時バッファを確保
Span<byte> buffer = stackalloc byte[1024];
// バッファを使用して処理
for (int i = 0; i < largeArray.Length; i += buffer.Length)
{
int count = Math.Min(buffer.Length, largeArray.Length - i);
largeArray.AsSpan(i, count).CopyTo(buffer);
ProcessBuffer(buffer[..count]);
}
}
private void ProcessBuffer(Span<byte> buffer)
{
// バッファ処理ロジック
}
}
4.3 文字列操作の最適化
C#
public class StringOptimization
{
public string TraditionalConcat(string str1, string str2)
{
// 従来の方法 - 複数の一時文字列が生成される
return str1 + "_" + str2;
}
public string OptimizedConcat(string str1, string str2)
{
// 必要なバッファサイズを計算
int length = str1.Length + 1 + str2.Length;
// スタック上にバッファを確保
Span<char> buffer = length <= 1024
? stackalloc char[length]
: new char[length];
// 一度の操作で連結
str1.AsSpan().CopyTo(buffer);
buffer[str1.Length] = '_';
str2.AsSpan().CopyTo(buffer.Slice(str1.Length + 1));
return new string(buffer);
}
}
5. 実践的なユースケース
5.1 バイナリデータ処理
C#
public class BinaryProcessing
{
public void ProcessBinaryFile(string filename)
{
using var fs = File.OpenRead(filename);
// バッファをスタック上に確保
Span<byte> buffer = stackalloc byte[4096];
while (true)
{
int read = fs.Read(buffer);
if (read == 0) break;
ProcessChunk(buffer[..read]);
}
}
private void ProcessChunk(Span<byte> chunk)
{
// チャンク処理ロジック
}
}
5.2 高性能なパース処理
C#
public class HighPerformanceParser
{
public decimal ParseDecimal(ReadOnlySpan<char> input)
{
// スタック上に一時バッファを確保
Span<char> buffer = stackalloc char[input.Length];
// 不要な空白を除去
int len = 0;
for (int i = 0; i < input.Length; i++)
{
if (!char.IsWhiteSpace(input[i]))
buffer[len++] = input[i];
}
// パース処理
return decimal.Parse(buffer[..len]);
}
}
ベストプラクティスとパフォーマンス注意点
-
メモリ割り当ての最小化
- 可能な限り
stackalloc
を活用 - 大きすぎるサイズはスタックオーバーフローの危険性あり
- 可能な限り
-
適切な型の選択
- 同期処理には
Span<T>
- 非同期処理には
Memory<T>
- メソッド間でのデータ受け渡しには
ReadOnlySpan<T>
- 同期処理には
-
パフォーマンス計測
- ベンチマークでの検証が重要
- 小さなデータでは従来の方法が速い場合も
まとめ
Span<T>
とMemory<T>
は、メモリ割り当てを最小限に抑えながら、安全に連続したメモリ領域を操作するための強力なツールです。内部実装を理解し、適切に使用することで、アプリケーションのパフォーマンスを大きく向上させることができます。
特に以下のような場面での使用を検討してください:
- 大量のデータ処理
- 文字列操作の最適化
- バイナリデータの処理
- パース処理の高速化