Lo Que Todo Programador Debe Saber Sobre La Memoria?


Me pregunto cuánto de Ulrich Drepper Lo que Todo Programador Debería Saber Sobre Memoria de 2007 sigue siendo válido. Tampoco pude encontrar una versión más nueva que la 1.0 o una errata.

Author: Peter Cordes, 2011-11-14

3 answers

Por lo que recuerdo, el contenido de Drepper describe conceptos fundamentales sobre la memoria: cómo funciona la caché de la CPU, qué son la memoria física y virtual y cómo el kernel de Linux se ocupa de ese zoológico. Probablemente hay referencias de 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 ser llamado obsoleto. "Lo que todo programador debe saber sobre la memoria" definitivamente vale la pena para leer, pero, bueno, no creo que sea para "todos los programadores". Es más adecuado para los chicos de system/embedded/kernel.

 78
Author: Dan Kruchinin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-24 17:22:06

De mi rápida mirada, parece bastante precisa. La única cosa a notar, es la porción en la diferencia entre los 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 se sienta como confirmar los cambios), pero no ha cambiado mucho. Al menos no de ninguna manera que a un desarrollador de software le importe.

 65
Author: Timothy Baldridge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-14 18:40:52

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

Sigue siendo generalmente excelente y altamente recomendado (por mí, y creo que por otros expertos en afinación 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). Ver también otros enlaces de optimización de rendimiento x86 y SSE/asm (y C/C++) en x86 tag wiki . (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 DRAM y las cachés siguen siendo aplicables . DDR4 usa los mismos comandos que se describen para DDR1/DDR2 (ráfaga de lectura/escritura). Las mejoras de DDR3 / 4 no son cambios fundamentales. AFAIK, todas las cosas independientes del arco todavía se aplican generalmente, por ejemplo, a AArch64 / ARM32.

Ver también la sección Plataformas enlazadas de latencia de esta respuesta para detalles importantes sobre el efecto de la latencia de memoria/L3 en el ancho de banda de un solo subproceso: bandwidth <= max_concurrency / latency, y este es en realidad el cuello de botella principal para el ancho de banda de un solo subproceso en una CPU moderna de muchos núcleos como una 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 tiendas NT vs. tiendas normales en x86.

Así la sugerencia de Ulrich en 6.5.8 Utilizando Todo el Ancho de banda (mediante el uso de memoria remota en otros nodos NUMA, así como el suyo propio) es contraproducente en el hardware moderno, donde los controladores de memoria tienen más ancho de banda que un solo núcleo puede utilizar. Bueno, posiblemente se puede imaginar una situación en la que hay algún beneficio de ejecutar múltiples subprocesos hambrientos 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 alto ancho de banda no sensibles a la latencia. Pero esto es bastante oscuro; por lo general, en lugar de intencionalmente usando memoria remota cuando podría haber usado local, simplemente divida los hilos entre los nodos NUMA y pídales que usen memoria local.


(generalmente) No use prefetch de software

Una cosa importante que ha cambiado es que el prefetch de hardware es mucho mejor que en P4 y puede reconocer patrones de acceso con estrías hasta una zancada bastante grande, y múltiples flujos a la vez (por ejemplo, un avance / retroceso por página 4k). Manual de optimización de Intel describe algunos detalles de los prefetchers HW en varios niveles de caché para su microarquitectura de la familia Sandybridge. Ivybridge y más tarde tienen prefetch de hardware de la página siguiente, en lugar de esperar a que se pierda la 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 viejos consejos, algunos de los cuales solo son buenos para P4. Las secciones específicas de Sandybridge son, por supuesto, precisas para SnB, pero p. 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 todas las prefetch SW del código antiguo, y solo considere volver a colocarlo si el perfil muestra errores de caché (y no está saturando el ancho de banda de la memoria). Prefetching los dos lados de la siguiente paso de una búsqueda binaria todavía puede ayudar. por ejemplo, una vez que decida qué elemento mirar a continuación, prefetch los elementos 1/4 y 3/4 para que puedan carga en paralelo con el centro de carga/comprobación.

La sugerencia de usar un subproceso de prefetch separado (6.3.4) es totalmente obsoleta, creo, y solo fue buena en Pentium 4. P4 tenía hyperthreading (2 núcleos lógicos que compartían un núcleo físico), pero no suficientes recursos de ejecución fuera de orden o caché de seguimiento 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 fuertes y deberían ejecute un subproceso real o no use hyperthreading (deje el otro núcleo lógico inactivo para que el subproceso solo tenga todos los recursos.)

El prefetch de software siempre ha sido "frágil" : los números de ajuste mágicos correctos para obtener una aceleración dependen de los detalles del hardware, y tal vez de la carga del sistema. Demasiado pronto y es desalojado antes de la carga de demanda. Demasiado tarde y no ayuda. Este artículo de blog muestra gráficos de código + para un interesante experimento en el uso de SW prefetch en Haswell para prefetching la parte no secuencial de un problema. Ver también ¿Cómo utilizar correctamente las instrucciones de prefetch?. NT prefetch es interesante, pero aún más frágil (porque un desalojo temprano 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 para una máquina específica, vale la pena mirar el prefetch SW para el acceso secuencial, pero si puede seguir siendo una desaceleración si tiene suficiente trabajo de ALUMINIO para hacer mientras nos acercamos al embotellamiento de la memoria.


El tamaño de la línea de caché sigue siendo de 64 bytes. (L1D read / write bandwidth is very high, and modern CPUs can do 2 vector loads per clock + 1 vector store if it all hits in L1D. See How can cache be that fast?.) Con AVX512, line size = vector width, por lo que puede 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 línea de caché, en lugar de cada otro 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 cero penalización si la dirección está alineada en tiempo de ejecución, pero los compiladores (especialmente gcc) hacen mejor código cuando se autovectorizan si conocen alguna garantía de alineación. En realidad, las operaciones no alineadas son generalmente rápidas, pero las divisiones de página aún duelen (mucho menos en Skylake, sin embargo; solo ~11 ciclos adicionales de latencia vs.100, pero aún así una penalización de rendimiento).


Como Ulrich predicho, cada sistema multi-socket es NUMA en estos días: los controladores de memoria integrados son estándar, es decir, no hay un puente Norte 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 una gran caché L3 inclusiva como respaldo para la coherencia entre 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 etiquetas que le permite comprobar lo que está almacenado en caché en cualquier lugar del chip (y si es así, dónde) sin transmitir fisgones a todos los núcleos. SKX utiliza una malla en lugar de un bus de anillo , con una latencia generalmente aún 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 los detalles de exactamente lo que sucede cuando no puede evitar errores de caché o contención variar.


6.4.2 Atomic ops : el punto de referencia que muestra un bucle de reintento de CAS como 4 veces peor que el arbitraje por hardware lock add probablemente aún refleje un caso de contención máxima . Pero en programas multihilo reales, la sincronización se mantiene al mínimo (porque es costosa), por lo que la contención es baja y un bucle CAS-retry generalmente tiene éxito sin tener que volver a intentarlo.

C++11 std::atomic fetch_add compilará a un lock add (o lock xadd si se usa el valor devuelto), pero un algoritmo que utiliza CAS para hacer algo que no se puede hacer con una instrucción ed lockno suele ser un desastre. Use C++11 std::atomic o C11 stdatomic en lugar del legado del CCG __sync built-ins o el más reciente __atomic built-ins a menos que desee mezclar acceso atómico y no atómico a la misma ubicación...

8.1 DCAS (cmpxchg16b): Puedes persuadir a gcc para que lo emita, pero si quieres cargas eficientes de solo la mitad del objeto, necesitas feo unionhacks: ¿Cómo puedo implementar el contador ABA con CAS c++11?

8.2.4 memoria transaccional: Después de un par de arranques en falso (liberados luego desactivados por una actualización de microcódigo debido a un error rara vez activado), Intel tiene memoria transaccional en funcionamiento en el último modelo Broadwell y todas las CPU Skylake. El diseño sigue siendo lo que David Kanter describió para Haswell. Hay una forma de bloqueo-ellision de usarlo para acelerar el código que utiliza (y puede recurrir a) un lock (especialmente con un solo lock para todos los elementos de un contenedor por lo que múltiples subprocesos en la misma sección crítica a menudo no chocan), o para escribir código que sabe acerca de las transacciones directamente.


7.5 Hugepages : anonymous transparent hugepages funciona bien en Linux sin tener que usar manualmente hugetlbfs. Haga asignaciones > = 2MiB con alineación de 2MiB (p. ej. posix_memalign, o una aligned_alloc eso no hace cumplir el estúpido requisito de ISO C++17 de fallar cuando size % alignment != 0).

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


Apéndice B: Oprofile : Linux perf ha reemplazado en su mayoría a oprofile. Para eventos detallados específicos de ciertas microarquitecturas, use el wrapper ocperf.py . por ejemplo,

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 algunos ejemplos de su uso, ver ¿Puede el MOV de x86 realmente ser "libre"? ¿Por qué no puedo reproducir esto en absoluto?.

 41
Author: Peter Cordes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-25 15:15:30