LINQ'i değiştirmeden önce üretilen koda bakın
Daniel Marbach, Azure Service Bus AMQP receiver'ındaki kısa bir LINQ sorgusunu optimize ederken asıl maliyetin derleyicinin ürettiği delegate allocation olduğunu gösteriyor. İlk değişiklik her input'ta kazanç sağlarken, collection-aware ikinci sürüm set ve list'lerde hızlandırıp lazy enumerable'larda yavaşlatıyor; ders ise LINQ'ten vazgeçmek değil, sorgunun neye dönüştüğünü ölçmek.
Kilit noktalar
- Instance field'e erişen bir lambda, `Any` gibi metotlara her çağrıda yeni bir `Func<Guid, bool>` delegate'i ürettirebilir.
- İlk değişiklik (predicate'i `foreach`'e çevirmek) tüm input şekillerinde kazanç sağladı: tek elemanlı array 129 ns / 240 byte'tan 93 ns / 144 byte'a indi.
- Collection-aware sürüm dört elemanlı `HashSet<string>` için 225 ns / 280 byte'tan 80 ns / 128 byte'a düşürdü, ama boş lazy enumerable 20 ns'den 31 ns'e çıktı.
- LINQ benchmark'larını target framework olmadan değerlendirmeyin; .NET 10'da `OrderBy(...).Contains(...)` gibi zincirler çok daha hızlı.
- Profiling için decompiler ve allocation profiler çoğu soruyu yanıtlar; assembly okumak yalnızca bounds check, vectorization veya inlining sorusu kaldığında gerekir.
- `Array.Empty<T>()`, `Enumerable.Empty<T>()`, önceden bilinen capacity ve `Enumerable.TryGetNonEnumeratedCount` gibi küçük collection değişikliklerini önce değerlendirin.
Makale, her mesajda çalışan bir hot path'teki birkaç satırın neden incelenmeye değer olduğunu anlatıyor. Receiver, string lock token'ları Guid'e çevirip `_lockedMessages` içinde olup olmadığını `Any` ile kontrol ediyordu. Decompiled koda bakıldığında `Select` için üretilen delegate'in cache'lendiği, ancak `Any`'ye verilen instance-bound `Func<Guid, bool>` predicate'inin her çağrıda yeniden oluşturulduğu görülüyor; bu dönüşüme lowering deniyor.
İlk çözüm predicate'i `foreach` döngüsüne çevirip allocation'ı kaldırıyor ve tüm input şekillerinde kazanç sağlıyor. İkinci denemede metot `IReadOnlyCollection<string>` alıp count bilgisiyle tek seferde array ayırıyor; HashSet ve list'ler hızlanırken lazy enumerable'lar ve büyük array'ler bu değişikliğin bedelini ödüyor. Yazar, .NET 10'da LINQ'in iterator ve `Contains` özel durumlarıyla çok daha iyi olduğunu hatırlatarak benchmark sonuçlarının target framework ile birlikte okunmasını öneriyor.
Gerçek dünyada kullanımı
- Her mesajda çalışan bir Azure Service Bus veya queue receiver'ında micro-optimization yaparken nereden başlanacağını belirlemek
- Yüksek trafikli bir API endpoint'inde allocation profiler çıktısını okuyup hangi LINQ zincirinin gerçekten maliyetli olduğunu ayırmak
- Bir SDK veya kütüphane yazarken public API'yi `IEnumerable<T>` olarak geniş tutup internal metotta collection tipini korumak
- .NET Framework veya eski runtime'dan .NET 10'a geçen bir projede elle yazılmış LINQ replacement'larının hâlâ gerekli olup olmadığını kontrol etmek
Neden önemli
LINQ'i körü körüne değiştirmek, runtime'ın artık kendiliğinden yaptığı optimizasyonları elle yazılmış kodla takas ederek bakım yükü ekleyebilir; doğru sıra profil, decompile ve target framework üzerinde benchmark.