¿Cuánto de 'Lo que todo programador debería saber sobre la memoria' sigue siendo válido?

Resuelto Framester asked hace 12 años • 3 respuestas

Me pregunto qué parte de Lo que todo programador debería saber sobre la memoria de Ulrich Drepper de 2007 sigue siendo válido. Además, no pude encontrar una versión más reciente que la 1.0 o una errata.

(También en formato PDF en el sitio de Ulrich Drepper: https://www.akkadia.org/drepper/cpumemory.pdf )

Framester avatar Nov 15 '11 01:11 Framester
Aceptado

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

En general, sigue siendo excelente y muy recomendado (por mí y creo que por otros expertos en ajuste del rendimiento). Sería genial si Ulrich (o cualquier otra persona) escribiera una actualización de 2017, pero eso requeriría mucho trabajo (por ejemplo, volver a ejecutar los puntos de referencia). Consulte también otros enlaces de optimización del rendimiento de x86 y SSE/asm (y C/C++) en elx86 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 los cachés aún 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 vinculadas a latencia de esta respuesta para obtener detalles importantes sobre el efecto de la latencia de memoria/L3 en el ancho de banda de un solo subproceso: bandwidth <= max_concurrency / latencyy este es en realidad el principal cuello de botella para el ancho de banda de un solo subproceso en una CPU moderna de muchos núcleos como un Xeon. . Pero una computadora de escritorio Skylake de cuatro núcleos puede acercarse a maximizar el ancho de banda de DRAM con un solo hilo. Ese enlace tiene muy buena información sobre las tiendas NT frente a las tiendas normales en x86. ¿Por qué Skylake es mucho mejor que Broadwell-E en cuanto a 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 memoria remota en otros nodos NUMA además del suyo propio, es contraproducente en hardware moderno donde los controladores de memoria tienen más ancho de banda del 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 una comunicación entre subprocesos de baja latencia, pero hacer que usen memoria remota para cosas de alto ancho de banda que no son sensibles a la latencia. Pero esto es bastante oscuro, normalmente simplemente divide los subprocesos entre nodos NUMA y haz que usen la memoria local. El ancho de banda por núcleo es sensible a la latencia debido a los límites de concurrencia máxima (ver más abajo), pero todos los núcleos en un socket generalmente pueden saturar con creces los controladores de memoria en ese socket.


(normalmente) 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 a zancadas hasta un paso bastante grande y múltiples flujos a la vez (por ejemplo, uno hacia adelante/hacia atrás por página de 4k). El manual de optimización de Intel describe algunos detalles de los captadores previos de 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 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 sólo son válidos para el P4. Las secciones específicas de Sandybridge son, por supuesto, precisas para SnB, pero, por ejemplo, la deslaminación de uops microfusionados cambió en HSW y el manual no lo menciona .

El consejo habitual en estos días es eliminar toda la captación previa de software del código antiguo y solo considerar volver a colocarlo si el perfil muestra errores de caché (y no está saturando el ancho de banda de la memoria). La búsqueda previa de ambos lados del siguiente paso de una búsqueda binaria aún puede resultar útil. por ejemplo, una vez que decida qué elemento mirar a continuación, precapture los elementos 1/4 y 3/4 para que puedan cargarse en paralelo con la carga/verificación del medio.

Creo que la sugerencia de utilizar un subproceso de captación previa independiente (6.3.4) es totalmente obsoleta y solo fue buena en Pentium 4. P4 tenía hiperprocesamiento (2 núcleos lógicos que comparten un núcleo físico), pero no suficiente caché de seguimiento (y /o recursos de ejecución fuera de orden) para obtener rendimiento al ejecutar dos subprocesos de cálculo completos en el mismo núcleo. Pero las CPU modernas (la familia Sandybridge y Ryzen) son mucho más potentes y deberían ejecutar un subproceso real o no utilizar hyperthreading (deje el otro núcleo lógico inactivo para que el subproceso individual tenga todos los recursos en lugar de particionar el ROB).

La captación previa del 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 tal vez de la carga del sistema. Demasiado pronto y será desalojado antes de que llegue la carga de demanda. Demasiado tarde y no ayuda. Este artículo de blog muestra código y gráficos para un experimento interesante sobre el uso de la captación previa de SW en Haswell para captar previamente la parte no secuencial de un problema. Consulte también ¿Cómo utilizar 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 anticipado de L1 significa que tienes que llegar hasta L3 o DRAM, no solo L2. Si necesita hasta la última gota de rendimiento y puede ajustar una máquina específica, vale la pena considerar la captación previa de SW para el acceso secuencial, pero aún así puede ser una desaceleración si tiene suficiente trabajo de ALU por 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 realizar 2 cargas vectoriales por reloj + 1 almacén de vectores si todo llega a L1D. Consulte ¿Cómo puede el caché ser tan rápido? ). 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/almacenamiento desalineado cruza un límite de línea de caché, en lugar de cualquier otro para 256b AVX1/AVX2, lo que a menudo no ralentiza el bucle sobre una matriz que no estaba en L1D.

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


Como predijo Ulrich, hoy en día cualquier sistema multi-socket es NUMA: los controladores de memoria integrados son estándar, es decir, no hay ningún Northbridge externo. Pero SMP ya no significa multisocket, porque las CPU multinúcleo están muy extendidas. Las CPU Intel desde Nehalem hasta Skylake han utilizado una gran caché L3 inclusiva como respaldo para la coherencia entre los núcleos. Las CPU AMD son diferentes, pero no tengo tan claros 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 espías a todos los núcleos. SKX utiliza un bus de malla en lugar de un bus de anillo , desafortunadamente con una latencia generalmente incluso peor que la de los Xeon de muchos núcleos anteriores.

Básicamente, todos los consejos sobre la optimización de la ubicación de la memoria siguen siendo válidos, solo los detalles de qué sucede exactamente cuando no se pueden evitar errores de caché o contención varían.


6.1 Omitir el caché - SSE4.1 movntdqa( _mm_stream_load_si128)las cargas NT solo hacen algo en las regiones de memoria WC. En la memoria normal que obtiene de malloc/ newo mmap(atributo de memoria WB = reescritura en caché), movntqdase ejecuta igual que una carga SIMD normal, sin omitir caché. Pero cuesta una ALU uop adicional. AFAIK, esto era cierto incluso en las CPU en el momento en que se escribió el artículo, lo que lo convierte en un error poco común en la guía. A diferencia de las tiendas NT, las cargas NT no anulan las reglas habituales de ordenación de memoria para la región. Y tienen que respetar la coherencia, por lo que no pueden omitir completamente el caché en regiones cacheables por WB, los datos deben estar en algún lugar que otros núcleos puedan invalidar en escritura. Pero SSE4.1 no se introdujo hasta el Core de segunda generación. 2, por lo que no había CPU de un solo núcleo.

La captación previa de NT ( prefetchnta) puede minimizar la contaminación de la caché, pero aún llena la caché L1d y una forma de L3 en las CPU Intel con caché L3 inclusiva. Pero es frágil y difícil de ajustar: si se acorta una distancia de captación previa, se obtienen cargas de demanda que probablemente anulen el aspecto NT; si es demasiado larga, los datos se desalojan antes de su uso. Y como no estaba en L2, y tal vez ni siquiera en L3, podría perderse hasta la DRAM. Dado que la distancia de captación previa depende del sistema y de la carga de trabajo de otro código, no solo del suyo propio, esto es un problema.

Relacionado:

  • Diferencia entre instrucciones PREFETCH y PREFETCHNTA
  • Las cargas no temporales y el captador previo de hardware, ¿funcionan juntos?
  • Copia de búferes de fotogramas de decodificación de vídeo acelerada : documento técnico de Intel que describe el caso de uso de SSE4.1 movntdqa, lectura desde la RAM de vídeo.

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 aún 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 ciclo de reintento 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. Utilice C++11std::atomic o C11 stdatomicen lugar de las __syncfunciones integradas heredadas de gcc o las __atomicfunciones integradas 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) : puedes convencer a gcc para que lo emita, pero si quieres cargas eficientes de solo la mitad del objeto, necesitas uniontrucos 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 en falso (liberados y luego deshabilitados por una actualización de microcódigo debido a un error que rara vez se activa), Intel tiene memoria transaccional funcional en el último modelo de Broadwell y en todas las CPU Skylake. El diseño sigue siendo lo que David Kanter describió para Haswell . Existe una forma de elisión de bloqueo para usarlo para acelerar el código que usa (y puede recurrir a) un bloqueo normal (especialmente con un único bloqueo para todos los elementos de un contenedor, de modo que varios subprocesos en la misma sección crítica a menudo no colisionan). ), o escribir código que conozca las transacciones directamente.

Actualización: y ahora Intel ha desactivado la elisión de bloqueo en CPU posteriores (incluido Skylake) con una actualización de microcódigo. La parte no transparente RTM (xbegin/xend) de TSX aún puede funcionar si el sistema operativo lo permite, pero TSX en general se está convirtiendo seriamente en el fútbol de Charlie Brown .

  • ¿Ha desaparecido Hardware Lock Elision para siempre debido a Spectre Mitigation? (Sí, pero debido a un tipo MDS de vulnerabilidad de canal lateral ( TAA ), no a Spectre. Tengo entendido que el microcódigo actualizado desactiva totalmente HLE. En ese caso, el sistema operativo solo puede habilitar RTM, no HLE).

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

Una asignación anónima alineada con 2MiB utilizará páginas enormes de forma predeterminada. Algunas cargas de trabajo (por ejemplo, que siguen usando grandes asignaciones durante un tiempo después de realizarlas) pueden beneficiarse de
echo defer+madvise >/sys/kernel/mm/transparent_hugepage/defragque el kernel desfragmente la memoria física cuando sea necesario, en lugar de recurrir a páginas de 4k. (Consulte los documentos del kernel ). Úselo madvise(MADV_HUGEPAGE)después de realizar asignaciones grandes (preferiblemente todavía con una alineación de 2MiB) para alentar más fuertemente al kernel a detenerse y desfragmentarse ahora. defrag = alwayses demasiado agresivo para la mayoría de las cargas de trabajo y dedicará más tiempo a copiar páginas del que ahorra en errores de TLB. (kcompactd tal vez podría ser más eficiente ).

Por cierto, Intel y AMD llaman a las páginas de 2 millones "páginas grandes", y "enorme" solo se usa para páginas de 1G. Linux usa "página enorme" para todo lo que es más grande que el tamaño estándar.

(Las tablas de páginas heredadas en modo de 32 bits (no PAE) solo tenían 4 millones de páginas como el siguiente tamaño más grande, con solo tablas de páginas de 2 niveles con entradas más compactas. El siguiente tamaño habría sido 4G, pero ese es todo el espacio de direcciones. , y ese "nivel" de traducción es el registro de control CR3, no una entrada del directorio de páginas. IDK si eso está relacionado con la terminología de Linux).


Apéndice B: Oprofile : Linux perfha reemplazado en su mayor parte a oprofile. perf list/ perf stat -e event1,event2 ...tiene nombres para la mayoría de las formas útiles de programar contadores de rendimiento de HW.

perf 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

Hace unos años, el ocperf.pycontenedor era necesario para traducir los nombres de eventos en códigos, pero hoy en día perftiene esa funcionalidad incorporada.

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

Peter Cordes avatar Dec 08 '2017 12:12 Peter Cordes

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

Por tanto, cualquier libro o artículo que describa algo fundamental no puede considerarse obsoleto. Definitivamente vale la pena leer "Lo que todo programador debería saber sobre la memoria", pero, bueno, no creo que sea para "todos los programadores". Es más adecuado para los chicos de sistemas/integrados/kernel.

Dan Kruchinin avatar Nov 14 '2011 18:11 Dan Kruchinin

Según mi rápido vistazo, parece bastante preciso. Lo único que hay que tener en cuenta es la parte sobre la diferencia entre controladores de memoria "integrados" y "externos". Desde el lanzamiento de la línea i7, todas las CPU Intel están integradas y AMD ha estado utilizando controladores de memoria integrados desde que se lanzaron por primera vez 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 tenga ganas de realizar los cambios), pero no ha cambiado mucho. . Al menos no de la forma que le interesaría a un desarrollador de software.

Timothy Baldridge avatar Nov 14 '2011 18:11 Timothy Baldridge