Allocation-Free Desenler¶
Genel Bakış¶
Bu sayfa, Span ve Memory ile Pooling temellerini günlük kodda çöp (garbage) üretmeden uygulamaya döken modern .NET API'lerini ve desenlerini toplar: string işlemleri, UTF-8, SearchValues, SIMD ve gizli allocation kaynakları.
String İşlemleri: Allocation Cehennemi¶
String'ler immutable olduğu için her birleştirme/değişiklik yeni bir string (heap allocation) üretir. Sıcak yollarda en büyük allocation kaynağıdır.
1. string.Create ile tek seferde inşa¶
Çıktının uzunluğu önceden biliniyorsa, ara string üretmeden doğrudan tampona yazılır:
// "USER-00042" gibi bir kimlik üret — ara allocation YOK
public static string FormatId(int id) =>
string.Create(10, id, static (span, value) =>
{
"USER-".CopyTo(span);
value.TryFormat(span[5..], out _, "D5");
});
Karşılaştırma: "USER-" + id.ToString("D5") iki ara string + bir birleştirme üretir; string.Create sıfır ara allocation üretir.
2. StringBuilder (bilinmeyen uzunluk)¶
Döngüde string biriktiriyorsan += yerine StringBuilder — ve mümkünse havuzdan al:
var sb = new StringBuilder(capacity: 256); // kapasiteyi baştan ver → yeniden tahsisi azalt
foreach (var item in items) sb.Append(item).Append(',');
3. Span ile allocation'sız ayrıştırma¶
Split bir dizi + N string üretir. ReadOnlySpan<char> ile bölme hiç allocation üretmez:
ReadOnlySpan<char> line = "12,34,56";
foreach (Range r in line.Split(',')) // .NET 8+ Span.Split, allocation'sız enumerator
{
int value = int.Parse(line[r]);
}
UTF-8 Doğrudan İşleme¶
Web/JSON dünyası UTF-8'dir. string (UTF-16) ↔ byte dönüşümü hem CPU hem allocation maliyetidir. .NET, UTF-8 ile doğrudan çalışmayı destekler:
// UTF-8 literal — derleme zamanında byte'lara çevrilir, runtime'da string yok
ReadOnlySpan<byte> prefix = "data: "u8;
// IUtf8SpanFormattable: sayıyı doğrudan UTF-8 byte tamponuna yaz (string atlanır)
Span<byte> buffer = stackalloc byte[32];
int value = 42;
value.TryFormat(buffer, out int written); // UTF-16 string üretmeden
System.Text.Json'ın hızının temeli budur: byte → değer, ara string üretmeden.
SearchValues<T>: Hızlı Çoklu Arama¶
Bir karakter/byte kümesini tekrar tekrar aramak gerektiğinde (IndexOfAny ile), SearchValues<T> (.NET 8+) önceden optimize edilmiş, SIMD-hızlandırmalı bir arama yapısı sağlar:
private static readonly SearchValues<char> Vowels =
SearchValues.Create("aeiouAEIOU");
public static int FirstVowel(ReadOnlySpan<char> text) =>
text.IndexOfAny(Vowels); // klasik IndexOfAny'den belirgin hızlı, allocation'sız
SIMD: Veri Paralelliği (Vector<T>)¶
SIMD (Single Instruction, Multiple Data), tek CPU komutuyla birden çok veri öğesini işler. Büyük dizilerde toplama/karşılaştırma gibi işlemleri katlarca hızlandırır:
// Klasik: eleman eleman topla
public static int SumScalar(ReadOnlySpan<int> data)
{
int sum = 0;
foreach (int x in data) sum += x;
return sum;
}
// SIMD: aynı anda Vector<int>.Count eleman topla
public static int SumSimd(ReadOnlySpan<int> data)
{
var acc = Vector<int>.Zero;
int i = 0;
int width = Vector<int>.Count;
for (; i <= data.Length - width; i += width)
acc += new Vector<int>(data.Slice(i, width));
int sum = Vector.Dot(acc, Vector<int>.One); // vektör içini topla
for (; i < data.Length; i++) sum += data[i]; // kalan elemanlar
return sum;
}
SIMD güçlüdür ama kodu karmaşıklaştırır. Yalnızca büyük veri üzerinde, ölçülmüş sıcak yolda değer. Çoğu zaman
Spanüzerindeki yerleşik metotlar (IndexOf,SequenceEqual) zaten içeride SIMD kullanır — önce onları dene.
Gizli Allocation Kaynakları¶
Sıcak yolda farkında olmadan çöp üreten yaygın noktalar:
| Kaynak | Sorun | Çözüm |
|---|---|---|
| Closure / lambda yakalama | Yakalanan değişken için heap nesnesi | static lambda, yakalamadan kaçın |
| Boxing | Değer tipi → object/interface | Generic, Span |
params object[] |
Her çağrıda dizi | Overload veya params ReadOnlySpan (.NET 9) |
| LINQ zinciri | Her operatör enumerator + delegate allocation | Sıcak yolda elle for döngüsü |
IEnumerator (interface) |
Struct enumerator yerine boxing | foreach'i somut tip üzerinde yap |
| String interpolation | Karmaşık formatta ara string | string.Create, IFormattable |
async state machine |
Her await için (gerekirse) heap |
Sıcak yolda ValueTask, senkron tamamlanan yol |
// ❌ Lambda her çağrıda 'threshold'u yakalar → closure allocation
items.Where(x => x.Value > threshold);
// ✅ Sıcak yolda elle döngü — allocation yok
foreach (var x in items) if (x.Value > threshold) yield return x;
Not: LINQ ve string interpolation sıradan kodda gayet iyidir. Bunlardan kaçınmak yalnızca ölçülmüş, çok sık çalışan sıcak yollar için geçerlidir. Okunabilirlik genelde önceliklidir.
Mülakat Soruları¶
1. Soru: "+ ile string birleştirme neden yavaştır, alternatifi nedir?"¶
Cevap: String immutable olduğu için her + yeni bir string (heap allocation + karakter kopyası) üretir; döngüde bu O(n²) kopya ve çok sayıda çöp demektir. Alternatifler: uzunluk biliniyorsa string.Create (sıfır ara allocation), bilinmiyorsa StringBuilder (tercihen kapasiteyle veya havuzdan), ayrıştırmada ReadOnlySpan<char> slicing, sabit birleştirmede string.Join/interpolation.
2. Soru: "UTF-8 ile doğrudan çalışmak neden allocation azaltır?"¶
Cevap: string UTF-16'dır; ağdan/diskten gelen veri ise genelde UTF-8 byte'larıdır. Geleneksel yol byte → string (UTF-16, allocation) → işlem yapar. u8 literal'ler, IUtf8SpanFormattable ve Utf8JsonReader gibi API'ler byte'lar üzerinde doğrudan çalışır; ara string hiç oluşmaz. System.Text.Json'ın yüksek performansının temeli budur.
3. Soru: "Closure allocation nedir, nasıl önlersin?"¶
Cevap: Bir lambda, kendi dışındaki bir değişkeni yakaladığında (capture), derleyici bu değişkeni tutmak için bir heap nesnesi (display class) oluşturur. Sıcak yolda her çağrıda allocation üretir. Önlemek için: değişken yakalamayan static lambda kullanmak, gerekli veriyi parametre/state olarak geçirmek (örn. string.Create'in state parametresi), veya sıcak yolda lambda yerine elle döngü.
4. Soru: "SIMD ne zaman kullanılır, riski nedir?"¶
Cevap: Büyük diziler üzerinde aynı işlemi tekrarlayan (toplama, karşılaştırma, filtreleme) sayısal/veri-yoğun sıcak yollarda. Riski: kod ciddi karmaşıklaşır, bakımı zorlaşır ve yanlış yazılırsa (kalan elemanlar, hizalama) hata üretir. Üstelik Span'in IndexOf/SequenceEqual gibi yerleşik metotları zaten içeride SIMD kullanır; çoğu durumda elle SIMD yazmaya gerek kalmadan onlardan faydalanılır. Her zaman benchmark ile doğrulanmalıdır.
Best Practices¶
- Sabit uzunluklu string üretiminde
string.Create; değişkende kapasiteliStringBuilder. - Ayrıştırmada
SplityerineReadOnlySpan<char>slicing /Span.Split. - UTF-8 verisinde
u8literal ve UTF-8 API'leriyle doğrudan çalış. - Tekrarlı çoklu-karakter aramada
SearchValues<T>. - SIMD'den önce
Span'in yerleşik (zaten SIMD'li) metotlarını dene. - Sıcak yolda closure/boxing/LINQ allocation'larını
[MemoryDiagnoser]ile avla. - Bu tekniklerin hiçbirini sıradan koda uygulama — yalnızca ölçülmüş, çok sık çalışan sıcak yollar için.
İlişkili Konular¶
Kaynaklar¶
- Stephen Toub — "Performance Improvements in .NET 8/9" (UTF-8, SearchValues)
- Microsoft Docs — SearchValues, string.Create, System.Numerics.Vector
- "Pro .NET Memory Management" — Konrad Kokosa