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
vs read(2)
(que es realmente el syscall comparable a nivel de sistema operativo para leer bloques ) es que mmap
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
a mmap
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 el mmap
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:
- Cada
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 sus read
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 uso read()
?
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:
- El sistema operativo tiene un manejo rápido de fallas menores y, especialmente, optimizaciones de aumento de fallas menores, como la resolución de fallas.
- El sistema operativo tiene un buen
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.
- El hardware tiene un sólido rendimiento de traducción de páginas, como TLB grandes, TLB rápidos de segundo nivel, caminadores de páginas rápidos y paralelos, buena interacción de captación previa con la traducción, etc.
... mientras que el read()
enfoque se vuelve relativamente más rápido cuando:
- La
read()
llamada al sistema tiene un buen rendimiento de copia. Por ejemplo, buen copy_to_user
rendimiento en el lado del núcleo.
- El núcleo tiene una forma eficiente (en relación con el país de usuario) de asignar memoria, por ejemplo, utilizando solo unas pocas páginas grandes con soporte de hardware.
- El kernel tiene syscalls rápidas y una forma de mantener las entradas TLB del kernel en todas las syscalls.
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:
- Adición de falla, descrita anteriormente, que realmente ayuda al
mmap
caso sin MAP_POPULATE
.
- Adición de
copy_to_user
métodos de vía rápida en arch/x86/lib/copy_user_64.S
, por ejemplo, el uso REP MOVQ
cuando es rápido, que realmente ayudan al read()
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 los mmap
métodos basados, ya que los read()
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 memoria MAP_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
y read
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
o fadvise
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.
mmap()
es 2-6 veces más rápido que usar syscalls, por ejemploread()
.