Modern .NET'in performans hikayesinin büyük bölümü Span<T> ve Memory<T>'ye dayanıyor. Bu tipler, dizi kopyalamadan bellek dilimleri üzerinde çalışmanızı sağlayarak hem GC baskısını hem de CPU zamanını önemli ölçüde azaltıyor.

Sorun: Gereksiz Kopya ve Allocation

// Klasik yaklaşım — her operasyon yeni string/dizi oluşturur
string ParseToken(string header)
{
    // "Bearer eyJhbGci..." → "eyJhbGci..."
    if (header.StartsWith("Bearer "))
        return header.Substring(7); // Yeni string! Heap allocation.
    return string.Empty;
}

// 1000 req/sn geliyorsa: saniyede 1000 gereksiz string nesnesi

Span<T>: Stack'te Yaşayan Dilim

Span<T> bir struct'tır ve her zaman stack'te yaşar. Heap'teki veriyi kopyalamadan referans alır:

// Sıfır allocation ile aynı iş
ReadOnlySpan<char> ParseToken(ReadOnlySpan<char> header)
{
    const string prefix = "Bearer ";
    if (header.StartsWith(prefix.AsSpan()))
        return header.Slice(prefix.Length); // Yeni nesne yok, sadece pointer+length
    return ReadOnlySpan<char>.Empty;
}

// Kullanım
string authHeader = "Bearer eyJhbGciOiJSUzI1NiJ9....";
ReadOnlySpan<char> token = ParseToken(authHeader.AsSpan());
// token, authHeader'ın heap'teki verisini gösteriyor — kopya yok

stackalloc ile Gerçek Stack Belleği

// Küçük buffer'lar için heap allocation gerekmez
void ProcessPacket(ReadOnlySpan<byte> data)
{
    // 256 byte'a kadar stack'te tut, daha büyükse heap'e geç
    Span<byte> buffer = data.Length <= 256
        ? stackalloc byte[data.Length]
        : new byte[data.Length];

    data.CopyTo(buffer);
    // buffer üzerinde çalış — scope bitince otomatik temizlenir
    XorBuffer(buffer, key: 0x42);
    SendToNetwork(buffer);
}

// Pratik örnek: Base64 encode — heap allocation yok
string EncodeToBase64(ReadOnlySpan<byte> data)
{
    int base64Length = ((data.Length + 2) / 3) * 4;
    Span<char> output = base64Length <= 512
        ? stackalloc char[base64Length]
        : new char[base64Length];

    Convert.TryToBase64Chars(data, output, out int written);
    return new string(output[..written]);
}

String Parsing: MemoryExtensions ile

// CSV satırı parse etme — sıfır allocation
void ParseCsvRow(ReadOnlySpan<char> line)
{
    int fieldStart = 0;

    for (int i = 0; i <= line.Length; i++)
    {
        if (i == line.Length || line[i] == ',')
        {
            var field = line.Slice(fieldStart, i - fieldStart).Trim();
            ProcessField(field); // Span<char> — kopya yok
            fieldStart = i + 1;
        }
    }
}

// Sayı parse etme — string oluşturulmadan
bool TryParsePort(ReadOnlySpan<char> input, out int port)
    => int.TryParse(input, out port) && port is > 0 and <= 65535;

// Kullanım
var config = "host:8080".AsSpan();
int colonIdx = config.IndexOf(':');
var host = config[..colonIdx];
TryParsePort(config[(colonIdx + 1)..], out int port);

Memory<T>: Async ile Uyumlu Span

Span<T> stack-only olduğu için async metotlarda, field'larda veya lambda'larda kullanılamaz. Bu durumlarda Memory<T> kullanılır:

// Span async metotta kullanılamaz — derleme hatası
async Task ProcessAsync(Span<byte> data) { } // Hata!

// Memory async uyumlu
async Task ProcessAsync(Memory<byte> buffer, CancellationToken ct)
{
    // Stream'den oku
    int bytesRead = await _stream.ReadAsync(buffer, ct);

    // Span'e dönüştür (senkron işlem için)
    var data = buffer.Span[..bytesRead];
    var checksum = ComputeChecksum(data);

    await _output.WriteAsync(buffer[..bytesRead], ct);
}

// IMemoryOwner ile havuzdan bellek al
async Task ProcessLargeFileAsync(string path)
{
    // ArrayPool üzerinden — GC'ye değil pool'a geri döner
    using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(4096);
    Memory<byte> buffer = owner.Memory;

    await using var file = File.OpenRead(path);
    int bytesRead;
    while ((bytesRead = await file.ReadAsync(buffer)) > 0)
        await ProcessAsync(buffer[..bytesRead], CancellationToken.None);
}

ArrayPool<T>: Buffer'ları Yeniden Kullan

// Büyük geçici diziler için ArrayPool
async Task<string> CompressAndEncodeAsync(byte[] data)
{
    // Pool'dan al — new byte[] yerine
    byte[] rentedBuffer = ArrayPool<byte>.Shared.Rent(data.Length * 2);
    try
    {
        using var ms = new MemoryStream(rentedBuffer);
        using var gz = new GZipStream(ms, CompressionLevel.Fastest);
        await gz.WriteAsync(data);
        await gz.FlushAsync();

        return Convert.ToBase64String(rentedBuffer, 0, (int)ms.Position);
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(rentedBuffer); // Pool'a geri ver
    }
}

Kısıtlamalar

  • Span<T> ref struct'tır: class field'ı olamaz, boxing edilemez, lambda'da yakalanamaz
  • Async metotlarda Span yerine Memory kullanın
  • stackalloc için genellikle 1 KB altı tavsiye edilir; daha büyük boyutlar için ArrayPool
  • Memory<T>.Span özelliği senkron context'te kullanılabilir

Span<T> ve Memory<T> ilk kullanımda biraz alışma gerektiriyor ama özellikle ağ protokolü implementasyonları, dosya parsing işlemleri ve yüksek frekanslı servislerde GC baskısını dramatik biçimde azaltıyor. .NET runtime'ın kendisi de bu API'leri yoğun kullanıyor.