Estamos desarrollando un software crítico de alto rendimiento en C ++. Allí necesitamos un mapa hash concurrente y uno implementado. Así que escribimos un punto de referencia para averiguar con qué velocidad se compara nuestro mapa hash concurrente std::unordered_map
.
Pero, std::unordered_map
parece ser increíblemente lento ... Así que este es nuestro micro-benchmark (para el mapa concurrente generamos un nuevo hilo para asegurarnos de que el bloqueo no se optimice y tenga en cuenta que nunca inser 0 porque también comparo con google::dense_hash_map
, que necesita un 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: el código fuente completo se puede encontrar aquí: http://pastebin.com/vPqf7eya )
El resultado de std::unordered_map
es:
inserts: 35126
get : 2959
Para google::dense_map
:
inserts: 3653
get : 816
Para nuestro mapa concurrente respaldado a mano (que se bloquea, aunque el punto de referencia es de un solo hilo, pero en un hilo de generación separado):
inserts: 5213
get : 2594
Si compilo el programa de referencia sin el soporte de pthread y ejecuto todo en el hilo principal, obtengo los siguientes resultados para nuestro mapa concurrente respaldado a mano:
inserts: 4441
get : 1180
Yo compilo con el siguiente comando:
g++-4.7 -O3 -DNDEBUG -I/tmp/benchmap/sparsehash-2.0.2/src/ -std=c++11 -pthread main.cc
Así que, especialmente, las inserciones std::unordered_map
parecen ser extremadamente caras: 35 segundos frente a 3-5 segundos para otros mapas. Además, el tiempo de búsqueda parece ser bastante elevado.
Mi pregunta: ¿por qué es esto? Leí otra pregunta en stackoverflow donde alguien pregunta por qué std::tr1::unordered_map
es más lento que su propia implementación. Allí, la respuesta mejor calificada dice que es std::tr1::unordered_map
necesario implementar una interfaz más complicada. Pero no puedo ver este argumento: usamos un enfoque de cubo en nuestro concurrent_map, también std::unordered_map
usa un enfoque de cubo ( google::dense_hash_map
no lo hace, pero std::unordered_map
debería ser al menos tan rápido que nuestra versión segura de concurrencia respaldada a mano). Aparte de eso, no puedo ver nada en la interfaz que fuerce una característica que hace que el mapa hash funcione mal ...
Entonces mi pregunta: ¿es cierto que std::unordered_map
parece ser muy lento? Si no, ¿qué pasa? Si es así: ¿cuál es la razón?
Y mi pregunta principal: ¿por qué es std::unordered_map
tan caro insertar un valor en un valor tan terrible (incluso si reservamos suficiente espacio al principio, no funciona mucho mejor, por lo que el refrito no parece ser el problema)?
EDITAR:
En primer lugar: sí, el punto de referencia presentado no es impecable, esto se debe a que jugamos mucho con él y es solo un truco (por ejemplo, la uint64
distribución para generar ints en la práctica no sería una buena idea, excluir 0 en un bucle es algo estúpido, etc.).
Por el momento, la mayoría de los comentarios explican que puedo hacer que unordered_map sea más rápido asignando previamente suficiente espacio para él. En nuestra aplicación esto simplemente no es posible: estamos desarrollando un sistema de administración de bases de datos y necesitamos un mapa hash para almacenar algunos datos durante una transacción (por ejemplo, información de bloqueo). Por lo tanto, este mapa puede ser de todo, desde 1 (el usuario solo hace una inserción y confirma) hasta miles de millones de entradas (si se realizan escaneos completos de la tabla). Es simplemente imposible preasignar suficiente espacio aquí (y simplemente asignar mucho al principio consumirá demasiada memoria).
Además, me disculpo por no haber planteado mi pregunta lo suficientemente clara: no estoy realmente interesado en hacer un mapa ordenado rápido (usar mapas hash densos de Google funciona bien para nosotros), simplemente no entiendo de dónde vienen estas enormes diferencias de rendimiento . No puede ser solo una preasignación (incluso con suficiente memoria preasignada, el mapa denso es un orden de magnitud más rápido que unordered_map, nuestro mapa concurrente con respaldo manual comienza con una matriz de tamaño 64, por lo que una más pequeña que unordered_map).
Entonces, ¿cuál es la razón de este mal desempeño std::unordered_map
? O preguntado de otra manera: ¿Se podría escribir una implementación de la std::unordered_map
interfaz que sea estándar y (casi) tan rápida como el mapa de hash denso de Google? ¿O hay algo en el estándar que obliga al implementador a elegir una forma ineficiente de implementarlo?
EDITAR 2:
Al crear perfiles, veo que se usa mucho tiempo para divisiones enteras. std::unordered_map
usa números primos para el tamaño de la matriz, mientras que las otras implementaciones usan potencias de dos. ¿Por qué std::unordered_map
utiliza números primos? ¿Funcionar mejor si el hash es malo? Para buenos hashes, en mi humilde opinión, no hace ninguna diferencia.
EDITAR 3:
Estos son los números para std::map
:
inserts: 16462
get : 16978
Sooooooo: ¿por qué se insertan en un std::map
más rápido que se inserta en un std::unordered_map
... quiero decir WAT? std::map
tiene una localidad peor (árbol vs matriz), necesita hacer más asignaciones (por inserción vs por repetición + más ~ 1 por cada colisión) y, lo más importante: tiene otra complejidad algorítmica (O (logn) vs O (1))!
SIZE
.