6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C# 深掘り】Span<T>とMemory<T>の内部実装から理解する最適化手法

Posted at

はじめに

C#のメモリ最適化において重要な役割を果たすSpan<T>Memory<T>について、その内部実装から最適化手法まで深掘りしていきます。

目次

  1. SpanとMemoryの基本
  2. 内部実装の詳細
  3. メモリ安全性の仕組み
  4. パフォーマンス最適化手法
  5. 実践的なユースケース

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]);
    }
}

ベストプラクティスとパフォーマンス注意点

  1. メモリ割り当ての最小化

    • 可能な限りstackallocを活用
    • 大きすぎるサイズはスタックオーバーフローの危険性あり
  2. 適切な型の選択

    • 同期処理にはSpan<T>
    • 非同期処理にはMemory<T>
    • メソッド間でのデータ受け渡しにはReadOnlySpan<T>
  3. パフォーマンス計測

    • ベンチマークでの検証が重要
    • 小さなデータでは従来の方法が速い場合も

まとめ

Span<T>Memory<T>は、メモリ割り当てを最小限に抑えながら、安全に連続したメモリ領域を操作するための強力なツールです。内部実装を理解し、適切に使用することで、アプリケーションのパフォーマンスを大きく向上させることができます。

特に以下のような場面での使用を検討してください:

  • 大量のデータ処理
  • 文字列操作の最適化
  • バイナリデータの処理
  • パース処理の高速化

参考文献

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?