Gcc std :: unordered_map uygulaması yavaş mı? Öyleyse neden?


100

C ++ ile yüksek performanslı kritik bir yazılım geliştiriyoruz. Orada eşzamanlı bir hash haritasına ihtiyacımız var ve bir tane uyguluyoruz. Bu nedenle, eşzamanlı hash haritamızın ne kadar yavaş olduğunu anlamak için bir kıyaslama yazdık std::unordered_map.

Ancak, std::unordered_mapinanılmaz derecede yavaş görünüyor ... Yani bu bizim mikro ölçütümüz (eşzamanlı harita için kilitlemenin optimize edilmediğinden emin olmak için yeni bir iş parçacığı oluşturduk ve asla 0 eklemediğimi çünkü aynı zamanda kıyaslama yapıyorum google::dense_hash_map, boş bir değere ihtiyaç duyan):

boost::random::mt19937 rng;
boost::random::uniform_int_distribution<> dist(std::numeric_limits<uint64_t>::min(), std::numeric_limits<uint64_t>::max());
std::vector<uint64_t> vec(SIZE);
for (int i = 0; i < SIZE; ++i) {
    uint64_t val = 0;
    while (val == 0) {
        val = dist(rng);
    }
    vec[i] = val;
}
std::unordered_map<int, long double> map;
auto begin = std::chrono::high_resolution_clock::now();
for (int i = 0; i < SIZE; ++i) {
    map[vec[i]] = 0.0;
}
auto end = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
std::cout << "inserts: " << elapsed.count() << std::endl;
std::random_shuffle(vec.begin(), vec.end());
begin = std::chrono::high_resolution_clock::now();
long double val;
for (int i = 0; i < SIZE; ++i) {
    val = map[vec[i]];
}
end = std::chrono::high_resolution_clock::now();
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
std::cout << "get: " << elapsed.count() << std::endl;

(DÜZENLEME: kaynak kodunun tamamı burada bulunabilir: http://pastebin.com/vPqf7eya )

Sonuç std::unordered_mapşudur:

inserts: 35126
get    : 2959

Şunun için google::dense_map:

inserts: 3653
get    : 816

El destekli eşzamanlı haritamız için (ölçüt tek iş parçacıklı olmasına rağmen kilitleme yapar - ancak ayrı bir spawn dizisinde):

inserts: 5213
get    : 2594

Kıyaslama programını pthread desteği olmadan derler ve her şeyi ana iş parçacığında çalıştırırsam, el destekli eşzamanlı haritamız için aşağıdaki sonuçları alırım:

inserts: 4441
get    : 1180

Aşağıdaki komutla derliyorum:

g++-4.7 -O3 -DNDEBUG -I/tmp/benchmap/sparsehash-2.0.2/src/ -std=c++11 -pthread main.cc

Bu nedenle, özellikle eklemeler std::unordered_mapson derece pahalı görünüyor - diğer haritalar için 35 saniyeye karşılık 3-5 saniye. Ayrıca arama süresi oldukça yüksek görünüyor.

Sorum: bu neden? Stackoverflow hakkında birisinin sorduğu başka bir soruyu okudum, neden std::tr1::unordered_mapkendi uygulamasından daha yavaş? En yüksek puan alan yanıtlar, std::tr1::unordered_mapdaha karmaşık bir arayüzün uygulanması gerektiğini belirtir . Ancak bu argümanı göremiyorum: concurrent_map'imizde bir paket yaklaşımı std::unordered_mapkullanıyoruz, bir paket yaklaşımı da kullanıyoruz ( google::dense_hash_mapdeğil, ama std::unordered_mapen azından el destekli eşzamanlılık güvenli sürümümüz kadar hızlı olmalı mı?). Bunun dışında arayüzde hash haritasının kötü performans göstermesine neden olan bir özelliği zorlayan hiçbir şey göremiyorum ...

Öyleyse sorum: std::unordered_mapçok yavaş göründüğü doğru mu? Hayır ise: sorun nedir? Cevabınız evet ise: bunun nedeni nedir?

Ve asıl sorum: neden std::unordered_mapbu kadar pahalıya bir değer eklemek bu kadar pahalı (başlangıçta yeterince yer ayırsak bile, çok daha iyi performans göstermiyor - bu yüzden yeniden düzenleme sorun değil gibi görünüyor)?

DÜZENLE:

Her şeyden önce: evet, sunulan kıyaslama kusursuz değil - bunun nedeni, onunla çok fazla oynadığımız ve sadece bir hack olduğudur (örneğin, uint64int üretecek dağıtım pratikte iyi bir fikir olmayacaktır, bir döngüde 0'ı hariç tutun aptalca vb ...).

Şu anda çoğu yorum, unordered_map'i bunun için yeterli alanı önceden ayırarak daha hızlı hale getirebileceğimi açıklıyor. Uygulamamızda bu mümkün değildir: bir veritabanı yönetim sistemi geliştiriyoruz ve bir işlem sırasında bazı verileri depolamak için bir hash haritasına ihtiyacımız var (örneğin, bilgilerin kilitlenmesi). Dolayısıyla bu harita 1'den (kullanıcı sadece bir ekleme ve taahhütte bulunur) milyarlarca girişe (tam tablo taramaları gerçekleşirse) kadar her şey olabilir. Burada yeterince alan ayırmak imkansızdır (ve sadece başlangıçta çok fazla alan ayırmak çok fazla bellek tüketecektir).

Dahası, sorumu yeterince açık bir şekilde belirtmediğim için özür dilerim: unordered_map'i hızlı yapmakla gerçekten ilgilenmiyorum (googles yoğun hash haritasının kullanılması bizim için iyi çalışıyor), bu büyük performans farklılıklarının nereden geldiğini gerçekten anlamıyorum . Bu sadece önceden tahsis olamaz (yeterince önceden tahsis edilmiş bellek olsa bile, yoğun harita, sıralı olmayan haritadan daha hızlı bir büyüklük sırasıdır, el destekli eşzamanlı haritamız 64 boyutunda bir diziyle başlar - yani unordered_map'ten daha küçüktür).

Peki bu kötü performansın sebebi std::unordered_mapnedir? Veya farklı bir şekilde sorulursa: std::unordered_mapArayüzün standart uyumlu ve (neredeyse) googles yoğun hash haritası kadar hızlı bir uygulaması yazılabilir mi? Veya standartta, uygulayıcıyı onu uygulamak için verimsiz bir yol seçmeye zorlayan bir şey var mı?

DÜZENLEME 2:

Profil oluşturarak tam sayı bölmeleri için çok zaman kullanıldığını görüyorum. std::unordered_mapdizi boyutu için asal sayıları kullanırken, diğer uygulamalar ikinin gücünü kullanır. Neden std::unordered_mapasal sayılar kullanılır? Hash kötü ise daha iyi performans için mi? İyi karmalar için hiçbir fark yaratmaz.

DÜZENLEME 3:

Bunlar için numaralar std::map:

inserts: 16462
get    : 16978

Sooooooo: neden bir içine eklemeden std::mapdaha hızlı bir std::unordered_mapekleniyor ... yani WAT? std::mapdaha kötü bir yerelliğe sahiptir (ağaç ve dizi), daha fazla ayırma yapması gerekir (ekleme başına, yeniden çalıştırma başına + artı her çarpışma için ~ 1) ve en önemlisi: başka bir algoritmik karmaşıklığa sahiptir (O ​​(logn) ve O (1))!


1
Standarttaki kapların çoğu tahminleri ile ÇOK muhafazakar, kullandığınız kova sayısına (kurucuda belirtilir) bir göz atarım ve bunu sizin için daha iyi bir tahmine yükseltirim SIZE.
Ylisar

Intel TBB'den concurrent_hash_map'i denediniz mi? threadingbuildingblocks.org/docs/help/reference/…
MadScientist

1
@MadScientist TBB'yi düşündük. Sorun lisanslama: bu bir araştırma projesidir ve onu nasıl yayınlayacağımızdan henüz emin değiliz (kesinlikle açık kaynaktır - ancak ticari bir üründe kullanıma izin vermek istiyorsak, GPLv2 çok kısıtlayıcıdır). Ayrıca başka bir bağımlılıktır. Ama belki daha sonraki bir zamanda kullanacağız, şimdiye kadar onsuz iyi yaşayabiliriz.
Markus Pilman

1
Bir profil oluşturucu altında, örneğin valgrind altında çalıştırmak anlayışlı olabilir.
Maxim Egorushkin

1
Bir hash tablosundaki yerellik, en azından bir ağaçtaki yerellikten biraz daha iyidir, en azından hash işlevi "rasgele" ise. Bu hash işlevi, yakın zamanlarda yakındaki öğelere nadiren erişmenizi sağlar. Sahip olduğunuz tek avantaj, hashtable dizisinin bir bitişik blok olmasıdır. Yığın parçalanmamışsa ve ağacı bir kerede inşa ederseniz, bu bir ağaç için yine de doğru olabilir. Boyut önbellekten daha büyük olduğunda, yerellikteki farklılıklar, performansta çok az fark yaratır.
Steve314

Yanıtlar:


87

Nedeni buldum: bu bir gcc-4.7 Sorunu !!

İle gcc-4.7

inserts: 37728
get    : 2985

İle gcc-4.6

inserts: 2531
get    : 1565

Yani std::unordered_mapgcc-4.7'de bozuldu (veya Ubuntu üzerinde gcc-4.7.0 yüklemesi olan kurulumum ve debian testinde gcc 4.7.1 olan başka bir kurulum).

Bir hata raporu göndereceğim .. O zamana kadar: std::unordered_mapgcc 4.7 ile KULLANMAYIN !


4.6'dan itibaren deltada buna neden olacak herhangi bir şey var mı?
Mark Canlas

30
Posta listesinde zaten bir rapor var. Tartışma max_load_factor, performansta farklılığa yol açan işleme "düzeltmelerine" işaret ediyor gibi görünüyor .
jxh

Bu hata için kötü zamanlama! Unordered_map ile çok zayıf performans alıyordum ama rapor edilmiş ve "düzeltilmiş" olmasına sevindim.
Bo Lu

+1 - Ne berbat bir BBBBBUG .. gcc-4.8.2 ile ne oluyor merak ediyorum
ikh

2
Bu hatayla ilgili herhangi bir güncelleme var mı? GCC'nin sonraki sürümleri (5+) için hala mevcut mu?
rph

21

unordered_mapYlisar'ın önerdiği gibi, doğru boyutlandırmadığınızı tahmin ediyorum . Zincirler çok uzun süre büyüdüğünde unordered_map, g ++ uygulaması otomatik olarak daha büyük bir karma tabloya dönüşür ve bu performans üzerinde büyük bir engel olur. Doğru hatırlıyorsam, unordered_mapvarsayılan (en küçük asal büyüktür) 100.

chronoSistemimde yoktu , bu yüzden zaman geçirdim times().

template <typename TEST>
void time_test (TEST t, const char *m) {
    struct tms start;
    struct tms finish;
    long ticks_per_second;

    times(&start);
    t();
    times(&finish);
    ticks_per_second = sysconf(_SC_CLK_TCK);
    std::cout << "elapsed: "
              << ((finish.tms_utime - start.tms_utime
                   + finish.tms_stime - start.tms_stime)
                  / (1.0 * ticks_per_second))
              << " " << m << std::endl;
}

Kullanılmış bir SIZEbölgesinin 10000000ve benim sürümü için işleri biraz değiştirmek zorunda boost. Ayrıca, hash tablosunu eşleşecek şekilde önceden boyutlandırdım SIZE/DEPTH, burada DEPTHhash çarpışmalarından dolayı kepçe zincirinin uzunluğunun bir tahmini var.

Düzenleme: Howard için maksimum yük faktörü olduğu yorumlarda bana işaret unordered_mapolduğunu 1. Yani, DEPTHkodun kaç kez yeniden işleyeceğini kontrol eder.

#define SIZE 10000000
#define DEPTH 3
std::vector<uint64_t> vec(SIZE);
boost::mt19937 rng;
boost::uniform_int<uint64_t> dist(std::numeric_limits<uint64_t>::min(),
                                  std::numeric_limits<uint64_t>::max());
std::unordered_map<int, long double> map(SIZE/DEPTH);

void
test_insert () {
    for (int i = 0; i < SIZE; ++i) {
        map[vec[i]] = 0.0;
    }
}

void
test_get () {
    long double val;
    for (int i = 0; i < SIZE; ++i) {
        val = map[vec[i]];
    }
}

int main () {
    for (int i = 0; i < SIZE; ++i) {
        uint64_t val = 0;
        while (val == 0) {
            val = dist(rng);
        }
        vec[i] = val;
    }
    time_test(test_insert, "inserts");
    std::random_shuffle(vec.begin(), vec.end());
    time_test(test_insert, "get");
}

Düzenle:

Daha kolay değiştirebilmek için kodu değiştirdim DEPTH.

#ifndef DEPTH
#define DEPTH 10000000
#endif

Bu nedenle, varsayılan olarak, karma tablo için en kötü boyut seçilir.

elapsed: 7.12 inserts, elapsed: 2.32 get, -DDEPTH=10000000
elapsed: 6.99 inserts, elapsed: 2.58 get, -DDEPTH=1000000
elapsed: 8.94 inserts, elapsed: 2.18 get, -DDEPTH=100000
elapsed: 5.23 inserts, elapsed: 2.41 get, -DDEPTH=10000
elapsed: 5.35 inserts, elapsed: 2.55 get, -DDEPTH=1000
elapsed: 6.29 inserts, elapsed: 2.05 get, -DDEPTH=100
elapsed: 6.76 inserts, elapsed: 2.03 get, -DDEPTH=10
elapsed: 2.86 inserts, elapsed: 2.29 get, -DDEPTH=1

Vardığım sonuç, herhangi bir başlangıç ​​karma tablo boyutu için, onu beklenen benzersiz ekleme sayısının tamamına eşit hale getirmekten başka önemli bir performans farkı olmadığıdır. Ayrıca, gözlemlediğiniz büyüklük sırasını performans farkını göremiyorum.


6
std::unordered_mapvarsayılan maksimum yük faktörü 1'dir. Bu nedenle, başlangıçtaki kepçe sayısı dışında, DEPTH değeriniz dikkate alınmaz. İsterseniz yapabilirsiniz map.max_load_factor(DEPTH).
Howard Hinnant

@HowardHinnant: Bu bilgi için teşekkürler. Yani DEPTHgöz ardı edilir, ancak haritanın ne sıklıkla daha büyük bir haritaya aktarılacağını kontrol eder. Cevap güncellendi ve tekrar teşekkürler
jxh

@ user315052 Evet, başlangıçta makul bir boyut vererek daha iyi hale getirebileceğimi biliyorum - ama bunu yazılımımızda yapamıyorum (bu bir araştırma projesi - bir DBMS - ve orada ne kadar ekleyeceğimi bilmiyorum - 0 ile 1 milyar arasında değişebilir ...). Ancak önceden ayarlamayla bile haritamızdan daha yavaş ve googles yoğun haritasından çok daha yavaş - hala büyük farkı yaratanın ne olduğunu merak ediyorum.
Markus Pilman

@MarkusPilman: Sonuçlarımın sizinkine kıyasla nasıl olduğunu bilmiyorum, çünkü ne kadar büyük bir a SIZEile çalıştığınızı asla belirtmediniz . Ayarlanmış ve önceden tahsis edilmiş unordered_mapolarak iki kat daha hızlı diyebilirim . DEPTH1
jxh

1
@MarkusPilman: Benim sürelerim zaten saniyeler. Zamanlarınızın milisaniye cinsinden olduğunu sanıyordum. 'Ye DEPTHayarlı eklemeler saniyeden 1az 3sürüyorsa, bu nasıl daha yavaş olur?
jxh

3

Kodunuzu 64 bit / AMD / 4 çekirdekli (2.1GHz) bir bilgisayar kullanarak çalıştırdım ve bana şu sonuçları verdi:

MinGW-W64 4.9.2:

Std :: unordered_map kullanarak :

inserts: 9280 
get: 3302

Std :: map kullanarak :

inserts: 23946
get: 24824

Bildiğim tüm optimizasyon bayraklarıyla VC 2015:

Std :: unordered_map kullanarak :

inserts: 7289
get: 1908

Std :: map kullanarak :

inserts: 19222 
get: 19711

Kodu GCC kullanarak test etmedim, ancak bunun VC'nin performansıyla karşılaştırılabilir olabileceğini düşünüyorum, bu yüzden bu doğruysa, GCC 4.9 std :: unordered_map hala bozuk.

[DÜZENLE]

Yani evet, birisinin yorumlarda söylediği gibi, GCC 4.9.x'in performansının VC performansıyla karşılaştırılabilir olacağını düşünmek için hiçbir neden yok. Değişikliği aldığımda kodu GCC'de test edeceğim.

Cevabım sadece diğer cevaplara bir tür bilgi tabanı oluşturmak.


"Kodu GCC kullanarak test etmedim ancak VC'nin performansıyla karşılaştırılabilir olabileceğini düşünüyorum." Orijinal gönderide bulunanla karşılaştırılabilir herhangi bir kıyaslama olmaksızın tamamen temelsiz iddia. Bu "cevap", "neden" sorusunu yanıtlamak şöyle dursun, soruyu hiçbir şekilde yanıtlamaz.
4ae1e1

2
"Kodu GCC kullanarak test etmedim" ... MinGW'yi bu kadar az bilgiye sahipken nasıl elde edip kullanmayı başardınız? MinGW, temelde GCC'nin yakından takip eden bir limanıdır.
alt
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.