6
7

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#】ZeroAllocationへの道 - 究極のメモリ最適化テクニック

Posted at

はじめに

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

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

目次

なぜZeroAllocationが重要なのか

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

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

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

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とメモリ管理

Span<T>は.NET Core/.NET 5+で導入された強力な機能です。

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

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

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を使用してヒープアロケーションを避けることができます。ただし、いくつかの重要な制約があることを理解しておく必要があります。

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>が状況によってはヒープ割り当てを行う場合があります。特に大きなサイズや特定の実行環境では、明示的に制御する必要があります。

ポイント

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

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

カスタムメモリプールの実装

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

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

構造体の最適化

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

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> を使用することで、アロケーションを削減できます。

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負荷を軽減
  • メモリプールやカスタムアロケータの実装で再利用を促進
  • 構造体の最適化でメモリレイアウトを改善
6
7
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
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?