boost :: flat_map ve harita ve unordered_map ile karşılaştırıldığında performansı


104

Önbellek isabetleri nedeniyle bellek yerelliğinin performansı çok artırdığı programlamada yaygın bir bilgidir. Yakın zamanda boost::flat_maphangisinin vektör tabanlı bir harita uygulaması olduğunu öğrendim . Tipikleriniz kadar popüler görünmüyor map/ unordered_mapbu yüzden herhangi bir performans karşılaştırması bulamadım. Nasıl karşılaştırılır ve bunun için en iyi kullanım durumları nelerdir?

Teşekkürler!


Dikkat edilmesi önemli boost.org/doc/libs/1_70_0/doc/html/boost/container/... rastgele sokma iddia n log (toplam O (n, rastgele elemanları dahil ederek) bir destek :: flat_map doldurma ima, logaritmik zaman alır ) zaman. @ V.oddou'nun aşağıdaki cevabındaki grafiklerden de anlaşılacağı gibi yalan söylüyor: rastgele ekleme O (n) ve n tanesi O (n ^ 2) zaman alıyor.
Don Hatch

@DonHatch Bunu buradan bildirmeye ne dersiniz: github.com/boostorg/container/issues ? (karşılaştırmaların sayısını veriyor olabilir, ancak bu, hamle sayısının bir sayımına eşlik etmiyorsa gerçekten yanıltıcıdır)
Marc Glisse

Yanıtlar:


190

Son zamanlarda şirketimde farklı veri yapıları üzerinde bir kıyaslama yaptım, bu yüzden bir kelime bırakmam gerektiğini hissediyorum. Bir şeyi doğru bir şekilde kıyaslamak çok karmaşıktır.

Kıyaslama

Web'de nadiren iyi tasarlanmış bir kıyaslama buluyoruz (eğer varsa). Bugüne kadar sadece gazetecilerin yöntemiyle yapılan ölçütler buldum (oldukça hızlı ve düzinelerce değişkeni halının altına süpüren).

1) Önbellek ısıtmayı düşünmeniz gerekir

Kriterleri çalıştıran çoğu insan, zamanlayıcı tutarsızlığından korkar, bu nedenle işlerini binlerce kez çalıştırırlar ve tüm zamanı alırlar, sadece her işlem için aynı bin kez almaya özen gösterirler ve sonra bunu karşılaştırılabilir olarak değerlendirirler.

Gerçek şu ki, gerçek dünyada bu pek mantıklı değil çünkü önbelleğiniz ısınmayacak ve operasyonunuz muhtemelen sadece bir kez çağrılacak. Bu nedenle, RDTSC kullanarak kıyaslama yapmanız ve onları yalnızca bir kez çağırarak zaman yapmanız gerekir. Intel, RDTSC'nin nasıl kullanılacağını açıklayan bir makale hazırladı (boru hattını temizlemek için bir cpuid talimatı kullanarak ve onu stabilize etmek için programın başında en az 3 kez çağırarak).

2) RDTSC doğruluk ölçüsü

Bunu da yapmanızı tavsiye ederim:

u64 g_correctionFactor;  // number of clocks to offset after each measurement to remove the overhead of the measurer itself.
u64 g_accuracy;

static u64 const errormeasure = ~((u64)0);

#ifdef _MSC_VER
#pragma intrinsic(__rdtsc)
inline u64 GetRDTSC()
{
    int a[4];
    __cpuid(a, 0x80000000);  // flush OOO instruction pipeline
    return __rdtsc();
}

inline void WarmupRDTSC()
{
    int a[4];
    __cpuid(a, 0x80000000);  // warmup cpuid.
    __cpuid(a, 0x80000000);
    __cpuid(a, 0x80000000);

    // measure the measurer overhead with the measurer (crazy he..)
    u64 minDiff = LLONG_MAX;
    u64 maxDiff = 0;   // this is going to help calculate our PRECISION ERROR MARGIN
    for (int i = 0; i < 80; ++i)
    {
        u64 tick1 = GetRDTSC();
        u64 tick2 = GetRDTSC();
        minDiff = std::min(minDiff, tick2 - tick1);   // make many takes, take the smallest that ever come.
        maxDiff = std::max(maxDiff, tick2 - tick1);
    }
    g_correctionFactor = minDiff;

    printf("Correction factor %llu clocks\n", g_correctionFactor);

    g_accuracy = maxDiff - minDiff;
    printf("Measurement Accuracy (in clocks) : %llu\n", g_accuracy);
}
#endif

Bu bir tutarsızlık ölçücüsüdür ve zaman zaman -10 ** 18 (64 bit ilk negatif değerler) elde etmekten kaçınmak için ölçülen tüm değerlerin minimumunu alacaktır.

Satır içi montaj yerine içsel kullanımına dikkat edin. İlk satır içi montaj, günümüzde derleyiciler tarafından nadiren destekleniyor, ancak hepsinden daha kötüsü, derleyici, iç kısmı statik olarak analiz edemediği için satır içi montaj etrafında tam bir sipariş engeli oluşturuyor, bu nedenle bu, özellikle yalnızca öğeleri çağırırken gerçek dünya materyallerini karşılaştırmak için bir sorundur. bir Zamanlar. Dolayısıyla, burada bir içsellik uygundur, çünkü derleyicinin talimatları ücretsiz olarak yeniden sırasını bozmaz.

3) parametreler

Son sorun, insanların genellikle senaryonun çok az varyasyonunu test etmeleridir. Bir kapsayıcı performansı şunlardan etkilenir:

  1. Ayırıcı
  2. içerilen tipin boyutu
  3. kopyalama işleminin uygulama maliyeti, atama işlemi, taşıma işlemi, inşaat işlemi, içerilen tipte.
  4. kaptaki elemanların sayısı (problemin boyutu)
  5. tür önemsiz 3. işlemlere sahiptir
  6. tip POD'dur

Nokta 1 önemlidir, çünkü kapsayıcılar zaman zaman tahsis eder ve CRT "yeni" veya havuz tahsisi veya serbest liste veya diğer gibi kullanıcı tanımlı bir işlemi kullanarak ayırmaları çok önemlidir ...

( pt 1 ile ilgilenen kişiler için , gamedev'de sistem ayırıcı performans etkisi hakkındaki gizemli konuya katılın )

Nokta 2, bazı kapların (örneğin A) etrafındaki şeyleri kopyalarken zaman kaybedeceğidir ve tür ne kadar büyükse, ek yük o kadar büyük olur. Sorun şu ki, başka bir konteyner B ile karşılaştırıldığında, A, küçük tipler için B'yi kazanabilir ve daha büyük tipler için kaybedebilir.

3. nokta, 2. nokta ile aynıdır, ancak maliyeti bazı ağırlık faktörleriyle çarpmasıdır.

Nokta 4, önbellek sorunlarıyla karışık büyük bir sorudur. Bazı kötü karmaşık kapsayıcılar, az sayıdaki türler için düşük karmaşıklık düzeyine sahip kaplardan büyük ölçüde daha iyi performans gösterebilir ( önbellek konumları iyi olduğu, ancak belleği parçalara ayırdığı için mapvs. gibi ). Ve sonra bir kesişme noktasında kaybedecekler, çünkü içerilen toplam boyut ana belleğe "sızmaya" başlıyor ve önbellek ıskalarına neden oluyor, buna ek olarak asimptotik karmaşıklık hissedilmeye başlayabiliyor.vectormap

Nokta 5, derleyicilerin derleme zamanında boş veya önemsiz olan şeyleri elden çıkarabilmeleriyle ilgilidir. Bu, bazı işlemleri büyük ölçüde optimize edebilir, çünkü kapsayıcılar şablonludur, bu nedenle her türün kendi performans profili olacaktır.

Nokta 6, nokta 5 ile aynı, POD'lar, kopya yapısının sadece bir memcpy olduğu gerçeğinden faydalanabilir ve bazı kapsayıcılar, T'nin özelliklerine göre algoritmaları seçmek için kısmi şablon uzmanlıkları veya SFINAE kullanarak bu durumlar için özel bir uygulamaya sahip olabilir.

Düz harita hakkında

Görünüşe göre düz harita, Loki AssocVector gibi sıralı bir vektör sarmalayıcıdır, ancak C ++ 11 ile gelen bazı ek modernizasyonlarla, tek öğelerin eklenmesini ve silinmesini hızlandırmak için hareket semantiğinden yararlanır.

Bu hala düzenli bir konteynerdir. Çoğu insan genellikle sipariş kısmına, dolayısıyla varlığına ihtiyaç duymaz unordered...

Belki bir ihtiyacınız olduğunu düşündünüz mü flat_unorderedmap? hangi bir şey google::sparse_mapveya bunun gibi bir şey olabilir - açık bir adres karma haritası.

Açık adres hash haritalarının sorunu rehash, tahsis edilen veriler olduğu yerde kalırken, standart bir sırasız haritanın sadece hash indeksini yeniden oluşturması gerekiyorken, her şeyi yeni genişletilmiş düz araziye kopyalamak zorunda olmalarıdır. Elbette dezavantajı hafızanın cehennem gibi parçalanmış olmasıdır.

Bir açık adres karma haritasında yeniden çalışmanın kriteri, kapasitenin kova vektörünün boyutunun yük faktörü ile çarpımını aşmasıdır.

Tipik bir yük faktörü 0.8; bu nedenle, buna dikkat etmeniz gerekir, eğer karma haritanızı doldurmadan önce önceden boyutlandırabilirseniz, her zaman önceden boyutlandırabilirseniz: intended_filling * (1/0.8) + epsilonbu, doldurma sırasında her şeyi gizlice yeniden düzenlemeniz ve yeniden kopyalamanız gerekmeyeceğinin garantisini verecektir.

Kapalı adres haritalarının ( std::unordered..) avantajı, bu parametrelerle ilgilenmenize gerek olmamasıdır.

Ancak, boost::flat_mapsıralı bir vektördür; bu nedenle, her zaman bir log (N) asimptotik karmaşıklığa sahip olacaktır, bu da açık adres karma haritasından (amortize edilmiş sabit zaman) daha az iyidir. Bunu da düşünmelisiniz.

Karşılaştırma sonuçları

Bu, farklı haritaları ( intanahtarlı ve __int64/ somestructdeğerli) ve std::vector.

test edilen tür bilgileri:

typeid=__int64 .  sizeof=8 . ispod=yes
typeid=struct MediumTypePod .  sizeof=184 . ispod=yes

Yerleştirme

DÜZENLE:

Önceki sonuçlarımda bir hata vardı: Düz haritalar için çok hızlı bir davranış sergileyen sıralı eklemeyi gerçekten test ettiler.
Bu sonuçları daha sonra bu sayfada bıraktım çünkü ilginçler.
Bu doğru test: rastgele ekle 100

rastgele ekle 10000

Uygulamayı kontrol ettim, burada düz haritalarda uygulanan ertelenmiş sıralama diye bir şey yok. Her ekleme anında sıralama yapar, bu nedenle bu kıyaslama asimptotik eğilimleri sergiler:

map: O (N * log (N))
hashmaps: O (N)
vector and flatmaps: O (N * N)

Uyarı : bundan sonra s için 2 test std::mapve her ikisi flat_mapde hatalı ve aslında sıralı yerleştirmeyi test ediyor (diğer kapsayıcılar için rastgele yerleştirmeye kıyasla. Evet kafa karıştırıcı üzgünüm):
çekincesiz 100 elementli karışık ek

Sıralı yerleştirmenin geri itmeye neden olduğunu ve son derece hızlı olduğunu görebiliriz. Bununla birlikte, kıyaslamamın çizelgesiz sonuçlarından, bunun bir geri ekleme için mutlak optimalliğe yakın olmadığını da söyleyebilirim. 10k elemanlarda, önceden ayrılmış bir vektör üzerinde mükemmel bir geri ekleme optimizasyonu elde edilir. Bu da bize 3 Milyon döngü verir; Buraya sıralı ekleme için 4,8 milyonu gözlemliyoruz flat_map(bu nedenle optimalin% 160'ı).

çekincesiz 10000 elemanlı karışık insert Analiz: Bunun vektör için 'rastgele ekleme' olduğunu unutmayın, bu nedenle devasa 1 milyar döngü, her eklemede verilerin yarısını (ortalama olarak) yukarı doğru (bir öğeye bir öğe) kaydırmaktan kaynaklanır.

3 öğenin rastgele aranması (1'e yeniden normalleştirilmiş saatler)

boyutta = 100

100 element içeren konteyner içinde rand arama

boyutta = 10000

10000 element içeren konteyner içinde rand arama

Yineleme

100'den büyük (yalnızca MediumPod tipi)

100'den fazla orta bölme yineleme

10000'den büyük (sadece MediumPod tipi)

10000 orta bölme üzerinde yineleme

Son tuz tanesi

Sonunda "Benchmarking §3 Pt1" (sistem ayırıcı) üzerine geri dönmek istedim. Yakın zamanda yaptığım bir deneyde , geliştirdiğim bir açık adres hash haritasının performansıyla ilgili olarak yapıyorum , bazı std::unordered_mapkullanım durumlarında Windows 7 ve Windows 8 arasında% 3000'den fazla bir performans farkı ölçtüm ( burada tartışılmıştır ).
Bu da okuyucuyu yukarıdaki sonuçlar hakkında uyarmak istememe neden oluyor (bunlar Win7'de yapıldı): kilometreniz değişebilir.

Saygılarımla


1
oh, bu durumda mantıklı. Vector'un sabit amortisman süresi garantileri, yalnızca sonuna eklerken geçerlidir. Rastgele konumlarda yerleştirme, ekleme noktasından sonraki her şeyin ileriye taşınması gerektiğinden, ekleme başına ortalama O (n) olmalıdır. Bu nedenle, karşılaştırmanızda, küçük N için bile oldukça hızlı patlayan ikinci dereceden davranış beklemekteyiz. AssocVector tarzı uygulamalar muhtemelen sıralamayı, örneğin her eklemeden sonra sıralamak yerine, bir arama gerekene kadar erteler. Kriterinizi görmeden söylemek zor.
Billy ONeal

1
@BillyONeal: Ah, bir iş arkadaşımızla kodu inceledik ve suçluyu bulduk, "rastgele" eklemem sipariş edildi çünkü eklenen anahtarların benzersiz olduğundan emin olmak için std :: seti kullandım. Bu basit bir aptallıktır, ancak bunu bir random_shuffle ile düzelttim, şimdi yeniden oluşturuyorum ve bazı yeni sonuçlar bir düzenleme yapıldıktan sonra görünecek. Dolayısıyla, mevcut durumundaki test, "sıralı yerleştirmenin" çok hızlı olduğunu kanıtlıyor.
v.oddou

3
"Intel kağıdı olan" ← ve burada o edilmektedir
isomorphismes

5
Belki de bir şey bariz eksik ama rastgele arama daha yavaş olmasının nedeni anlamıyorum flat_mapkıyasla std::map- Herkes bu sonucu açıklar mı?
boycy

1
Bunu, bu seferin destek uygulamasının belirli bir ek yükü olarak açıklayacağım flat_map, bir konteyner olarak özel bir karakter olarak değil . Çünkü Aska::sürüm, aramadan daha hızlı std::map. Optimizasyon için yer olduğunu kanıtlıyor. Beklenen performans asimptotik olarak aynıdır, ancak önbellek konumu sayesinde biraz daha iyi olabilir. Yüksek boyutlu setlerle birleşmeleri gerekir.
v.oddou

6

Dokümanlardan, Loki::AssocVectorbu benim oldukça yoğun bir kullanıcı olduğuma benziyor gibi görünüyor . Bir vektöre dayandığından, bir vektörün özelliklerine sahiptir, yani:

  • Yineleyiciler, sizeötesine geçtiğinde geçersiz kılınmaktadır capacity.
  • Daha fazla büyüdüğünde capacity, nesneleri yeniden tahsis etmesi ve hareket ettirmesi gerekir, yani yerleştirme, özel durum haricinde sabit bir süre garanti edilmez end.capacity > size
  • Arama daha hızlı olduğu std::mapnedeniyle önbellek localityve aynı performans özelliklerine sahip bir ikili arama std::mapaksi
  • Bağlı bir ikili ağaç olmadığı için daha az bellek kullanır
  • Zorla söylemediğiniz sürece asla küçülmez (çünkü bu yeniden tahsisi tetikler)

En iyi kullanım, öğelerin sayısını önceden bildiğiniz (böylece önceden yapabilirsiniz reserve) veya ekleme / çıkarma nadir olduğunda, ancak aramaların sık olduğu zamandır. Yineleyici geçersiz kılma, bazı kullanım durumlarında onu biraz hantal hale getirir, bu nedenle program doğruluğu açısından birbirinin yerine geçemezler.


1
false :) yukarıdaki ölçümler haritanın bulma işlemleri için flat_map'ten daha hızlı olduğunu gösteriyor, sanırım ppl'nin uygulamayı düzeltmesi gerekiyor, ancak teoride haklısınız.
NoSenseEtAl
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.