Primitive vs strongly typed dictionary keys (frozen collections ve BenchmarkDotNet)

Giriş

dondurulmuş koleksiyonlar .NET’e yakın zamanda eklendi, FrozenDictionary Ve FrozenSet, merak ettim:

Öncelikle bir liste için dondurulmanın ne demek olduğunu tartışalım . Bunu da .net8 alfa sürümüyle çalışan bazı kodlarla yapıyoruz:

List<int> normalList = new List<int> { 1, 2, 3 };
ReadOnlyCollection<int> readonlyList = normalList.AsReadOnly();
FrozenSet<int> frozenSet = normalList.ToFrozenSet();
ImmutableList<int> immutableList = normalList.ToImmutableList();

normalList.Add(4);

Console.WriteLine($"List count: {normalList.Count}");
Console.WriteLine($"ReadOnlyList count: {readonlyList.Count}");
Console.WriteLine($"FrozenSet count: {frozenSet.Count}");
Console.WriteLine($"ImmutableList count: {immutableList.Count}");

Hangisi aşağıdakileri yazdıracaktır:

List count: 4
ReadOnlyList count: 4
FrozenSet count: 3
ImmutableList count: 3

Performans Durumu ve  Senaryolar

Kurulum çok karmaşık değil ancak birden fazla senaryoyu test etmek istediğim için en basit şey de değil. Örnek olarak şuna odaklanmaya başladım: strings, muhtemelen sözlük anahtarı olarak en sık kullanıldığını gördüğüm türdür, ancak çok yaygın olduğu göz önüne alındığında, belki de özel olarak buna yönelik özel optimizasyonlar vardır ve daha iyi bir anlayış için belki de diğer türlerle bazı şeyleri denemek uygun olabilir diye düşündüm. aynı zamanda (PS: şuna hızlı bir bakış: kaynak kodu optimize edilmiş özel durumların olduğu yönündeki şüpheleri doğrulamaktadır).

Yani özetle test edilen varyasyonlar:

  • Farklı anahtar türleri: string, int Ve Guidbunların sarılmış sürümlerinin yanı sıra (yani, gerçek bir uygulamada belirli etki alanı kavramlarını temsil edecek özel türler)
    • Sarmalayıcılar salt okunur kayıt yapıları olarak uygulandı
  • Çeşitli miktarlarda girişler – 1, 10, 100, 1000 ve 10000

Paketleyici şuna benzer:

1
public  readonly  record  struct StronglyTypedKey<T>(T Value);

Temel kıyaslama uygulaması şuna benzer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class BaseImplementation<T> where T : notnull  
{  
    private readonly T _toLookup;  
    private readonly StronglyTypedKey<T> _toLookupStronglyTyped;  
    private readonly Dictionary<T, string> _traditional;  
    private readonly Dictionary<StronglyTypedKey<T>, string> _traditionalWithStronglyTypedKey;  
    private readonly Dictionary<StronglyTypedKey<T>, string> _traditionalWithStronglyTypedKeyWithCustomComparer;  
    private readonly FrozenDictionary<T, string> _frozen;  
    private readonly FrozenDictionary<StronglyTypedKey<T>, string> _frozenWithStronglyTypedKey;  
    private readonly FrozenDictionary<StronglyTypedKey<T>, string> _frozenWithStronglyTypedKeyWithCustomComparer;  
  
    public BaseImplementation(
      int n,
      IEqualityComparer<StronglyTypedKey<T>> equalityComparer,
      Func<int, T> factory)  
    { 
      var contents = Enumerable.Range(0, n).Select(factory).ToArray();  
      _toLookup = contents.Last();  
      _toLookupStronglyTyped = new StronglyTypedKey<T>(_toLookup);  
      _traditional = contents.ToDictionary(x => x, x => x.ToString()!);  
      _traditionalWithStronglyTypedKey = contents.ToDictionary(x => new StronglyTypedKey<T>(x), x => x.ToString()!);  
      _traditionalWithStronglyTypedKeyWithCustomComparer
        = contents.ToDictionary(x => new StronglyTypedKey<T>(x), x => x.ToString()!, equalityComparer);  
      _frozen = _traditional.ToFrozenDictionary();  
      _frozenWithStronglyTypedKey = _traditionalWithStronglyTypedKey.ToFrozenDictionary();  
      _frozenWithStronglyTypedKeyWithCustomComparer 
        = _traditionalWithStronglyTypedKey.ToFrozenDictionary(equalityComparer);
    }
    
    public string LookupTraditional() => _traditional[_toLookup];  
  
    public string LookupTraditionalWithStronglyTypedKey() => _traditionalWithStronglyTypedKey[_toLookupStronglyTyped];  
  
    public string LookupTraditionalWithStronglyTypedKeyWithCustomComparer()  
        => _traditionalWithStronglyTypedKeyWithCustomComparer[_toLookupStronglyTyped];  
  
    public string LookupFrozen() => _frozen[_toLookup];  
  
    public string LookupFrozenWithStronglyTypedKey() => _frozenWithStronglyTypedKey[_toLookupStronglyTyped];  
  
    public string LookupFrozenWithStronglyTypedKeyWithCustomComparer()  
        => _frozenWithStronglyTypedKeyWithCustomComparer[_toLookupStronglyTyped];  
}

Sonuçlar

Sonuçlara!

Karşılaştırma yapmak için kurduğum senaryoların sayısı göz önüne alındığında, sonuçların çıktısı az değil, bu yüzden sadece bulguları özetleyeceğim ve buraya birkaç ekstra önemli nokta ekleyeceğim. Sonuçların tamamı için şuraya göz atabilirsiniz: GitHub deposu.

Sözlükteki girişlerin miktarına bağlı olarak bir miktar değişiklik olsa da, 100 girişli yapılandırmayı referans olarak kullanalım, çünkü bu, genel sonuçları temsil ediyor gibi görünüyor. stringS.

string

MethodNMeanErrorStdDevRatioRatioSDRank
LookupTraditionalString10011.9771 ns0.0137 ns0.0122 ns1.000.002
LookupTraditionalWithStronglyTypedKeyString10030.8755 ns0.1421 ns0.1187 ns2.580.016
LookupTraditionalWithStronglyTypedKeyStringWithCustomComparer10027.1268 ns0.1055 ns0.0881 ns2.260.014
LookupFrozenString1004.7020 ns0.0071 ns0.0059 ns0.390.001
LookupFrozenWithStronglyTypedKeyString10029.2874 ns0.2304 ns0.2155 ns2.440.025
LookupFrozenWithStronglyTypedKeyStringWithCustomComparer10026.6226 ns0.2129 ns0.1992 ns2.220.023

Bu sonuçlarda açıkça görebileceğimiz gibi, sarmalayıcı tipini kullanarak string hem geleneksel hem de dondurulmuş sözlükler için arama performansını düşürür.

Beklendiği gibi doğrudan string Dondurulmuş sözlüğü kullanırken geleneksel sözlükle karşılaştırıldığında arama daha hızlıdır. Bununla birlikte, güçlü bir şekilde yazılan anahtar devreye girdiğinde, iki sözlük türü arasında neredeyse hiçbir fark yoktur; bu nedenle, donmuş sözlükler tarafından yapılan optimizasyonlar, özel türlere iyi bir şekilde tercüme edilmiyor gibi görünüyor (belki de özel bir türü uyarlamanın bir yolu vardır). bu optimizasyonlarla daha iyi oynadığınızdan emin değil misiniz, daha fazla araştırma yapmadınız).

Son olarak özel eşitlik karşılaştırıcısının kullanımına kısaca değinelim. Giriş sayısının birden fazla olduğu tüm kıyaslamalarda, özel anahtar için varsayılan eşitlik karşılaştırıcısından tutarlı bir şekilde daha hızlıydı, ancak görebildiğimiz gibi fark özellikle etkileyici değil.

Şimdi sonuçlara kısaca göz atalım int.

int

MethodNMeanErrorStdDevRatioRatioSDRank
LookupTraditionalInt1003.1269 ns0.1041 ns0.1157 ns1.000.004
LookupTraditionalWithStronglyTypedKeyInt1002.9621 ns0.0774 ns0.0724 ns0.950.033
LookupTraditionalWithStronglyTypedKeyIntWithCustomComparer1003.3904 ns0.0658 ns0.0616 ns1.090.045
LookupFrozenInt1001.3530 ns0.0182 ns0.0171 ns0.440.021
LookupFrozenWithStronglyTypedKeyInt1002.2000 ns0.0214 ns0.0190 ns0.710.022
LookupFrozenWithStronglyTypedKeyIntWithCustomComparer1003.1473 ns0.0303 ns0.0283 ns1.010.044

Bu sonuçlara bakıldığında, karşılaştırma yapıldığında bazı benzerliklerin olduğu görülmektedir. stringama aynı zamanda bazı farklılıklar da var.

Yeni başlayanlar için, farklılıklar açısından tüm sonuçlar birbirine daha yakındır. int için olduğundan string. Ayrıca, özel türde bir anahtarın kullanımının o kadar etkili olmadığı görülüyor; geleneksel sözlük sonuçları birbirine çok yakın ve donmuş sözlük sonuçları biraz daha mesafe gösteriyor ancak yine de sayılar kadar net değil. stringS. Ek olarak, özel eşitlik karşılaştırıcısı, aşağıdaki durumlarda sayıları marjinal olarak iyileştirirken strings, onları daha da kötüleştiriyor intS.

arasındaki tek önemli benzerlik string Ve int öyle görünüyor ki her iki durumda da sarılmamış değerler için donmuş sözlük geleneksel sözlüğe göre performansı artırdı.

Şimdi nasıl olduğunu görelim Guid karşılaştırır string Ve int.

Rehber

MethodNMeanErrorStdDevRatioRatioSDRank
LookupTraditionalGuid1003.6592 ns0.0609 ns0.0570 ns1.000.001
LookupTraditionalWithStronglyTypedKeyGuid1004.1773 ns0.0143 ns0.0119 ns1.140.023
LookupTraditionalWithStronglyTypedKeyGuidWithCustomComparer1004.4013 ns0.0726 ns0.0679 ns1.200.014
LookupFrozenGuid1003.8696 ns0.1064 ns0.0944 ns1.060.032
LookupFrozenWithStronglyTypedKeyGuid1003.6643 ns0.0985 ns0.0922 ns1.000.031
LookupFrozenWithStronglyTypedKeyGuidWithCustomComparer1004.6254 ns0.0144 ns0.0120 ns1.260.025

Genel olarak, sonuçlar gördüğümüze yakın intsonuçlar çok yakın, özel tür anahtarı kullanmanın çok fazla etkisi yok, ancak orada bir şey var ve karışıma özel bir eşitlik karşılaştırıcısı eklendiğinde daha da kötüleşiyor.

Şimdi nerede Guid her ikisinden de farklıdır string Ve intSonuçların hemen hemen aynı olması nedeniyle, donmuş bir sözlük kullanmanın geleneksel olana göre bir avantajı yok gibi görünüyor.

Bitirmeden önce sonuçlardan birkaç değerli söz edelim.

Diğer notlar

  • 100, 1000 ve 10000 giriş için sonuçlar daha önce açıklanana benzer bir modeli izler.
  • 10 giriş için dondurulmuş sözlüğün geleneksel sözlükten daha kötü performans gösterdiği çeşitli durumlar vardır
  • 1 giriş için, özellikle özel türdeki anahtar senaryosu için dondurulmuş bir sözlük, geleneksel sözlüğe kıyasla yürütme süresinde daha belirgin bir azalmaya sahiptir. Bu gelişme muhtemelen 10’dan az girişe sahip sözlükler için uygulanan özel durumdan kaynaklanmaktadır.

Çıkış

Bu, bazı farklı türlerin arama performansı açısından sözlük anahtarları olarak nasıl karşılaştırıldığına hızlı bir bakış için bunu yapar; string, int Ve Guidve bunların sarılmış versiyonları.

Beklenen bazı sonuçları gördük: string Ve int temel arama, dondurulmuş sözlüklerle karşılaştırıldığında geleneksel sözlüklerle daha iyi performans gösteriyor; bazı beklenmedik sonuçlar da var: Guid sözlük türünden bağımsız olarak performans çoğunlukla aynı ve ayrıca, en azından benim için, özel türleri sözlük anahtarları olarak kullanmanın etkisine ilişkin ne beklenen ne de beklenmedik sonuçlar (ne bekleyeceğime dair hiçbir fikrim yoktu, dolayısıyla tüm bu araştırma 😅).

Her zaman ilginç olan şey, bu kriterleri oluştururken ve sonuçları analiz ederken, bazı şeylerin neden bu şekilde davrandığını daha iyi anlamak için çalışma zamanının kaynak koduna, özellikle de dondurulmuş sözlüğe bakma fırsatını da kullanmasıdır. Örneğin, kodu inceleyerek sözlük boyutu, bilinen türler, özel sürümler gibi farklı faktörlere bağlı özel durumların varlığını görebiliriz. stringaynı zamanda intve çok daha fazlası.

Hatırlanması gereken bir diğer önemli nokta da, ortaya çıkan sayılardan da fark edebileceğiniz gibi, nanosaniye mertebesindeyiz ki bu çok çok hızlıdır. Çoğu durumda uygulama geliştiricilerin bu konuda endişelenmelerine gerek olmadığını düşünüyorum, ancak sıcak yolda bir kod parçası varsa belki de bir göz atmaya değer olabilir. Ayrıca değmese bile en azından bunları öğrenmek eğlenceli 🙂.

İlgili bağlantılar:

Uğradığınız için teşekkürler cyaz! 👋

Total
0
Shares
0 0 votes
Article Rating
Subscribe
Bildir
guest

0 Yorum
Inline Feedbacks
View all comments
Previous Post

Almanya Huawei ve ZTE'yi 5G Ağından Yasaklamayı Planlıyor | Dijital saat

Next Post

C# Nedir?

0
Would love your thoughts, please comment.x