Estamos desenvolvendo um software crítico de alto desempenho em C ++. Precisamos de um mapa hash concorrente e um implementado. Então, nós escrevemos um benchmark para descobrir, quanto mais lento nosso mapa hash simultâneo é comparado std::unordered_map
.
Mas, std::unordered_map
parece ser incrivelmente lento ... Portanto, este é o nosso micro-benchmark (para o mapa simultâneo, geramos um novo thread para garantir que o bloqueio não seja otimizado e observe que eu nunca insiro 0 porque também faço o benchmark com google::dense_hash_map
, que precisa de um valor nulo):
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;
(EDITAR: o código-fonte completo pode ser encontrado aqui: http://pastebin.com/vPqf7eya )
O resultado para std::unordered_map
é:
inserts: 35126
get : 2959
Para google::dense_map
:
inserts: 3653
get : 816
Para o nosso mapa simultâneo apoiado manualmente (que bloqueia, embora o benchmark seja de thread único - mas em um thread de spawn separado):
inserts: 5213
get : 2594
Se eu compilar o programa de benchmark sem suporte pthread e executar tudo no thread principal, obtenho os seguintes resultados para nosso mapa simultâneo apoiado manualmente:
inserts: 4441
get : 1180
Eu compilo com o seguinte comando:
g++-4.7 -O3 -DNDEBUG -I/tmp/benchmap/sparsehash-2.0.2/src/ -std=c++11 -pthread main.cc
Então, especialmente as inserções std::unordered_map
parecem ser extremamente caras - 35 segundos contra 3-5 segundos para outros mapas. Além disso, o tempo de consulta parece ser bastante alto.
Minha pergunta: por que isso? Eu li outra pergunta sobre stackoverflow onde alguém pergunta, por que std::tr1::unordered_map
é mais lento do que sua própria implementação. Lá, a resposta com classificação mais alta afirma que é std::tr1::unordered_map
necessário implementar uma interface mais complicada. Mas não consigo ver este argumento: usamos uma abordagem de balde em nosso concurrent_map, também std::unordered_map
usa uma abordagem de balde ( google::dense_hash_map
não, mas std::unordered_map
deveria ser pelo menos tão rápido quanto nossa versão segura para simultaneidade com suporte manual?). Tirando isso, não consigo ver nada na interface que force um recurso que faz o hash map funcionar mal ...
Então minha pergunta: é verdade que std::unordered_map
parece muito lento? Se não: o que há de errado? Se sim: qual a razão disso.
E minha principal pergunta: por que inserir um valor em um std::unordered_map
tão caro é tão caro (mesmo se reservarmos espaço suficiente no início, ele não funciona muito melhor - então refazer parece não ser o problema)?
EDITAR:
Primeiro de tudo: sim, o benchmark apresentado não é perfeito - isso é porque nós brincamos muito com ele e é apenas um hack (por exemplo, a uint64
distribuição para gerar ints na prática não seria uma boa ideia, excluir 0 em um loop é meio estúpido, etc ...).
No momento, a maioria dos comentários explica que posso tornar o unordered_map mais rápido ao pré-alocar espaço suficiente para ele. Em nossa aplicação, isso simplesmente não é possível: estamos desenvolvendo um sistema de gerenciamento de banco de dados e precisamos de um mapa hash para armazenar alguns dados durante uma transação (por exemplo, informações de bloqueio). Portanto, este mapa pode ser tudo de 1 (o usuário apenas faz uma inserção e confirma) a bilhões de entradas (se ocorrerem varreduras completas da tabela). É simplesmente impossível pré-alocar espaço suficiente aqui (e apenas alocar muito no início consumirá muita memória).
Além disso, peço desculpas por não ter formulado minha pergunta com clareza suficiente: não estou realmente interessado em tornar o unordered_map rápido (usar o mapa hash denso do Google funciona bem para nós), simplesmente não entendo de onde vêm essas enormes diferenças de desempenho . Não pode ser apenas pré-alocação (mesmo com memória pré-alocada suficiente, o mapa denso é uma ordem de magnitude mais rápido do que unordered_map, nosso mapa simultâneo apoiado manualmente começa com uma matriz de tamanho 64 - portanto, menor que unordered_map).
Então, qual é a razão para esse mau desempenho de std::unordered_map
? Ou de outra forma: std::unordered_map
seria possível escrever uma implementação da interface em conformidade com o padrão e (quase) tão rápida quanto o denso mapa hash do Google? Ou há algo no padrão que força o implementador a escolher uma forma ineficiente de implementá-lo?
EDIT 2:
Ao fazer o perfil, vejo que muito tempo é usado para divisões inteiras. std::unordered_map
usa números primos para o tamanho do array, enquanto as outras implementações usam potências de dois. Por que std::unordered_map
usa números primos? Para ter um melhor desempenho se o hash estiver ruim? Para bons hashes, isso não faz diferença.
EDITAR 3:
Estes são os números de std::map
:
inserts: 16462
get : 16978
Sooooooo: por que as inserções em um são std::map
mais rápidas do que as inserções em um std::unordered_map
... quero dizer WAT? std::map
tem uma localidade pior (árvore vs array), precisa fazer mais alocações (por inserção vs por rehash + mais ~ 1 para cada colisão) e, o mais importante: tem outra complexidade algorítmica (O (logn) vs O (1))!
SIZE
.