Yüksek trafikli .NET sistemlerinde performans ve thread güvenliği (thread-safety) kritik öneme sahiptir. Veri yapıları arasındaki küçük farklar, saniyede binlerce isteğin işlendiği senaryolarda CPU ve bellek kullanımında büyük farklar yaratabilir.
.NET içinde kullanılan Dictionary yapıları verilerin verimli bir şekilde saklamasına ve geri almasına olanak tanıyan temel sınıftır. TKey (benzersiz) ve TValue alarak bir liste oluşturur. Dictionary kullanmanın en büyük avantajı hızlı arama imkanı sunmasıdır. Çünkü her elemanın benzersiz bir anahtarı vardır.
Yüksek trafikli bir sistemde “Dictionary” dediğimizde, aslında verinin değiştirilebilirliği (mutability) ve erişim hızını tartışıyoruz.
Cloned Dictionary
“Cloned Dictionary” aslında .NET literatüründe bir sınıf ismi değildir, bu en çok karıştırılan şeydir. Yüksek trafikli sistemlerde kullanılan bir tasarım desenidir aslında. Özünde standart bir Dictionary nesnesinin o anki halinin tam bir kopyasını alıp, orijinalinden bağımsız bir instance oluşturmayı ifade eder. Yani mevcut bir öğenin kopyalanarak (örneğin new Dictionary(existingDict)) oluşturulmasıdır.
var original = new Dictionary<int, string> { { 1, "Data" } };
var clone = new Dictionary<int, string>(original);
var filteredClone = original.Where(x => x.Key > 0).ToDictionary(x => x.Key, x => x.Value);Bu yapı tamamen değiştirilebilirdir ancak her kopyalama işlemi bellekte yeni bir alan kaplar. Eğer toplu bir güncelleme yapıp sonra sadece okuyacaksanız, bir Dictionary oluşturup kopyalamak çok daha CPU dostu olur.
Dezavantajı ise sözlükteki eleman sayısı arttıkça kopyalama kaynağı (allocation) giderek artar. Her işlemde bellekte devasa yeni alanlar ayrılır
Immutable Dictionary
.NET sisteminde standart Dictionary yapısı mutable (yani değiştirilebilir) bir karakterdedir. İçeriği dinamik olarak güncellenebilir. Fakat ImmutableDictionary immutable (değişmez) bir yapıya sahiptir. Verinin bütünlüğünü korur. Bu yapıda gerçekleştirilen her güncelleme işlemi, orijinal Dictionary yapısını değiştirmek yerine, değişikliği içeren yeni bir instance döndürür. Bu yapıya biraz değinelim.
Değeri değiştirilemeyen (immutable) dictionary, oluşturulduktan sonra içeriği değiştirilemez. Yani bir dictionary tanımlandıktan sonra mevcut anahtarların değerleri güncellenemez, yeni anahtar eklenemez ve var olan anahtarlar silinemez. Eğer bir değişiklik yapılmak istenirse, mevcut dictionary korunur ve yapılan değişikliği içeren yeni bir dictionary örneği (yeni bir referans) döndürür. Böylece verinin her zaman sabit kalmasını garanti eder.
Bu yapı .NET 8 ile birlikte System.Collections.Immutable kütüphanesi içinde gelir. Veri yapısı hiçbir şekilde değiştirilemez.
ImmutableDictionary yapısında bir veriyi dışarıya açtığınızda, verinin kendisi zaten değişmez olduğu için bu işlem sadece bir bellek adresini (referans) iletmekten ibarettir. Kaynak maliyetiniz sıfır gibidir. Ancak bu yapının değişmezlik kuralı, her yeni eleman eklendiğinde mevcut yapıyı güncellemek yerine, bellekte o değişikliği içeren yepyeni nesnelerin tahsis edilmesini (allocation) zorunlu kılar. Buradaki yazma işlemlerinde ciddi performans yükü oluşturur. Ayrıca bu koleksiyonlar standart sözlüklerin kullandığı yüksek hızlı “hash table” mimarisi yerine hiyerarşik bir “tree” yapısı üzerine kuruludur. Bu yapıda aranan bir key için doğrudan erişmek yerine dallar arasında dolaşıyorsunuz. Bu da ağır olmasına yol açar.

Immutable yapıların kullanılmasının en önemli nedenlerinden biri thread-safety problemidir. Normal bir dictionary yapısı gereği birden fazla thread tarafından aynı anda erişildiğinde senkronizasyon gerektirir. Concurrent istekler geleceği için lock olmalı. Bunlar performans düşüşüne yol açar. Immutable dictionary içinde veriler hiçbir zaman değişmediği için birden fazla thread tarafından güvenle okunabilir.
Kullanım örneklerini gösterelim.
using System.Collections.Immutable;
var dict = ImmutableDictionary<int, string>.Empty
.Add(1, "Domain")
.Add(2, "Hosting");
var source = new Dictionary<int, string> { { 3, "Cloud" } };
ImmutableDictionary<int, string> immutableDict = source.ToImmutableDictionary();Builder ile daha performanslı da oluşturabilirsiniz.
var builder = ImmutableDictionary.CreateBuilder<int, string>();
builder.Add(1, "EPP");
builder.Add(2, "API");
builder.Add(3, "XML");
ImmutableDictionary<int, string> finalDict = builder.ToImmutable();Frozen Dictionary
.NET için yeni bir sınıf olan FrozenDictionary, özellikle yüksek trafikli sistemlerdeki “nadiren yazılan ama sürekli okunan” veri setleri için optimize edilmiş bir yapıdır. Üzerinde epey emek harcanan bir yapı.
Normal bir Dictionary her zaman yeni veri eklenebileceğini varsayar, bu yüzden çakışmaları yönetmek için esnek ama daha yavaş bir yapı kullanır. FrozenDictionary ise oluşturulurken içindeki tüm verileri bilir. Bu sayede her bir key hash tablosunda mükemmel bir şekilde konumlandıracak özel bir algoritma çalıştırır. Okuma sırasında çakışma kontrolü yapmaya gerek kalmaz çünkü verinin yerini anında bulur.
FrozenDictionary verileri bellekte birbirine yakın ve CPU önbelleğine sığacak şekilde kaydeder. Yapı dondurulduktan sonra nesnenin yapısı sabittir. Binlerce thread aynı anda okuma yaparken hiçbir lock veya takip mekanizması çalışmaz. Bu da olağanüstü bir hız sağlıyor.
Bu hızın bedeli var ebette. ToFrozenDictionary() metodunu çağırdığınızda .NET arka planda tüm anahtarları analiz eder ve en verimli hash fonksiyonunu hesaplar. Bellekte optimize edilmiş yeni bir tablo kurar. Dolayısıyla elinizde binlerce kayıt varsa ve siz bunu sürekli olarak dondurmaya çalışıyorsanız ciddi bir yazma maliyeti oluşturur.
Örnek olması için FrozenDictionary kullanarak bir cache sınıfı oluşturalım.
using System.Collections.Frozen;
public class LocalizationProvider
{
// Created once, read by thousands of threads throughout the app lifecycle.
private static readonly FrozenDictionary<string, string> ErrorRegistry;
static LocalizationProvider()
{
var sourceData = new Dictionary<string, string>
{
{ "AUTH_001", "Invalid Credentials" },
{ "AUTH_002", "Token Expired" },
{ "SYS_001", "Internal Server Error" }
};
// .NET performs a deep analysis here to create a "Perfect Hash Table".
ErrorRegistry = sourceData.ToFrozenDictionary();
}
public string GetMessage(string code)
{
// Read speed is significantly faster than a standard
return ErrorRegistry.GetValueOrDefault(code, "Unknown Error");
}
}Benchmark
Tercih için basit bir benchmark sınıfı oluşturdum. Deneyebilirsiniz.
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Collections.Immutable;
using System.Collections.Frozen;
namespace DictionaryBenchmarks
{
[MemoryDiagnoser]
public class HighTrafficBenchmarks
{
private Dictionary<int, string> _sourceData;
// 1. Source for Cloned Dictionary scenario
private Dictionary<int, string> _standardDict;
// 2. Instance for Immutable Dictionary scenario
private ImmutableDictionary<int, string> _immutableDict;
// 3. Instance for Frozen Dictionary scenario
private FrozenDictionary<int, string> _frozenDict;
private int _searchKey;
[GlobalSetup]
public void Setup()
{
// Prepare a dataset with 10,000 elements
_sourceData = Enumerable.Range(1, 10000)
.ToDictionary(i => i, i => $"Value-{i}");
_standardDict = new Dictionary<int, string>(_sourceData);
_immutableDict = _sourceData.ToImmutableDictionary();
_frozenDict = _sourceData.ToFrozenDictionary();
// Look up a key in the middle of the range
_searchKey = 5000;
}
// SCENARIO 1: User permissions (cloning)
[Benchmark]
public Dictionary<int, string> Clone_UserPermissions_Benchmark()
{
return new Dictionary<int, string>(_standardDict);
}
// SCENARIO 2: Metadata tags (thread-safe read)
[Benchmark]
public string Immutable_MetadataTags_Lookup()
{
return _immutableDict[_searchKey];
}
// SCENARIO 3: Currency rates (ultra-fast read)
[Benchmark]
public string Frozen_CurrencyRates_Lookup()
{
return _frozenDict[_searchKey];
}
}
public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<HighTrafficBenchmarks>();
}
}
}Cloned Dictionary için user permissions örneği uyguladık. İşlem “new Dictionary(_standardDict)” yaptırmak. Bu testte okuma değil oluşturma maliyetini ölçüyoruz. Yüksek trafikte her istekte öğeyi clone yapmanın ne kadar maliyetli olacağını bu benchmark ile görebilirsiniz.
Immutable Dictionary için metadata tags örneği verdim. _immutableDict[_searchKey]” kullanarak okuma hızını ölçüyoruz. “Tree” yapısı kullanıldığı için sonuçların FrozenDictionary’den daha yavaş ama thread-safe olduğunu göreceksiniz.
Frozen Dictionary için currency rates örneği verdim. “_frozenDict[_searchKey]” ile okuma örneği gösterdik. Okuma işlemi perfect hashing ile yapıldığı için benchmark sonuçlarında en düşük değeri bu metot veriyor. Global, sabit ve uygulamanın her yerinden saniyede milyonlarca kez sorgulanan referans verilerde kesinlikle tercih edilmelidir.

Sonuç
Yüksek trafikli .NET uygulamalarında performans elde etmek yalnızca doğru kodu yazmakla değil, verinin yaşam döngüsüne en uygun veri yapısını seçmekle ilgilidir.
Eğer elinizdeki veri uygulama ömrü boyunca hiç değişmeyen sabit referanslardan oluşuyorsa (ISO kodları, dil veya ülke listesi gibi) veya verileriniz periyodik olarak güncelleniyorsa (döviz kurları gibi) .NET 8 ile gelen FrozenDictionary’nin sunduğu “perfect hash” optimizasyonu tartışmasız en hızlı ve verimli çözümdür.
Öte yandan verinin sıkça değiştiği ve thread-safety yapısının daha önemli olduğu fonksiyonel mimarilerde ImmutableDictionary devreye girmelidir.
Verinin yazma maliyetini sorun etmiyor ve en hızlı read yöntemi istiyorsanız Frozen tercih etmelisiniz. Veri bütünlüğünü her adımda korumak istiyorsanız Immutable olmalıdır.



Yorum bırakın