¿Qué debería saber todo progtwigdor sobre la memoria?

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

Por lo que recuerdo, el contenido de Drepper describe conceptos fundamentales sobre la memoria: cómo funciona la memoria caché de la CPU, qué son la memoria física y virtual y cómo el núcleo de Linux trata ese zoológico. Probablemente hay referencias API desactualizadas 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. “Lo que todo progtwigdor debería saber sobre la memoria” definitivamente vale la pena leerlo, pero, bueno, no creo que sea para “todos los progtwigdores”. Es más adecuado para chicos del sistema / embedded / kernel.

Desde mi mirada rápida, parece bastante preciso. Una cosa a tener en cuenta es la parte de la diferencia entre los controladores de memoria “integrados” y “externos”. Desde el lanzamiento de la línea i7, las CPU Intel están todas integradas, y AMD ha estado usando controladores de memoria integrados desde que se lanzaron por primera vez los chips AMD64.

Desde que se escribió este artículo, no mucho ha cambiado, 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 están realizando los cambios), pero no mucho ha cambiado. . Al menos no de ninguna manera le importaría a un desarrollador de software.

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

En general, sigue siendo excelente y altamente recomendado (por mí, y creo que 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 la wiki de la etiqueta x86 . (El artículo de Ulrich no es específico de 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 las memorias DRAM y las cachés aún se aplican . DDR4 usa los mismos comandos que se describen para DDR1 / DDR2 (lectura / escritura de ráfaga). 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 de latencia enlazadas de esta respuesta para obtener detalles importantes sobre el efecto de la memoria / latencia L3 en el ancho de bandwidth <= max_concurrency / latency solo subproceso: bandwidth <= max_concurrency / latency , y este es realmente el principal cuello de botella para un ancho de banda único en un moderno -core CPU como un Xeon. (Pero un escritorio Skylake de cuatro núcleos puede acercarse a maximizar el ancho de banda de DRAM con un solo hilo). Ese enlace tiene información muy buena sobre las tiendas NT en comparación con las tiendas normales en x86.

Por lo tanto, la sugerencia de Ulrich en 6.5.8 Utilizar todo el ancho de banda (mediante el uso de memoria remota en otros nodos NUMA y el suyo propio) es contraproducente en hardware moderno donde los controladores de memoria tienen más ancho de banda que un solo núcleo. Bueno, posiblemente pueda imaginarse una situación en la que hay algunos beneficios al ejecutar múltiples hilos de memoria en el mismo nodo NUMA para la comunicación entre subprocesos de baja latencia, pero hacer que usen memoria remota para cosas de gran ancho de banda no sensibles a la latencia. Pero esto es bastante oscuro; por lo general, en lugar de utilizar intencionalmente la memoria remota cuando podría haber usado local, solo divida los hilos entre los nodos NUMA y pídales que usen la memoria local.


(por lo general) 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 P4 y puede reconocer patrones de acceso restringido hasta una zancada bastante grande, y múltiples flujos a la vez (por ejemplo, un avance / retroceso por página de 4k). El manual de optimización de Intel describe algunos detalles de los prefetchers de HW en varios niveles de caché para su microarchitecture Sandybridge-family. Ivybridge y luego tienen precarga de hardware de próxima página, en lugar de esperar a que falte un caché en la nueva página para desencadenar 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 ejemplo, la no laminación de uops micro-fusionados cambió en HSW y el manual no lo menciona .

El consejo habitual en estos días es eliminar toda la captación previa SW del código anterior , y solo considerar volver a colocarla si el perfil muestra fallas en la caché (y no está saturando el ancho de banda de la memoria). La obtención de los dos lados del próximo paso de una búsqueda binaria puede ayudar. por ejemplo, una vez que decidas qué elemento mirar a continuación, recupera los elementos 1/4 y 3/4 para que puedan cargarse en paralelo con la carga / comprobación del medio.

La sugerencia de usar un subproceso de captación previa independiente (6.3.4) es totalmente obsoleta , creo, y solo fue buena en Pentium 4. P4 tenía hyperthreading (2 núcleos lógicos compartían un núcleo físico), pero no lo suficiente fuera de orden recursos de ejecución o trace-cache para obtener un rendimiento que ejecuta dos hilos de cálculo completos en el mismo núcleo. Pero los CPU modernos (Sandybridge-family y Ryzen) son mucho más robustos y deberían ejecutar un hilo conductor o no usar hyperthreading (deje el otro núcleo lógico inactivo para que el hilo solo tenga todos los recursos).

La captación previa de software siempre ha sido "frágil" : los números correctos de ajuste mágico para obtener una aceleración dependen de los detalles del hardware, y tal vez de la carga del sistema. Demasiado temprano y es desalojado antes de la carga de demanda. Demasiado tarde y no ayuda. Este artículo de blog muestra códigos + gráficos para un experimento interesante al usar la captación previa de SW en Haswell para la recuperación previa de la parte no secuencial de un problema. Consulte también ¿Cómo usar correctamente las instrucciones de captación previa? . La captación previa NT es interesante, pero aún más frágil (porque un desalojo anticipado de L1 significa que tienes que ir hasta L3 o DRAM, no solo L2). Si necesita hasta la última gota de rendimiento y puede sintonizar una máquina específica, la captación previa de SW vale la pena mirar para obtener acceso secuencial, pero si todavía puede haber una desaceleración si tiene suficiente trabajo de ALU 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 L1D es muy alto, y las CPU modernas pueden hacer 2 cargas vectoriales por reloj + 1 vector store si todo golpea en L1D. Consulte ¿Cómo puede la memoria caché ser tan rápida? ). Con AVX512, tamaño de línea = ancho del vector, para que pueda cargar / almacenar una línea de caché completa en una instrucción. (Y, por lo tanto, cada carga / almacén desalineado cruza un límite de la línea de caché, en lugar de todos los demás para 256b AVX1 / AVX2, que a menudo no ralentiza el bucle en una matriz que no estaba en L1D).

Las instrucciones de carga desalineada no tienen penalidad si la dirección está alineada en el tiempo de ejecución, pero los comstackdores (especialmente gcc) hacen un mejor código cuando se autovectorizan si conocen alguna garantía de alineación. En realidad, las operaciones desalineadas son generalmente rápidas, pero las divisiones de página aún duelen (mucho menos en Skylake, solo ~ 11 lapsos de ciclos extra frente a 100, pero aún una penalización de rendimiento).


Como predijo Ulrich, cada sistema multi-socket es NUMA actualmente: los controladores de memoria integrados son estándar, es decir, no hay ningún Northbridge externo. Pero SMP ya no significa multi-socket, porque las CPU multinúcleo están muy extendidas. (Las CPU Intel de Nehalem a Skylake han utilizado un gran caché L3 inclusivo como barrera para la coherencia entre los núcleos.) Las CPU AMD son diferentes, pero no estoy tan claro en los detalles.

Skylake-X (AVX512) ya no tiene un L3 inclusivo, pero creo que todavía hay un directorio de tags que le permite verificar lo que se almacena en caché en cualquier lugar del chip (y si es así) sin transmitir realmente snoops a todos los núcleos. SKX utiliza una malla en lugar de un bus de anillo , con una latencia generalmente peor que los anteriores Xeons de muchos núcleos, desafortunadamente.

Básicamente, todos los consejos sobre la optimización de la ubicación de la memoria aún se aplican, solo varían los detalles de lo que sucede exactamente cuando no se puede evitar el error de caché o la contención.


6.4.2 Operaciones atómicas : el punto de referencia que muestra un bucle de rebash CAS como 4 veces peor que el lock add hardware probablemente aún refleje un caso de contención máximo . Pero en los progtwigs reales de subprocesos múltiples, la sincronización se mantiene al mínimo (porque es costosa), por lo que la contienda es baja y un ciclo de rebash CAS generalmente tiene éxito sin tener que volver a intentarlo.

C ++ 11 std::atomic fetch_add se comstackrá para lock add un lock add (o lock xadd si se usa el valor de retorno), pero un algoritmo que usa CAS para hacer algo que no se puede hacer con una instrucción de lock generalmente no es desastre. Utilice C ++ 11 std::atomic o C11 stdatomic lugar de gcc legacy __sync built-ins o los nuevos __atomic built-ins a menos que desee mezclar acceso atómico y no atómico a la misma ubicación ...

8.1 DCAS ( cmpxchg16b ) : Puede convencer a gcc para que lo cmpxchg16b , pero si quiere cargas eficientes de solo la mitad del objeto, necesita feos ataques union : ¿cómo puedo implementar el contador ABA con c ++ 11 CAS?

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 desencadena), Intel tiene memoria transaccional en funcionamiento en el último modelo de Broadwell y todas las CPU Skylake. El diseño sigue siendo lo que David Kanter describió para Haswell . Hay una forma de usar lock-ellision para acelerar el código que usa (y puede recurrir a) un locking regular (especialmente con un único locking para todos los elementos de un contenedor, por lo que múltiples hilos en la misma sección crítica a menudo no colisionan ), o para escribir código que sepa sobre transacciones directamente.


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

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


Apéndice B: Oprofile : Linux perf ha reemplazado principalmente a oprofile . Para eventos detallados específicos de ciertas microarchitectures, use el contenedor ocperf.py . 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, ¿puede el MOV de Can x86 ser realmente "libre"? ¿Por qué no puedo reproducir esto en absoluto? .