Stiamo sviluppando un software critico ad alte prestazioni in C ++. Lì abbiamo bisogno di una mappa hash simultanea e ne implementiamo una. Quindi abbiamo scritto un benchmark per capire quanto più lenta viene confrontata la nostra mappa hash simultanea std::unordered_map
.
Ma std::unordered_map
sembra essere incredibilmente lento ... Quindi questo è il nostro micro-benchmark (per la mappa simultanea abbiamo generato un nuovo thread per assicurarci che il blocco non venga ottimizzato e nota che non inserisco mai 0 perché faccio anche benchmark con google::dense_hash_map
, che necessita di un valore nullo):
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;
(EDIT: l'intero codice sorgente può essere trovato qui: http://pastebin.com/vPqf7eya )
Il risultato per std::unordered_map
è:
inserts: 35126
get : 2959
Per google::dense_map
:
inserts: 3653
get : 816
Per la nostra mappa simultanea supportata a mano (che esegue il blocco, sebbene il benchmark sia a thread singolo, ma in un thread di spawn separato):
inserts: 5213
get : 2594
Se compilo il programma di benchmark senza il supporto di pthread ed eseguo tutto nel thread principale, ottengo i seguenti risultati per la nostra mappa simultanea supportata a mano:
inserts: 4441
get : 1180
Compilo con il seguente comando:
g++-4.7 -O3 -DNDEBUG -I/tmp/benchmap/sparsehash-2.0.2/src/ -std=c++11 -pthread main.cc
Quindi, in particolare, gli inserti std::unordered_map
sembrano essere estremamente costosi: 35 secondi contro 3-5 secondi per altre mappe. Anche il tempo di ricerca sembra essere piuttosto alto.
La mia domanda: perché è questo? Ho letto un'altra domanda su stackoverflow in cui qualcuno chiede, perché std::tr1::unordered_map
è più lento della sua implementazione. La risposta con il punteggio più alto afferma che è std::tr1::unordered_map
necessario implementare un'interfaccia più complicata. Ma non riesco a vedere questo argomento: usiamo un approccio bucket nella nostra concurrent_map, std::unordered_map
utilizza anche un approccio bucket ( google::dense_hash_map
non lo fa, ma std::unordered_map
dovrebbe essere almeno altrettanto veloce della nostra versione sicura della concorrenza supportata a mano?). A parte questo non riesco a vedere nulla nell'interfaccia che forzi una funzione che fa funzionare male la mappa hash ...
Quindi la mia domanda: è vero che std::unordered_map
sembra essere molto lento? Se no: cosa c'è che non va? Se sì: qual è il motivo.
E la mia domanda principale: perché inserire un valore in un std::unordered_map
così terribile costoso (anche se riserviamo abbastanza spazio all'inizio, non funziona molto meglio - quindi il rehashing sembra non essere il problema)?
MODIFICARE:
Prima di tutto: sì, il benchmark presentato non è impeccabile - questo perché ci abbiamo giocato molto ed è solo un trucco (ad esempio la uint64
distribuzione per generare int non sarebbe in pratica una buona idea, escludere 0 in un ciclo è un po 'stupido ecc ...).
Al momento la maggior parte dei commenti spiega che posso rendere unordered_map più veloce preallocando abbastanza spazio per esso. Nella nostra applicazione questo non è possibile: stiamo sviluppando un sistema di gestione del database e abbiamo bisogno di una mappa hash per memorizzare alcuni dati durante una transazione (ad esempio informazioni di blocco). Quindi questa mappa può essere qualsiasi cosa da 1 (l'utente fa solo un inserimento e si impegna) a miliardi di voci (se si verificano scansioni complete della tabella). È semplicemente impossibile preallocare spazio sufficiente qui (e allocare solo molto all'inizio consumerà troppa memoria).
Inoltre, mi scuso per non aver espresso la mia domanda abbastanza chiara: non sono davvero interessato a rendere veloce unordered_map (usare la mappa hash densa di Google per noi funziona bene), semplicemente non capisco da dove provengono queste enormi differenze di prestazioni . Non può essere solo una preallocazione (anche con abbastanza memoria preallocata, la mappa densa è un ordine di grandezza più veloce di unordered_map, la nostra mappa simultanea sostenuta a mano inizia con un array di dimensione 64, quindi uno più piccolo di unordered_map).
Allora qual è la ragione di questa cattiva prestazione di std::unordered_map
? O diversamente chiesto: si potrebbe scrivere un'implementazione std::unordered_map
dell'interfaccia conforme allo standard e (quasi) veloce come la mappa hash densa di Google? O c'è qualcosa nello standard che impone all'implementatore di scegliere un modo inefficiente per implementarlo?
MODIFICA 2:
Dal profilo vedo che molto tempo viene utilizzato per le divioni intere. std::unordered_map
usa numeri primi per la dimensione dell'array, mentre le altre implementazioni usano potenze di due. Perché std::unordered_map
usa i numeri primi? Per funzionare meglio se l'hashish è cattivo? Per un buon hash non fa differenza.
MODIFICA 3:
Questi sono i numeri per std::map
:
inserts: 16462
get : 16978
Sooooooo: perché gli inserti in un sono std::map
più veloci degli inserti in un std::unordered_map
... intendo WAT? std::map
ha una località peggiore (albero vs array), ha bisogno di fare più allocazioni (per insert vs per rehash + plus ~ 1 per ogni collisione) e, cosa più importante: ha un'altra complessità algoritmica (O (logn) vs O (1))!
SIZE
.