¿Qué debe saber todo programador sobre la memoria?


164

Me pregunto cuánto de lo que todo programador debe saber sobre la memoria de 2007 de Ulrich Drepper sigue siendo válido. Además, no pude encontrar una versión más nueva que la 1.0 o una errata.


1
¿Alguien sabe si puedo descargar este artículo en formato mobi en algún lugar para poder leerlo fácilmente en kindle? "pdf" es muy difícil de leer debido a problemas con el zoom / formato
javapowered

1
No es mobi, pero LWN publicó el documento como un conjunto de artículos que son más fáciles de leer en un teléfono / tableta. El primero está en lwn.net/Articles/250967
Nathan

Respuestas:


111

Hasta donde recuerdo, el contenido de Drepper describe conceptos fundamentales sobre la memoria: cómo funciona la memoria caché de la CPU, qué es la memoria física y virtual y cómo el kernel de Linux maneja ese zoológico. Probablemente hay referencias API obsoletas en algunos ejemplos, pero no importa; eso no afectará la relevancia de los conceptos fundamentales.

Por lo tanto, cualquier libro o artículo que describa algo fundamental no puede llamarse obsoleto. Definitivamente vale la pena leer lo que todo programador debe saber sobre la memoria, pero, bueno, no creo que sea para "todos los programadores". Es más adecuado para el sistema / incrustado / kernel.


3
Sí, realmente no veo por qué un programador debería saber cómo funcionan SRAM y DRAM en el nivel analógico, eso no ayudará mucho al escribir programas. Y las personas que realmente necesitan ese conocimiento, mejor pasan el tiempo leyendo los manuales sobre detalles sobre los tiempos reales, etc. ¿Pero para las personas interesadas en las cosas de bajo nivel de HW? Quizás no sea útil, pero al menos entretenido.
Voo

47
Hoy en día rendimiento == rendimiento de memoria, por lo que comprender la memoria es lo más importante en cualquier aplicación de alto rendimiento. Esto hace que el documento sea esencial para cualquier persona involucrada en: desarrollo de juegos, informática científica, finanzas, bases de datos, compiladores, procesamiento de grandes conjuntos de datos, visualización, cualquier cosa que tenga que manejar muchas solicitudes ... Entonces, si está trabajando en una aplicación eso está inactivo la mayor parte del tiempo, como un editor de texto, el trabajo no tiene ningún interés hasta que necesitas hacer algo rápido como encontrar una palabra, contar las palabras, revisar la ortografía ... oh, espera ... no importa.
gnzlbg

144

La guía en formato PDF está en https://www.akkadia.org/drepper/cpumemory.pdf .

En general, sigue siendo excelente y muy recomendable (por mí y por otros expertos en ajuste de rendimiento). Sería genial si Ulrich (o cualquier otra persona) escribiera una actualización de 2017, pero eso sería mucho trabajo (por ejemplo, volver a ejecutar los puntos de referencia). Consulte también otros enlaces de optimización de rendimiento x86 y SSE / asm (y C / C ++) en el etiqueta wiki . (El artículo de Ulrich no es específico para x86, pero la mayoría (todos) de sus puntos de referencia están en hardware x86).

Los detalles de hardware de bajo nivel sobre cómo funcionan la DRAM y las memorias caché todavía se aplican . DDR4 utiliza los mismos comandos que se describen para DDR1 / DDR2 (ráfaga de lectura / escritura). Las mejoras DDR3 / 4 no son cambios fundamentales. AFAIK, todas las cosas independientes del arco todavía se aplican en general, por ejemplo, a AArch64 / ARM32.

Consulte también la sección Plataformas enlazadas a la latencia de esta respuesta para obtener detalles importantes sobre el efecto de la memoria / latencia L3 en el ancho de banda de un solo hilo: bandwidth <= max_concurrency / latencyy este es en realidad el principal cuello de botella para el ancho de banda de un solo hilo en una CPU moderna de muchos núcleos como un Xeon . Pero un escritorio Skylake de cuatro núcleos puede estar cerca de maximizar el ancho de banda DRAM con un solo hilo. Ese enlace tiene información muy buena sobre las tiendas NT frente a las tiendas normales en x86. ¿Por qué Skylake es mucho mejor que Broadwell-E para el rendimiento de memoria de un solo subproceso? Es un resumen.

Por lo tanto, la sugerencia de Ulrich en 6.5.8 Utilizando todo el ancho de banda sobre el uso de la memoria remota en otros nodos NUMA, así como en el suyo, es contraproducente en el hardware moderno donde los controladores de memoria tienen más ancho de banda de lo que puede usar un solo núcleo. Bueno, posiblemente pueda imaginar una situación en la que haya un beneficio neto al ejecutar múltiples subprocesos que consumen mucha memoria en el mismo nodo NUMA para la comunicación entre subprocesos de baja latencia, pero hacer que usen memoria remota para cosas de alto ancho de banda no sensibles a la latencia. Pero esto es bastante oscuro, normalmente solo divide los hilos entre nodos NUMA y pídales que usen la memoria local. El ancho de banda por núcleo es sensible a la latencia debido a los límites máximos de concurrencia (ver más abajo), pero todos los núcleos en un zócalo generalmente pueden más que saturar los controladores de memoria en ese zócalo.


(generalmente) No utilice la captación previa de software

Una cosa importante que ha cambiado es que la captación previa de hardware es mucho mejor que en el Pentium 4 y puede reconocer patrones de acceso progresivo hasta un paso bastante grande y múltiples transmisiones a la vez (por ejemplo, una hacia adelante / hacia atrás por página de 4k). El manual de optimización de Intel describe algunos detalles de los captadores previos HW en varios niveles de caché para su microarquitectura de la familia Sandybridge. Ivybridge y versiones posteriores tienen una captación previa de hardware en la página siguiente, en lugar de esperar a que se pierda un caché en la nueva página para activar un inicio rápido. Supongo que AMD tiene algunas cosas similares en su manual de optimización. Tenga en cuenta que el manual de Intel también está lleno de consejos antiguos, algunos de los cuales solo son buenos para P4. Las secciones específicas de Sandybridge son, por supuesto, precisas para SnB, pero por ejemplola deslaminación de los uops micro fusionados cambió en HSW y el manual no lo menciona .

El consejo habitual en estos días es eliminar todas las captaciones previas de SW del código anterior , y solo considere volver a colocarlo si el perfil muestra errores de caché (y no está saturando el ancho de banda de la memoria). Obtener previamente ambos lados del siguiente paso de una búsqueda binaria aún puede ayudar. Por ejemplo, una vez que decida qué elemento mirar a continuación, busque previamente los elementos 1/4 y 3/4 para que puedan cargarse en paralelo con la carga / comprobación del centro.

La sugerencia de usar un subproceso de captación previa separado (6.3.4) es totalmente obsoleto , creo, y solo fue bueno en Pentium 4. P4 tenía hyperthreading (2 núcleos lógicos que comparten un núcleo físico), pero no suficiente caché de rastreo (y / o recursos de ejecución fuera de orden) para obtener rendimiento ejecutando dos subprocesos de cálculo completos en el mismo núcleo. Pero las CPU modernas (Sandybridge-family y Ryzen) son mucho más robustas y deberían ejecutar un hilo real o no usar hyperthreading (deje el otro núcleo lógico inactivo para que el hilo solo tenga todos los recursos en lugar de particionar el ROB).

La captación previa de software siempre ha sido "frágil" : los números de ajuste mágico correctos para obtener una aceleración dependen de los detalles del hardware y quizás de la carga del sistema. Demasiado temprano y es desalojado antes de la carga de la demanda. Demasiado tarde y no ayuda. Este artículo de blog muestra gráficos de código + para un experimento interesante en el uso de captación previa de SW en Haswell para captar previamente la parte no secuencial de un problema. Consulte también ¿Cómo usar correctamente las instrucciones de captación previa? . La captación previa de NT es interesante, pero aún más frágil porque un desalojo temprano de L1 significa que tiene que ir hasta L3 o DRAM, no solo L2. Si necesita hasta la última caída de rendimiento, y puede sintonizar una máquina específica, vale la pena mirar la captación previa de SW para acceso secuencial, perotodavía puede ser una ralentización si tiene suficiente trabajo de ALU para hacer mientras se acerca al cuello de botella en la memoria.


El tamaño de la línea de caché sigue siendo de 64 bytes. (El ancho de banda de lectura / escritura de L1D es muy alto, y las CPU modernas pueden hacer 2 cargas de vectores por reloj + 1 almacenamiento de vectores si todo golpea en L1D. Consulte ¿Cómo puede ser tan rápido el caché? ). Con AVX512, tamaño de línea = ancho de vector, para que pueda cargar / almacenar una línea de caché completa en una sola instrucción. Por lo tanto, cada carga / almacén desalineado cruza un límite de línea de caché, en lugar de todos los demás para 256b AVX1 / AVX2, que a menudo no ralentiza el bucle sobre una matriz que no estaba en L1D.

Las instrucciones de carga no alineadas tienen penalización cero si la dirección está alineada en tiempo de ejecución, pero los compiladores (especialmente gcc) hacen un mejor código al autovectorizar si conocen alguna garantía de alineación. Las operaciones en realidad no alineadas son generalmente rápidas, pero las divisiones de página todavía duelen (mucho menos en Skylake, sin embargo; solo ~ 11 ciclos adicionales de latencia frente a 100, pero sigue siendo una penalización de rendimiento).


Como Ulrich predijo, todos los sistemas de múltiples sockets son NUMA en estos días: los controladores de memoria integrados son estándar, es decir, no hay un Northbridge externo. Pero SMP ya no significa multi-socket, porque las CPU multi-core están muy extendidas. Las CPU Intel de Nehalem a Skylake han utilizado un gran caché L3 inclusivo como respaldo para la coherencia entre núcleos. Las CPU AMD son diferentes, pero no soy tan claro en los detalles.

Skylake-X (AVX512) ya no tiene un L3 inclusivo, pero creo que todavía hay un directorio de etiquetas que le permite verificar qué está almacenado en caché en cualquier lugar del chip (y, de ser así, dónde) sin transmitir snoops a todos los núcleos. SKX usa una malla en lugar de un bus de anillo , con una latencia generalmente peor que los Xeons de muchos núcleos anteriores, desafortunadamente.

Básicamente, todos los consejos sobre la optimización de la ubicación de la memoria todavía se aplican, solo varían los detalles de lo que sucede exactamente cuando no se pueden evitar errores de caché o contenciones.


6.4.2 Operaciones atómicas : el punto de referencia que muestra un bucle de reintento de CAS 4 veces peor que el arbitrado por hardware lock addprobablemente todavía refleja un caso de contención máxima . Pero en programas reales de subprocesos múltiples, la sincronización se mantiene al mínimo (porque es costosa), por lo que la contención es baja y un bucle de reintento de CAS generalmente tiene éxito sin tener que volver a intentarlo.

C ++ 11 std::atomic fetch_addse compilará en a lock add(o lock xaddsi se usa el valor de retorno), pero un algoritmo que usa CAS para hacer algo que no se puede hacer con una lockinstrucción ed generalmente no es un desastre. Use C ++ 11std::atomic o C11 en stdatomiclugar de las __syncincorporaciones heredadas de gcc o las __atomicincorporaciones más nuevas , a menos que desee combinar el acceso atómico y no atómico a la misma ubicación ...

8.1 DWCAS ( cmpxchg16b) : puede convencer a gcc para que lo emita , pero si desea cargas eficientes de solo la mitad del objeto, necesita unionhacks feos : ¿cómo puedo implementar el contador ABA con c ++ 11 CAS? . (No confunda DWCAS con DCAS de 2 ubicaciones de memoria separadas . La emulación atómica sin bloqueo de DCAS no es posible con DWCAS, pero la memoria transaccional (como x86 TSX) lo hace posible).

8.2.4 memoria transaccional : después de un par de inicios falsos (liberados y luego deshabilitados por una actualización de microcódigo debido a un error que rara vez se activa), Intel tiene memoria transaccional en funcionamiento en Broadwell y todas las CPU Skylake. El diseño sigue siendo lo que David Kanter describió para Haswell . Hay una forma de elusión de bloqueo para usarlo para acelerar el código que usa (y puede recurrir a) un bloqueo normal (especialmente con un solo bloqueo para todos los elementos de un contenedor, por lo que múltiples hilos en la misma sección crítica a menudo no chocan) ), o para escribir código que sepa sobre transacciones directamente.


7.5 Grandes páginas : las grandes páginas transparentes anónimas funcionan bien en Linux sin tener que usar manualmente hugetlbfs. Haga asignaciones> = 2MiB con una alineación de 2MiB (por ejemplo posix_memalign, o unaaligned_alloc que no imponga el estúpido requisito de ISO C ++ 17 para fallar cuando size % alignment != 0).

Una asignación anónima alineada con 2MiB utilizará enormes páginas por defecto. Algunas cargas de trabajo (por ejemplo, que siguen usando asignaciones grandes durante un tiempo después de hacerlas) pueden beneficiarse
echo always >/sys/kernel/mm/transparent_hugepage/defragal hacer que el núcleo desfragmente la memoria física siempre que sea necesario, en lugar de retroceder a 4k páginas. (Ver los documentos del kernel ). Alternativamente, use madvise(MADV_HUGEPAGE)después de hacer grandes asignaciones (preferiblemente aún con una alineación de 2MiB).


Apéndice B: Perfil : Linux se perfha reemplazado en su mayoría oprofile. Para eventos detallados específicos de ciertas microarquitecturas, use el ocperf.pycontenedor . p.ej

ocperf.py stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,\
branches,branch-misses,instructions,uops_issued.any,\
uops_executed.thread,idq_uops_not_delivered.core -r2 ./a.out

Para ver algunos ejemplos de su uso, consulte ¿Puede el MOV de x86 ser realmente "gratis"? ¿Por qué no puedo reproducir esto en absoluto? .


3
Muy instructiva respuesta y punteros! ¡Esto claramente merece más votos!
claf

@ Peter Cordes ¿Hay otras guías / documentos que recomendaría leer? No soy un programador de alto rendimiento, pero me gustaría aprender más al respecto y espero aprender prácticas que pueda incorporar a mi programación diaria.
user3927312

44
@ user3927312: agner.org/optimize es una de las mejores y más coherentes guías de material de bajo nivel para x86 específicamente, pero algunas de las ideas generales se aplican a otras NIA. Además de las guías asm, Agner tiene un PDF C ++ optimizador. Para otros enlaces de arquitectura de rendimiento / CPU, consulte stackoverflow.com/tags/x86/info . También he escrito algo sobre la optimización de C ++ ayudando al compilador a hacer un mejor asm para los bucles críticos cuando vale la pena echar un vistazo a la salida asm del compilador: ¿ código C ++ para probar la conjetura de Collatz más rápido que el asm escrito a mano?
Peter Cordes

74

Desde mi rápido vistazo, parece bastante preciso. Una cosa a notar es la porción en la diferencia entre controladores de memoria "integrados" y "externos". Desde el lanzamiento de la línea i7, las CPU Intel están integradas, y AMD ha estado utilizando controladores de memoria integrados desde que se lanzaron los chips AMD64.

Desde que se escribió este artículo, no ha cambiado mucho, las velocidades han aumentado, los controladores de memoria se han vuelto mucho más inteligentes (el i7 retrasará las escrituras en la RAM hasta que parezca que se cometen los cambios), pero no ha cambiado mucho. . Al menos no de ninguna manera que le interese a un desarrollador de software.


55
Me hubiera gustado aceptarlos a ambos. Pero he votado tu publicación.
Framester

55
Probablemente el cambio más importante que sea relevante para los desarrolladores de SW es ​​que los subprocesos de captación previa son una mala idea. Las CPU son lo suficientemente potentes como para ejecutar 2 subprocesos completos con hyperthreading y tienen una captación previa de HW mucho mejor. La captación previa de SW en general es mucho menos importante, especialmente para el acceso secuencial. Mira mi respuesta.
Peter Cordes
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.