Es de conocimiento común en programación que la ubicación de la memoria mejora mucho el rendimiento debido a los aciertos de caché. Recientemente descubrí boost::flat_map
cuál es una implementación basada en vectores de un mapa. No parece ser tan popular como el típico, map
por unordered_map
lo que no he podido encontrar ninguna comparación de rendimiento. ¿Cómo se compara y cuáles son los mejores casos de uso para él?
¡Gracias!
Respuestas:
Recientemente he ejecutado un punto de referencia en diferentes estructuras de datos en mi empresa, así que siento que necesito decir algo. Es muy complicado comparar algo correctamente.
Benchmarking
En la web rara vez encontramos (si acaso alguna vez) un punto de referencia bien diseñado. Hasta el día de hoy, solo encontré puntos de referencia que se hicieron a la manera de los periodistas (bastante rápido y barriendo docenas de variables debajo de la alfombra).
1) Debe tener en cuenta el calentamiento de la caché
La mayoría de las personas que ejecutan puntos de referencia temen la discrepancia del temporizador, por lo tanto, ejecutan sus cosas miles de veces y se toman todo el tiempo, solo tienen cuidado de tomar las mismas miles de veces para cada operación y luego consideran que es comparable.
La verdad es que, en el mundo real, tiene poco sentido, porque su caché no estará caliente y su operación probablemente se llamará solo una vez. Por lo tanto, necesita comparar usando RDTSC y cronometrar las cosas llamándolos una sola vez. Intel ha elaborado un documento que describe cómo usar RDTSC (usando una instrucción cpuid para limpiar la tubería y llamándola al menos 3 veces al comienzo del programa para estabilizarla).
2) medida de precisión RDTSC
También recomiendo hacer esto:
Este es un medidor de discrepancias, y tomará el mínimo de todos los valores medidos, para evitar obtener un -10 ** 18 (primeros valores negativos de 64 bits) de vez en cuando.
Observe el uso de intrínsecos y no ensamblaje en línea. El primer ensamblaje en línea rara vez es compatible con los compiladores de hoy en día, pero lo peor de todo es que el compilador crea una barrera de ordenación completa alrededor del ensamblaje en línea porque no puede analizar estáticamente el interior, por lo que este es un problema para comparar cosas del mundo real, especialmente cuando se llaman cosas simplemente una vez. Entonces, un intrínseco es adecuado aquí, porque no interrumpe el reordenamiento libre de instrucciones del compilador.
3) parámetros
El último problema es que la gente suele probar muy pocas variaciones del escenario. El rendimiento de un contenedor se ve afectado por:
El punto 1 es importante porque los contenedores se asignan de vez en cuando, y es muy importante si asignan utilizando el CRT "nuevo" o alguna operación definida por el usuario, como la asignación de grupos o la lista gratuita u otra ...
( para las personas interesadas en el punto 1, únase al hilo misterioso en gamedev sobre el impacto en el rendimiento del asignador del sistema )
El punto 2 se debe a que algunos contenedores (por ejemplo, A) perderán tiempo copiando cosas, y cuanto más grande sea el tipo, mayor será la sobrecarga. El problema es que cuando se compara con otro contenedor B, A puede ganarle a B para tipos pequeños y perder para tipos más grandes.
El punto 3 es el mismo que el punto 2, excepto que multiplica el costo por algún factor de ponderación.
El punto 4 es una cuestión de gran O mezclada con problemas de caché. Algunos contenedores de mala complejidad pueden superar en gran medida a los contenedores de baja complejidad para una pequeña cantidad de tipos (como
map
vs.vector
, porque su localidad de caché es buena, peromap
fragmenta la memoria). Y luego, en algún punto de cruce, perderán, porque el tamaño total contenido comienza a "filtrarse" a la memoria principal y provocar errores de caché, además del hecho de que la complejidad asintótica puede comenzar a sentirse.El punto 5 trata de que los compiladores puedan eludir cosas vacías o triviales en el momento de la compilación. Esto puede optimizar en gran medida algunas operaciones, ya que los contenedores tienen plantilla, por lo que cada tipo tendrá su propio perfil de rendimiento.
El punto 6 al igual que el punto 5, los POD pueden beneficiarse del hecho de que la construcción de copias es solo una memoria, y algunos contenedores pueden tener una implementación específica para estos casos, utilizando especializaciones de plantilla parciales, o SFINAE para seleccionar algoritmos de acuerdo con los rasgos de T.
Sobre el mapa plano
Aparentemente, el mapa plano es un contenedor vectorial ordenado, como Loki AssocVector, pero con algunas modernizaciones complementarias que vienen con C ++ 11, explotando la semántica de movimiento para acelerar la inserción y eliminación de elementos individuales.
Este sigue siendo un contenedor ordenado. La mayoría de las personas generalmente no necesitan la parte de pedido, por lo tanto, la existencia de
unordered..
.¿Has considerado que quizás necesites un
flat_unorderedmap
? que sería algo asígoogle::sparse_map
o algo así: un mapa hash de direcciones abierto.El problema de los mapas hash de direcciones abiertos es que en el momento de
rehash
tener que copiar todo en la nueva zona plana extendida, mientras que un mapa desordenado estándar solo tiene que volver a crear el índice hash, mientras que los datos asignados permanecen donde están. La desventaja, por supuesto, es que la memoria está muy fragmentada.El criterio de un refrito en un mapa hash de direcciones abiertas es cuando la capacidad excede el tamaño del vector de cubeta multiplicado por el factor de carga.
Un factor de carga típico es
0.8
; por lo tanto, debe preocuparse por eso, si puede pre-dimensionar su mapa hash antes de llenarlo, siempre pre-dimensione a:intended_filling * (1/0.8) + epsilon
esto le dará la garantía de no tener que volver a copiar y volver a copiar todo de manera falsa durante el llenado.La ventaja de los mapas de direcciones cerrados (
std::unordered..
) es que no tiene que preocuparse por esos parámetros.Pero
boost::flat_map
es un vector ordenado; por lo tanto, siempre tendrá una complejidad log (N) asintótica, que es menos buena que el mapa hash de direcciones abiertas (tiempo constante amortizado). Deberías considerar eso también.Resultados comparativos
Esta es una prueba que involucra diferentes mapas (con
int
clave y__int64
/somestruct
como valor) ystd::vector
.información de tipos probados:
Inserción
EDITAR:
Mis resultados anteriores incluyeron un error: en realidad probaron la inserción ordenada, que mostró un comportamiento muy rápido para los mapas planos.
Dejé esos resultados más adelante en esta página porque son interesantes.
Esta es la prueba correcta:
He comprobado la implementación, no existe tal cosa como una clasificación diferida implementada en los mapas planos aquí. Cada inserción se ordena sobre la marcha, por lo tanto, este punto de referencia exhibe las tendencias asintóticas:
mapa: O (N * log (N))
hashmaps: O (N)
vector y mapas planos: O (N * N)
Advertencia : de ahora en adelante, las 2 pruebas para
std::map
y ambosflat_map
s tienen errores y en realidad prueban la inserción ordenada (frente a la inserción aleatoria para otros contenedores. Sí, es confuso, lo siento):Podemos ver que la inserción ordenada resulta en retroceso y es extremadamente rápida. Sin embargo, a partir de los resultados no graficados de mi punto de referencia, también puedo decir que esto no está cerca de la optimización absoluta para una inserción posterior. En 10k elementos, se obtiene una optimización de inserción posterior perfecta en un vector reservado previamente. Lo que nos da 3 millones de ciclos; observamos 4.8M aquí para la inserción ordenada en el
flat_map
(por lo tanto, 160% del óptimo).Análisis: recuerde que esto es una 'inserción aleatoria' para el vector, por lo que los mil millones de ciclos masivos provienen de tener que cambiar la mitad (en promedio) de los datos hacia arriba (un elemento por un elemento) en cada inserción.
Búsqueda aleatoria de 3 elementos (relojes renormalizados a 1)
en tamaño = 100
en tamaño = 10000
Iteración
sobre el tamaño 100 (solo tipo MediumPod)
sobre el tamaño 10000 (solo tipo MediumPod)
Grano de sal final
Al final, quería volver a "Benchmarking §3 Pt1" (el asignador del sistema). En un experimento reciente que estoy haciendo sobre el rendimiento de un mapa hash de direcciones abiertas que desarrollé , medí una brecha de rendimiento de más del 3000% entre Windows 7 y Windows 8 en algunos
std::unordered_map
casos de uso ( discutidos aquí ).Lo que me hace querer advertir al lector sobre los resultados anteriores (se hicieron en Win7): su kilometraje puede variar.
atentamente
fuente
flat_map
comparación constd::map
: ¿alguien puede explicar este resultado?flat_map
como contenedor. Porque laAska::
versión es más rápida que lastd::map
búsqueda. Demostrando que hay margen para la optimización. El rendimiento esperado es asintóticamente el mismo, pero quizás un poco mejor gracias a la localidad de caché. Con conjuntos de gran tamaño, deberían converger.Según los documentos, parece que esto es análogo a
Loki::AssocVector
lo que soy un usuario bastante pesado. Al estar basado en un vector tiene las características de un vector, es decir:size
crece más allácapacity
.capacity
, necesita reasignar y mover objetos, es decir, no se garantiza un tiempo constante de inserción, excepto en el caso especial de insertar enend
cuandocapacity > size
std::map
debido a la localidad de la caché, una búsqueda binaria que tiene las mismas características de rendimiento que destd::map
otra maneraEl mejor uso es cuando conoce la cantidad de elementos de antemano (para que pueda hacerlo por
reserve
adelantado), o cuando la inserción / eliminación es rara pero la búsqueda es frecuente. La invalidación del iterador lo hace un poco engorroso en algunos casos de uso, por lo que no son intercambiables en términos de corrección del programa.fuente