Estoy trabajando en un programa que procesará archivos que podrían tener un tamaño de 100 GB o más. Los archivos contienen conjuntos de registros de longitud variable. Tengo una primera implementación en funcionamiento y ahora estoy buscando mejorar el rendimiento, particularmente para hacer E / S de manera más eficiente ya que el archivo de entrada se escanea muchas veces.
¿Existe una regla general para usar mmap()
versus leer en bloques a través de la fstream
biblioteca de C ++ ? Lo que me gustaría hacer es leer bloques grandes del disco en un búfer, procesar registros completos del búfer y luego leer más.
El mmap()
código podría volverse muy desordenado ya que mmap
los bloques 'd deben ubicarse en los límites del tamaño de la página (según tengo entendido) y los registros podrían potencialmente atravesar los límites de la página. Con fstream
s, solo puedo buscar el comienzo de un registro y comenzar a leer nuevamente, ya que no estamos limitados a bloques de lectura que se encuentran en los límites del tamaño de página.
¿Cómo puedo decidir entre estas dos opciones sin escribir primero una implementación completa? ¿Alguna regla general (por ejemplo, mmap()
es 2 veces más rápido) o pruebas simples?
mmap()
es 2-6 veces más rápido que usar syscalls, por ejemploread()
.Respuestas:
Estaba tratando de encontrar la última palabra sobre el rendimiento de mmap / read en Linux y me encontré con una buena publicación ( enlace ) en la lista de correo del kernel de Linux. Es de 2000, por lo que ha habido muchas mejoras en IO y memoria virtual en el núcleo desde entonces, pero explica muy bien el motivo
mmap
oread
podría ser más rápido o más lento.mmap
tiene más sobrecarga queread
(al igual queepoll
tiene más sobrecarga quepoll
, que tiene más sobrecarga queread
). Cambiar las asignaciones de memoria virtual es una operación bastante costosa en algunos procesadores por las mismas razones que cambiar entre diferentes procesos es costoso.Sin embargo,
read
, su archivo puede haber sido vaciado del caché hace años. Esto no se aplica si usa un archivo y lo descarta de inmediato. (Si intentamlock
páginas solo para mantenerlas en la memoria caché, intenta burlar la memoria caché del disco y este tipo de tonterías rara vez ayuda al rendimiento del sistema).La discusión de mmap / read me recuerda a otras dos discusiones de rendimiento:
Algunos programadores de Java se sorprendieron al descubrir que la E / S sin bloqueo es a menudo más lenta que el bloqueo de E / S, lo que tiene mucho sentido si sabe que la E / S sin bloqueo requiere hacer más syscalls.
Algunos otros programadores de redes se sorprendieron al saber que a
epoll
menudo es más lento quepoll
, lo que tiene mucho sentido si sabes que administrarepoll
requiere hacer más llamadas al sistema.Conclusión: use mapas de memoria si accede a datos aleatoriamente, guárdelos durante mucho tiempo o si sabe que puede compartirlos con otros procesos (
MAP_SHARED
no es muy interesante si no existe un intercambio real). Lea los archivos normalmente si accede a los datos secuencialmente o los descarta después de leerlos. Y si cualquiera de estos métodos hace que su programa sea menos compleja, lo que . Para muchos casos del mundo real, no hay forma segura de mostrar que uno sea más rápido sin probar su aplicación real y NO un punto de referencia.(Perdón por negar esta pregunta, pero estaba buscando una respuesta y esta pregunta seguía apareciendo en la parte superior de los resultados de Google).
fuente
mmap
vsread()
en ese hilo siguen siendo ciertos como lo fueron en el pasado, el rendimiento general no puede determinarse realmente sumando los pros y los contras, sino solo probando una configuración de hardware en particular. Por ejemplo, es discutible que "Una llamada a mmap tiene más sobrecarga que lectura": sí,mmap
tiene que agregar asignaciones a la tabla de páginas de proceso, peroread
tiene que copiar todos los bytes leídos del núcleo al espacio de usuario.mmap
tiene una sobrecarga más baja queread
para lecturas de más de un tamaño de página (4 KiB). Ahora es muy cierto que si desea acceder a los datos de manera dispersa y aleatoria,mmap
es realmente muy bueno, pero lo contrario no es necesariamente cierto: tambiénmmap
puede ser el mejor acceso secuencial.mmap
sea más rápido, esperaría ver, como mínimo, todo el aparato de prueba (código fuente) con los resultados tabulados y el número de modelo del procesador.mmap
no vacía el TLB excepto en circunstancias inusuales (peromunmap
podría). Mis pruebas incluyeron microbenchmarks (incluidosmunmap
) y también "en la aplicación" que se ejecuta en un caso de uso del mundo real. Por supuesto, mi solicitud no es la misma que su solicitud, por lo que las personas deben realizar pruebas locales. Ni siquiera está claro quemmap
sea favorecido por un micro-punto de referencia:read()
también recibe un gran impulso ya que el búfer de destino del lado del usuario generalmente permanece en L1, lo que puede no suceder en una aplicación más grande. Entonces sí, "es complicado".El principal costo de rendimiento será la E / S de disco. "mmap ()" es ciertamente más rápido que istream, pero la diferencia puede no ser notable porque la E / S del disco dominará sus tiempos de ejecución.
Intenté el fragmento de código de Ben Collins (ver arriba / abajo) para probar su afirmación de que "mmap () es mucho más rápido" y no encontré ninguna diferencia medible. Ver mis comentarios sobre su respuesta.
Ciertamente no recomendaría por separado mmap'ing cada registro a su vez a menos que sus "registros" sean enormes, eso sería terriblemente lento, que requeriría 2 llamadas al sistema para cada registro y posiblemente perder la página del caché de la memoria del disco ... .
En su caso, creo que mmap (), istream y las llamadas de bajo nivel open () / read () serán todas iguales. Recomendaría mmap () en estos casos:
(por cierto, me encanta mmap () / MapViewOfFile ()).
fuente
mmap es mucho más rápido. Puede escribir un punto de referencia simple para probarlo usted mismo:
versus:
Claramente, estoy omitiendo detalles (como cómo determinar cuándo llegas al final del archivo en caso de que tu archivo no sea un múltiplo de
page_size
, por ejemplo), pero realmente no debería ser mucho más complicado que esto .Si puede, intente dividir sus datos en varios archivos que pueden ser mmap () editados en su totalidad en lugar de en parte (mucho más simple).
Hace un par de meses tuve una implementación a medias de una clase de flujo de mmap () ed de ventana deslizante para boost_iostreams, pero a nadie le importó y me ocupé de otras cosas. Lamentablemente, eliminé un archivo de proyectos antiguos sin terminar hace unas semanas, y esa fue una de las víctimas :-(
Actualización : también debería agregar la advertencia de que este punto de referencia se vería bastante diferente en Windows porque Microsoft implementó un ingenioso caché de archivos que hace la mayor parte de lo que haría con mmap en primer lugar. Es decir, para los archivos a los que se accede con frecuencia, podría hacer std :: ifstream.read () y sería tan rápido como mmap, porque el caché de archivos ya habría hecho un mapeo de memoria para usted, y es transparente.
Actualización final : Mire, gente: a través de muchas combinaciones de plataformas diferentes de SO y bibliotecas estándar y discos y jerarquías de memoria, no puedo decir con certeza que la llamada al sistema
mmap
, vista como una caja negra, siempre siempre será sustancialmente más rápida queread
. Esa no era exactamente mi intención, incluso si mis palabras pudieran interpretarse de esa manera. Finalmente, mi punto era que la E / S mapeada en memoria es generalmente más rápida que la E / S basada en bytes; Esto sigue siendo cierto . Si encuentra experimentalmente que no hay diferencia entre los dos, entonces la única explicación que me parece razonable es que su plataforma implementa el mapeo de memoria bajo las cubiertas de una manera que es ventajosa para el rendimiento de las llamadas aread
. La única forma de estar absolutamente seguro de que está utilizando E / S mapeadas en memoria de forma portátil es mediante el usommap
. Si no le importa la portabilidad y puede confiar en las características particulares de sus plataformas de destino, el usoread
puede ser adecuado sin sacrificar de manera considerable el rendimiento.Editar para limpiar la lista de respuestas: @jbl:
Claro, estaba escribiendo una biblioteca C ++ para Git (un libgit ++, si lo desea), y me encontré con un problema similar a este: necesitaba poder abrir archivos grandes (muy grandes) y no tener un rendimiento total. (como sería con
std::fstream
).Boost::Iostreams
ya tiene una fuente mapped_file, pero el problema era que estaba haciendommap
ping a archivos completos, lo que lo limita a 2 ^ (tamaño de palabras). En máquinas de 32 bits, 4 GB no es lo suficientemente grande. No es irracional esperar tener.pack
archivos en Git que sean mucho más grandes que eso, por lo que necesitaba leer el archivo en trozos sin recurrir a la E / S de archivo normal. Debajo de las cubiertas deBoost::Iostreams
, implementé una Fuente, que es más o menos otra vista de la interacción entrestd::streambuf
ystd::istream
. También puede probar un enfoque similar simplemente heredandostd::filebuf
enmapped_filebuf
ay, de manera similar, heredandostd::fstream
ena mapped_fstream
. Es la interacción entre los dos lo que es difícil de acertar.Boost::Iostreams
tiene parte del trabajo realizado para usted, y también proporciona ganchos para filtros y cadenas, por lo que pensé que sería más útil implementarlo de esa manera.fuente
mmap()
archivar una página a la vez? Si asize_t
tiene la capacidad suficiente para contener el tamaño del archivo (muy probablemente en sistemas de 64 bits), entonces solommap()
el archivo completo en una llamada.Aquí hay muchas buenas respuestas que cubren muchos de los puntos más destacados, así que solo agregaré un par de problemas que no vi abordados directamente arriba. Es decir, esta respuesta no debe considerarse como una integral de los pros y los contras, sino más bien como una adición a otras respuestas aquí.
mmap parece magia
Tomar el caso en el que el archivo ya está completamente en caché 1 como la línea de base 2 ,
mmap
podría parecerse a la magia :mmap
solo requiere 1 llamada al sistema para (potencialmente) mapear todo el archivo, después de lo cual no se necesitan más llamadas al sistema.mmap
no requiere una copia de los datos del archivo del kernel al espacio de usuario.mmap
le permite acceder al archivo "como memoria", incluido el procesamiento con cualquier truco avanzado que pueda hacer contra la memoria, como la vectorización automática del compilador, la intrínseca SIMD , la captación previa, las rutinas optimizadas de análisis en memoria, OpenMP, etc.En el caso de que el archivo ya esté en el caché, parece imposible de superar: simplemente accede directamente al caché de la página del núcleo como memoria y no puede ser más rápido que eso.
Bueno, si puede.
mmap no es realmente mágico porque ...
mmap todavía funciona por página
Un costo oculto primario de
mmap
vsread(2)
(que es realmente el syscall comparable a nivel de sistema operativo para leer bloques ) es quemmap
tendrá que hacer "algo de trabajo" para cada página 4K en el espacio de usuario, aunque pueda estar oculto por el mecanismo de falla de página.Por ejemplo, una implementación típica que solo
mmap
es el archivo completo necesitará una falla de manera que 100 GB / 4K = 25 millones de fallas para leer un archivo de 100 GB. Ahora, estos serán fallas menores , pero las fallas de 25 mil millones de páginas todavía no serán súper rápidas. El costo de una falla menor probablemente esté en los cientos de nanos en el mejor de los casos.mmap depende en gran medida del rendimiento de TLB
Ahora, puede pasar
MAP_POPULATE
ammap
decirle que configure todas las tablas de páginas antes de regresar, por lo que no debe haber fallas de página al acceder. Ahora, esto tiene el pequeño problema de que también lee todo el archivo en la RAM, que explotará si intenta asignar un archivo de 100GB, pero ignoremos eso por ahora 3 . El kernel necesita hacer un trabajo por página para configurar estas tablas de páginas (aparece como tiempo de kernel). Esto termina siendo un costo importante en elmmap
enfoque, y es proporcional al tamaño del archivo (es decir, no se vuelve relativamente menos importante a medida que crece el tamaño del archivo) 4 .Finalmente, incluso en el acceso al espacio de usuario, dicha asignación no es exactamente gratuita (en comparación con grandes memorias intermedias que no se originan a partir de un archivo
mmap
), incluso una vez que se configuran las tablas de páginas, cada acceso a una nueva página va a, conceptualmente, incurrir en una falta de TLB. Ya quemmap
crear un archivo significa usar el caché de la página y sus páginas 4K, nuevamente incurrirá en este costo 25 millones de veces por un archivo de 100GB.Ahora, el costo real de estas fallas de TLB depende en gran medida de al menos los siguientes aspectos de su hardware: (a) cuántas entradas de TLB de 4K tiene y cómo funciona el resto del almacenamiento en caché de traducción (b) qué tan bien se ocupa la captación previa de hardware con el TLB, por ejemplo, ¿puede la captación previa desencadenar una caminata de página? (c) qué tan rápido y qué tan paralelo es el hardware que recorre la página. En los modernos procesadores Intel x86 de gama alta, el hardware de paso de página es en general muy fuerte: hay al menos 2 caminadores de página paralelos, un paso de página puede ocurrir simultáneamente con la ejecución continua, y la captación previa de hardware puede desencadenar un paso de página. Entonces, el impacto de TLB en una transmisión carga de lectura de es bastante bajo, y dicha carga a menudo tendrá un rendimiento similar independientemente del tamaño de la página. Sin embargo, otro hardware suele ser mucho peor.
read () evita estas trampas
La
read()
llamada al sistema, que es lo que generalmente subyace a las llamadas de tipo "lectura en bloque" que se ofrecen, por ejemplo, en C, C ++ y otros lenguajes, tiene una desventaja principal que todos conocen:read()
llamada de N bytes debe copiar N bytes del núcleo al espacio del usuario.Por otro lado, evita la mayoría de los costos anteriores: no es necesario asignar 25 millones de páginas 4K en el espacio del usuario. Por lo general, puede
malloc
usar un solo búfer pequeño en el espacio de usuario y reutilizarlo repetidamente para todas susread
llamadas. En el lado del kernel, casi no hay problema con las páginas 4K o las fallas de TLB porque toda la RAM generalmente se mapea linealmente usando algunas páginas muy grandes (por ejemplo, páginas de 1 GB en x86), por lo que las páginas subyacentes en el caché de páginas están cubiertas de manera muy eficiente en el espacio del kernel.Básicamente, tiene la siguiente comparación para determinar cuál es más rápido para una sola lectura de un archivo grande:
¿Es el trabajo adicional por página implicado por el
mmap
enfoque más costoso que el trabajo por byte de copiar el contenido del archivo desde el núcleo al espacio de usuario implícito mediante el usoread()
?En muchos sistemas, en realidad están aproximadamente equilibrados. Tenga en cuenta que cada uno escala con atributos completamente diferentes del hardware y la pila del sistema operativo.
En particular, el
mmap
enfoque se vuelve relativamente más rápido cuando:MAP_POPULATE
implementación que puede procesar eficientemente mapas grandes en casos donde, por ejemplo, las páginas subyacentes son contiguas en la memoria física.... mientras que el
read()
enfoque se vuelve relativamente más rápido cuando:read()
llamada al sistema tiene un buen rendimiento de copia. Por ejemplo, buencopy_to_user
rendimiento en el lado del núcleo.Los factores de hardware anteriores varían enormemente entre diferentes plataformas, incluso dentro de la misma familia (por ejemplo, dentro de x86 generaciones y especialmente segmentos de mercado) y definitivamente entre arquitecturas (por ejemplo, ARM vs x86 vs PPC).
Los factores del sistema operativo siguen cambiando también, con varias mejoras en ambos lados que causan un gran salto en la velocidad relativa para un enfoque u otro. Una lista reciente incluye:
mmap
caso sinMAP_POPULATE
.copy_to_user
métodos de vía rápida enarch/x86/lib/copy_user_64.S
, por ejemplo, el usoREP MOVQ
cuando es rápido, que realmente ayudan alread()
caso.Actualización después de Specter and Meltdown
Las mitigaciones para las vulnerabilidades Spectre y Meltdown aumentaron considerablemente el costo de una llamada al sistema. En los sistemas que he medido, el costo de una llamada al sistema "no hacer nada" (que es una estimación de la sobrecarga pura de la llamada del sistema, aparte de cualquier trabajo real realizado por la llamada) pasó de aproximadamente 100 ns en un típico Sistema Linux moderno a unos 700 ns. Además, dependiendo de su sistema, la corrección de aislamiento de la tabla de páginas específicamente para Meltdown puede tener efectos posteriores adicionales además del costo directo de la llamada del sistema debido a la necesidad de volver a cargar las entradas TLB.
Todo esto es una desventaja relativa para los
read()
métodos basados en comparación con losmmap
métodos basados, ya que losread()
métodos deben hacer una llamada al sistema para cada valor de "tamaño de búfer". No puede aumentar arbitrariamente el tamaño del búfer para amortizar este costo, ya que el uso de grandes búferes generalmente funciona peor ya que excede el tamaño L1 y, por lo tanto, sufre constantemente errores de caché.Por otro lado, con
mmap
, puede asignar en una gran región de memoriaMAP_POPULATE
y acceder de manera eficiente, a costa de una sola llamada al sistema.1 Esto más o menos también incluye el caso en el que el archivo no estaba completamente en caché para empezar, pero donde la lectura del sistema operativo es lo suficientemente buena como para que parezca así (es decir, la página generalmente está en caché para cuando lo quiero). Este es un tema sutil, porque aunque el camino prelectura obras es a menudo bastante diferente entre
mmap
yread
llamadas, y se puede ajustar aún más por las llamadas "asesorar" como se describe en 2 .2 ... porque si el archivo no está en caché, su comportamiento estará completamente dominado por preocupaciones de E / S, incluyendo cuán comprensivo es su patrón de acceso al hardware subyacente, y todo su esfuerzo debe ser para garantizar que dicho acceso sea tan comprensivo como posible, por ejemplo, mediante el uso de
madvise
ofadvise
llamadas (y cualquier cambio de nivel de aplicación que pueda hacer para mejorar los patrones de acceso).3 Podría evitar eso, por ejemplo, introduciendo secuencialmente
mmap
en ventanas de un tamaño más pequeño, digamos 100 MB.4 De hecho, resulta que el
MAP_POPULATE
enfoque es (al menos una combinación de hardware / sistema operativo) solo un poco más rápido que no usarlo, probablemente porque el kernel está usando faultround , por lo que el número real de fallas menores se reduce en un factor de 16 más o menos.fuente
mmap
tendrá una ventaja insuperable ya que evita la sobrecarga fija de la llamada del núcleo. Por otro lado,mmap
también aumenta la presión TLB, y en realidad hace que sea más lento para la fase de "calentamiento" donde los bytes se leen por primera vez en el proceso actual (aunque todavía están en la página), ya que puede hacerlo más trabajo queread
, por ejemplo, para "solucionar" las páginas adyacentes ... ¡y para las mismas aplicaciones "calentar" es todo lo que importa! @CaetanoSauerLo siento, Ben Collins perdió su código fuente de ventanas deslizantes mmap. Sería bueno tenerlo en Boost.
Sí, mapear el archivo es mucho más rápido. Básicamente, está utilizando el subsistema de memoria virtual del sistema operativo para asociar la memoria al disco y viceversa. Piénselo de esta manera: si los desarrolladores del kernel del sistema operativo pudieran hacerlo más rápido, lo harían. Porque hacerlo hace que todo sea más rápido: bases de datos, tiempos de arranque, tiempos de carga de programas, etc.
El enfoque de ventana deslizante realmente no es tan difícil, ya que se pueden asignar varias páginas distinguidas a la vez. Por lo tanto, el tamaño del registro no importa siempre y cuando el más grande de todos los registros individuales quepa en la memoria. Lo importante es gestionar la contabilidad.
Si un registro no comienza en un límite getpagesize (), su mapeo debe comenzar en la página anterior. La longitud de la región asignada se extiende desde el primer byte del registro (redondeado si es necesario al múltiplo más cercano de getpagesize ()) hasta el último byte del registro (redondeado al múltiplo más cercano de getpagesize ()). Cuando termine de procesar un registro, puede desasignarlo () y pasar al siguiente.
Todo esto funciona bien en Windows también usando CreateFileMapping () y MapViewOfFile () (y GetSystemInfo () para obtener SYSTEM_INFO.dwAllocationGranularity --- no SYSTEM_INFO.dwPageSize).
fuente
mmap debería ser más rápido, pero no sé cuánto. Depende mucho de tu código. Si usa mmap, es mejor mapear todo el archivo a la vez, eso le hará la vida mucho más fácil. Un problema potencial es que si su archivo es más grande que 4GB (o en la práctica el límite es más bajo, a menudo 2GB) necesitará una arquitectura de 64 bits. Entonces, si está usando un entorno 32, probablemente no quiera usarlo.
Dicho esto, puede haber una mejor ruta para mejorar el rendimiento. Dijiste que el archivo de entrada se escanea muchas veces , si puedes leerlo de una vez y luego hacerlo, eso podría ser mucho más rápido.
fuente
Tal vez debería preprocesar los archivos, de modo que cada registro esté en un archivo separado (o al menos que cada archivo tenga un tamaño compatible con mmap).
¿También podría hacer todos los pasos de procesamiento para cada registro, antes de pasar al siguiente? ¿Tal vez eso evitaría algunos de los gastos generales de IO?
fuente
Estoy de acuerdo que mmap'd archivo de E / S va a ser más rápido, pero al mismo tiempo su evaluación comparativa del código, ¿no debería el contraejemplo ser un poco optimizado?
Ben Collins escribió:
Sugeriría también intentar:
Y más allá de eso, también puede intentar hacer que el tamaño del búfer sea del mismo tamaño que una página de memoria virtual, en caso de que 0x1000 no sea el tamaño de una página de memoria virtual en su máquina ... gana, pero esto debería acercar las cosas.
fuente
En mi opinión, usar mmap () "solo" libera al desarrollador de tener que escribir su propio código de almacenamiento en caché. En un caso simple de "leer el archivo de manera efectiva una vez", esto no va a ser difícil (aunque, como mlbrock señala, todavía guarda la copia de la memoria en el espacio de proceso), pero si va de un lado a otro en el archivo o omitiendo bits y demás, creo que los desarrolladores del kernel probablemente han hecho un mejor trabajo implementando el almacenamiento en caché que yo ...
fuente
mmap
almacenamiento en caché es que simplemente reutiliza el caché de página existente que ya estará allí, de modo que obtiene esa memoria de forma gratuita y también se puede compartir entre los procesos.Recuerdo mapear un archivo enorme que contiene una estructura de árbol en la memoria hace años. Me sorprendió la velocidad en comparación con la deserialización normal que implica mucho trabajo en la memoria, como la asignación de nodos de árbol y la configuración de punteros. De hecho, estaba comparando una sola llamada a mmap (o su contraparte en Windows) con muchas (MUCHAS) llamadas a llamadas de operador nuevas y de constructor. Para este tipo de tarea, mmap es inmejorable en comparación con la deserialización. Por supuesto, uno debe buscar en los punteros reubicables para esto.
fuente
Esto suena como un buen caso de uso para subprocesos múltiples ... Creo que podría configurar fácilmente un subproceso para leer datos mientras los otros lo procesan. Esa puede ser una forma de aumentar dramáticamente el rendimiento percibido. Solo un pensamiento.
fuente
Creo que lo mejor de mmap es el potencial para la lectura asincrónica con:
El problema es que no puedo encontrar el MAP_FLAGS correcto para dar una pista de que esta memoria debe sincronizarse desde el archivo lo antes posible. Espero que MAP_POPULATE dé la pista correcta para mmap (es decir, no intentará cargar todo el contenido antes de regresar de la llamada, pero lo hará de forma asincrónica con feed_data). Al menos, ofrece mejores resultados con este indicador, incluso en el manual, indica que no hace nada sin MAP_PRIVATE desde 2.6.23.
fuente
posix_madvise
con laWILLNEED
bandera prepoblaciones insinuantes.posix_madvise
es una llamada asincrónica. También sería bueno hacer referenciamlock
para aquellos que desean esperar hasta que toda la región de memoria esté disponible sin fallas de página.