【C#】ZeroAllocationへの道 - 究極のメモリ最適化テクニック

## はじめに
メモリアロケーションの最適化は、高パフォーマンスアプリケーションにおいて重要な要素です。

今回は、実際のプロジェクトで使える具体的なテクニックをまとめてみました。


# 目次

- [なぜZeroAllocationが重要なのか](#なぜzeroallocationが重要なのか)
- [Span<T>とメモリ管理](#spantとメモリ管理)
- [Stackallocの活用](#stackallocの活用)
- [カスタムメモリプールの実装](#カスタムメモリプールの実装)
- [構造体の最適化](#構造体の最適化)
- [文字列処理の最適化](#文字列処理の最適化)
- [まとめ](#まとめ)


## なぜZeroAllocationが重要なのか
高頻度で実行される処理において、不要なメモリアロケーションは大きなパフォーマンスの低下を引き起こします。

特にマイクロサービスやリアルタイム処理システムでは、GCの頻度を減らすことが重要です。

以下は、一般的なコードと最適化後のコードの比較です。

```csharp:C#
// 一般的なコード - 多くのアロケーションが発生
public string ProcessData(string data)
{
    var items = data.Split(',');
    var result = new List<string>();
    foreach (var item in items)
    {
        result.Add(item.Trim().ToUpper());
    }
    return string.Join(",", result);
}

// 最適化後 - アロケーションを最小限に
public int ProcessData(ReadOnlySpan<char> data, Span<char> output)
{
    var written = 0;
    var start = 0;
    
    for (var i = 0; i < data.Length; i++)
    {
        if (data[i] == ',')
        {
            var segment = data.Slice(start, i - start);
            written += ProcessSegment(segment.Trim(), output.Slice(written));
            output[written++] = ',';
            start = i + 1;
        }
    }
    
    if (start < data.Length)
    {
        written += ProcessSegment(data.Slice(start).Trim(), output.Slice(written));
    }
    
    return written;
}
```

## Span<T>とメモリ管理
`Span<T>`は.NET Core/.NET 5+で導入された強力な機能です。

スタック上のメモリやネイティブメモリを安全に扱うことができ、パフォーマンスの向上に大きく貢献します。

次の例は、画像処理での活用例です。

```csharp:C#
public unsafe void ProcessImage(byte[] imageData)
{
    fixed (byte* ptr = imageData)
    {
        var span = new Span<byte>(ptr, imageData.Length);
        ProcessImageData(span);
    }
}

private void ProcessImageData(Span<byte> data)
{
    // RGBデータの高速処理
    for (var i = 0; i < data.Length; i += 3)
    {
        var r = data[i];
        var g = data[i + 1];
        var b = data[i + 2];
        
        // グレースケール変換
        var gray = (byte)((r * 0.3) + (g * 0.59) + (b * 0.11));
        data[i] = data[i + 1] = data[i + 2] = gray;
    }
}
```

## Stackallocの活用

小さな配列が必要な場合、`stackalloc`を使用してヒープアロケーションを避けることができます。ただし、いくつかの重要な制約があることを理解しておく必要があります。

```csharp:C#
public int CalculateHash(ReadOnlySpan<byte> data)
{
    // 重要: stackallocのサイズは慎重に決定する
    // スタックサイズは環境に依存（Windows x64: 1MB, Linux x64: 8MB など）
    const int MaxStackAlloc = 1024; // 1KB以下なら安全

    Span<byte> buffer = data.Length <= MaxStackAlloc 
        ? stackalloc byte[data.Length] 
        : new byte[data.Length];  // 大きいサイズはヒープ割り当て
    
    data.CopyTo(buffer);
    return ComputeHash(buffer);
}

// 再帰処理での使用は避ける
public void ProcessRecursively(int depth, ReadOnlySpan<byte> data)
{
    // 危険: 再帰的なstackalloc
    // Span<byte> buffer = stackalloc byte[1024]; // ⚠️スタックオーバーフローの危険性

    // 代わりにヒープ割り当てを使用
    var buffer = new byte[1024];
    // ... 処理 ...
}
```

なお、.NET 5以降では`Span<T>`が状況によってはヒープ割り当てを行う場合があります。特に大きなサイズや特定の実行環境では、明示的に制御する必要があります。

:::note info
**ポイント**
- スタックサイズは環境依存であることを意識する
- 再帰処理でのstackallocは避ける
- 1KB程度の小さいサイズに制限する
- 大きいサイズはヒープ割り当てやプールを使用する
:::

このように、`stackalloc`は強力な最適化ツールですが、適切なサイズ制限と使用箇所の選定が重要です。


## カスタムメモリプールの実装
頻繁にアロケーションが必要な場合、カスタムメモリプールを実装することで再利用を促進できます。

```csharp:C#
public sealed class BufferPool<T>
{
    private readonly ConcurrentBag<T[]> _pool;
    private readonly int _bufferSize;
    private readonly int _maxPoolSize;

    public BufferPool(int bufferSize, int maxPoolSize)
    {
        _bufferSize = bufferSize;
        _maxPoolSize = maxPoolSize;
        _pool = new ConcurrentBag<T[]>();
    }

    public T[] Rent()
    {
        return _pool.TryTake(out var buffer) ? buffer : new T[_bufferSize];
    }

    public void Return(T[] buffer)
    {
        if (buffer.Length == _bufferSize && _pool.Count < _maxPoolSize)
        {
            Array.Clear(buffer, 0, buffer.Length);
            _pool.Add(buffer);
        }
    }
}
```

## 構造体の最適化
構造体のメモリレイアウトを最適化することで、パフォーマンスを向上させることができます。

```csharp:C#
// 最適化前
public struct Vector3D
{
    public double X;  // 8バイト
    public bool IsValid; // 1バイト
    public double Y;  // 8バイト
    public double Z;  // 8バイト
} // 32バイト（パディング含む）

// 最適化後
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Vector3D
{
    public double X, Y, Z;  // 連続した24バイト
    public bool IsValid;    // 1バイト
} // 25バイト
```

## 文字列処理の最適化
文字列操作は多くのアロケーションを引き起こす原因となります。`StringBuilder`の代わりに `Span<char> `を使用することで、アロケーションを削減できます。

```csharp:C#
public static class StringHelper
{
    public static bool TryFormat(int value, Span<char> destination, out int charsWritten)
    {
        return value.TryFormat(destination, out charsWritten);
    }

    public static string FormatWithoutAllocation(int value1, int value2)
    {
        Span<char> buffer = stackalloc char[32];
        var position = 0;

        if (value1.TryFormat(buffer.Slice(position), out var written))
        {
            position += written;
            if (position < buffer.Length)
            {
                buffer[position++] = ',';
                if (value2.TryFormat(buffer.Slice(position), out written))
                {
                    position += written;
                    return new string(buffer.Slice(0, position));
                }
            }
        }

        throw new InvalidOperationException("Buffer too small");
    }
}
```

## まとめ
- `ZeroAllocation`は、高パフォーマンスが要求される場面で重要
- `Span<T>`と`stackalloc`を適切に活用することでGC負荷を軽減
- メモリプールやカスタムアロケータの実装で再利用を促進
- 構造体の最適化でメモリレイアウトを改善