¿Qué tan importante es la alineación de la memoria? ¿Aún importa?

15

Desde hace algún tiempo, he buscado y leído mucho sobre la alineación de la memoria, cómo funciona y cómo usarla. El artículo más relevante que he encontrado por ahora es este .

Pero incluso con eso todavía tengo algunas preguntas al respecto:

  1. Fuera del sistema embebido, a menudo tenemos una gran cantidad de memoria en nuestra computadora que hace que la administración de la memoria sea mucho menos crítica, estoy completamente en la optimización, pero ahora, es realmente algo que puede marcar la diferencia si comparamos el mismo programa con o sin memoria reorganizada y alineada?
  2. ¿La alineación de la memoria tiene otras ventajas? Leí en alguna parte que la CPU funciona mejor / más rápido con la memoria alineada porque eso requiere menos instrucciones para procesar (si uno de ustedes tiene un enlace para un artículo / punto de referencia al respecto), en ese caso, ¿la diferencia es realmente significativa? ¿Hay más ventajas que estos dos?
  3. En el enlace del artículo, en el capítulo 5, el autor dice:

    Cuidado: en C ++, ¡las clases que parecen estructuras pueden romper esta regla! (Si lo hacen o no depende de cómo se implementen las clases base y las funciones virtuales de los miembros, y varía según el compilador).

  4. El artículo habla principalmente de estructuras, pero ¿la declaración de variables locales también se ve afectada por esta necesidad?

    ¿Tienes alguna idea de cómo funciona la alineación de la memoria exactamente en C ++ ya que parece tener algunas diferencias?

Esta pregunta anterior contiene la palabra "alineación", pero no proporciona ninguna respuesta a las preguntas anteriores.

Kane
fuente
Los compiladores de C ++ están más inclinados a hacer esto (insertar relleno donde sea necesario o beneficioso) para usted. Desde el enlace que mencionó, busque en la sección 12 "Herramientas" las cosas que puede usar.
rwong

Respuestas:

11

Sí, tanto la alineación como la disposición de sus datos pueden marcar una gran diferencia en el rendimiento, no solo de un pequeño porcentaje, sino de varios cientos a un porcentaje.

Tome este bucle, dos instrucciones importan si ejecuta suficientes bucles.

.globl ASMDELAY
ASMDELAY:
    subs r0,r0,#1
    bne ASMDELAY
    bx lr

Con y sin caché, y con alineación con y sin caché, arroje la predicción de rama y puede variar el rendimiento de esas dos instrucciones en una cantidad significativa (tics de temporizador):

min      max      difference
00016DDE 003E025D 003C947F

Una prueba de rendimiento que puedes hacer tú mismo muy fácilmente. agregue o elimine nops alrededor del código bajo prueba y haga un trabajo preciso de sincronización, mueva las instrucciones bajo prueba a lo largo de un rango lo suficientemente amplio de direcciones para tocar los bordes de las líneas de caché, etc.

El mismo tipo de cosas con los accesos a datos. Algunas arquitecturas se quejan de accesos no alineados (por ejemplo, realizando una lectura de 32 bits en la dirección 0x1001), dándole una falla de datos. A algunos de ellos se les puede desactivar la falla y recibir el golpe de rendimiento. Otros que permiten accesos no alineados solo obtienen el impacto en el rendimiento.

A veces son "instrucciones", pero la mayoría de las veces son ciclos de reloj / bus.

Mire las implementaciones de memcpy en gcc para varios objetivos. Supongamos que está copiando una estructura que tiene 0x43 bytes, puede encontrar una implementación que copia un byte dejando 0x42, luego copia 0x40 bytes en grandes bloques eficientes y luego el último 0x2 puede hacerlo como dos bytes individuales o como una transferencia de 16 bits. La alineación y el objetivo entran en juego si las direcciones de origen y de destino están en la misma alineación, digamos 0x1003 y 0x2003, entonces podría hacer un byte, luego 0x40 en fragmentos grandes, luego 0x2, pero si uno es 0x1002 y el otro 0x1003, entonces se obtiene muy feo y muy lento.

La mayoría de las veces son ciclos de autobuses. O peor aún, el número de transferencias. Tome un procesador con un bus de datos de 64 bits de ancho, como ARM, y realice una transferencia de cuatro palabras (lectura o escritura, LDM o STM) en la dirección 0x1004, es una dirección alineada con palabras, y perfectamente legal, pero si el bus es 64 bits de ancho es probable que la instrucción individual se convierta en tres transferencias en este caso, 32 bits a 0x1004, 64 bits a 0x1008 y 32 bits a 0x100A. Pero si tuviera la misma instrucción pero en la dirección 0x1008, podría hacer una sola transferencia de cuatro palabras en la dirección 0x1008. Cada transferencia tiene un tiempo de configuración asociado. Por lo tanto, la diferencia de direcciones de 0x1004 a 0x1008 por sí misma puede ser varias veces más rápida, incluso / esp cuando se usa un caché y todos son aciertos de caché.

Hablando de eso, incluso si lee dos palabras en la dirección 0x1000 frente a 0x0FFC, el 0x0FFC con errores de caché causará dos lecturas de línea de caché donde 0x1000 es una línea de caché, tiene la penalidad de una línea de caché leída de todos modos para un azar acceso (leer más datos que usar) pero luego eso se duplica. La forma en que se alinean sus estructuras o sus datos en general y su frecuencia de acceso a esos datos, etc., pueden causar la pérdida de memoria caché.

Puede terminar eliminando sus datos de manera tal que a medida que procesa los datos puede crear desalojos, podría ser realmente desafortunado y terminar usando solo una fracción de su caché y, a medida que salta, el siguiente bloque de datos colisiona con un blob anterior . Al mezclar sus datos o reorganizar las funciones en el código fuente, etc., puede crear o eliminar colisiones, ya que no todas las memorias caché se crean de la misma manera, el compilador no lo ayudará aquí. Incluso detectar el impacto o la mejora del rendimiento depende de usted.

Todas las cosas que hemos agregado para mejorar el rendimiento, buses de datos más amplios, tuberías, cachés, predicción de ramales, múltiples unidades / rutas de ejecución, etc. A menudo ayudarán, pero todos tienen puntos débiles, que pueden explotarse intencionalmente o accidentalmente. Es muy poco lo que el compilador o las bibliotecas pueden hacer al respecto, si está interesado en el rendimiento necesita ajustar y uno de los factores de ajuste más importantes es la alineación del código y los datos, no solo alineados en 32, 64, 128, 256 límites de bits, pero también donde las cosas son relativas entre sí, desea bucles muy utilizados o datos reutilizados para no aterrizar en la misma forma de caché, cada uno quiere el suyo. Los compiladores pueden ayudar, por ejemplo, al ordenar instrucciones para una arquitectura súper escalar, reorganizando instrucciones que no importan entre sí,

El mayor descuido es la suposición de que el procesador es el cuello de botella. No ha sido así durante una década o más, alimentar el procesador es el problema y es allí donde entran en juego problemas como el rendimiento de la alineación, el almacenamiento en caché, etc. Con un poco de trabajo incluso en el nivel del código fuente, reorganizar los datos en una estructura, ordenar las declaraciones de variables / estructuras, ordenar las funciones dentro del código fuente y un poco de código adicional para alinear los datos, puede mejorar el rendimiento varias veces o más.

viejo contador de tiempo
fuente
+1 solo para tu párrafo final. El ancho de banda de la memoria es el problema más crítico para cualquiera que intente escribir código rápido hoy, no el recuento de instrucciones. Y esto significa que es muy importante optimizar las cosas para reducir los errores de caché, lo que se puede hacer modificando la alineación en muchas circunstancias.
Jules
Si su código y sus datos se almacenan en caché y realiza suficientes bucles / ciclos en esos datos, entonces las instrucciones cuentan y dónde se encuentran las instrucciones dentro de una línea de recuperación, donde las ramas aterrizan dentro de la tubería en relación con lo que dependen, importa. Pero en los sistemas basados ​​en dram y / o flash primero debe preocuparse por alimentar el procesador sí.
old_timer
15

Sí, la alineación de la memoria todavía importa.

Algunos procesadores en realidad no pueden realizar lecturas en direcciones no alineadas. Si está ejecutando en dicho hardware y almacena sus enteros no alineados, es probable que tenga que leerlos con dos instrucciones seguidas de algunas instrucciones más para obtener los diversos bytes en los lugares correctos para que pueda usarlo . Por lo tanto, los datos alineados son críticos para el rendimiento.

La buena noticia es que en su mayoría no tiene que preocuparse. Casi cualquier compilador para casi cualquier idioma producirá código de máquina que respete los requisitos de alineación del sistema de destino. Solo necesita comenzar a pensar en ello si está tomando el control directo de la representación en memoria de sus datos, lo cual no es necesario en ningún lugar tan a menudo como antes. Es algo interesante de saber, y absolutamente crítico saber si desea comprender el uso de la memoria de varias estructuras que está creando, y cómo tal vez reorganizar las cosas para que sean más eficientes (evitando el relleno). Pero a menos que necesite ese tipo de control (y para la mayoría de los sistemas que simplemente no necesita), puede pasar felizmente una carrera completa sin saberlo ni preocuparse por él.

Matthew Walton
fuente
1
En particular, ARM no admite acceso no alineado. Y esa es la CPU que casi todo lo que usa el móvil.
Jan Hudec
También tenga en cuenta que Linux emula el acceso no alineado a un costo de tiempo de ejecución, pero Windows (CE y Phone) no lo hacen y el intento de acceso no alineado simplemente bloqueará la aplicación.
Jan Hudec
2
Si bien esto es principalmente cierto, tenga en cuenta que algunas plataformas (incluida x86) tienen diferentes requisitos de alineación dependiendo de las instrucciones que se van a utilizar , lo que no es fácil para el compilador, por lo que a veces es necesario rellenar para asegurarse ciertas operaciones (por ejemplo, las instrucciones SSE, muchas de las cuales requieren una alineación de 16 bytes) se pueden usar para algunas operaciones. Además, agregar un relleno adicional para que dos elementos que se usan juntos con frecuencia ocurran en la misma línea de caché (también 16 bytes) puede tener un gran efecto en el rendimiento en algunos casos, y tampoco está automatizado.
Jules
3

Sí, todavía importa, y en algunos algoritmos críticos de rendimiento, no puede confiar en el compilador.

Voy a enumerar solo algunos ejemplos:

  1. De esta respuesta :

Normalmente, el microcódigo obtendrá la cantidad adecuada de 4 bytes de la memoria, pero si no está alineado, tendrá que recuperar dos ubicaciones de 4 bytes de la memoria y reconstruir la cantidad deseada de 4 bytes de los bytes apropiados de las dos ubicaciones.

  1. El conjunto de instrucciones SSE requiere una alineación especial. Si no se cumple, debe usar funciones especiales para cargar y almacenar datos en la memoria no alineada. Eso significa dos instrucciones adicionales.

Si no está trabajando en algoritmos críticos de rendimiento, simplemente olvídese de las alineaciones de memoria. Realmente no es necesario para la programación normal.

BЈовић
fuente
1

Tendemos a evitar situaciones en las que importa. Si importa, importa. Los datos no alineados solían ocurrir, por ejemplo, al procesar datos binarios, lo que parece evitarse hoy en día (las personas usan mucho XML o JSON).

SI de alguna manera logra crear una matriz de enteros no alineados, entonces, en un procesador Intel típico, su código procesará esa matriz un poco más lento que para los datos alineados. En un procesador ARM, funciona un poco más lento si le dice al compilador que los datos no están alineados. Puede ejecutarse mucho, mucho más lento o dar resultados incorrectos, según el modelo de procesador y el sistema operativo, si utiliza datos no alineados sin avisar al compilador.

Explicando la referencia a C ++: en C, todos los campos en una estructura deben almacenarse en orden de memoria ascendente. Entonces, si tiene campos char / double / char y desea tener todo alineado, tendrá un byte char, siete byte sin usar, ocho byte doble, un byte char, siete byte sin usar. En estructuras C ++ es lo mismo por compatibilidad. Pero para las estructuras, el compilador puede reordenar los campos, por lo que puede tener un byte char, otro byte char, seis byte sin usar, 8 byte doble. Usando 16 en lugar de 24 bytes. En estructuras C, los desarrolladores generalmente evitarían esa situación y tendrían los campos en un orden diferente en primer lugar.

gnasher729
fuente
1
Los datos no alineados ocurren en la memoria. Los programas que no tienen estructuras de datos correctamente empaquetadas pueden sufrir penalizaciones masivas de rendimiento incluso por un ordenamiento de valores aparentemente intrascendente. En el código de subprocesos, por ejemplo, dos valores en una sola línea de caché provocarán bloqueos masivos de canalización cuando dos subprocesos accedan a ellos al mismo tiempo (ignorando los problemas de seguridad de subprocesos, por supuesto).
greyfade
Un compilador de C ++ puede reordenar campos solo bajo ciertas condiciones, que probablemente no se cumplan si no conoce esas reglas. Además de eso, no conozco ningún compilador de C ++ que realmente use esta libertad.
Sjoerd
1
Nunca he visto un compilador de C reordena los campos. Sin embargo, he visto muchos rellenos de inserción y alineación entre caracteres /
ints
1

Muchos puntos buenos ya se mencionan en las respuestas anteriores. Solo para agregar incluso en sistemas no integrados que se ocupan de la búsqueda / extracción de datos, el rendimiento de los asuntos de memoria y los tiempos de acceso son tan importantes que, aparte del código de ensamblaje de alineación, se escribe para el mismo.

También recomiendo una lectura que valga la pena: http://dewaele.org/~robbe/thesis/writing/references/what-every-programmer-should-know-about-memory.2007.pdf

Varun Mishra
fuente
1

¿Qué tan importante es la alineación de la memoria? ¿Aún importa?

Si. No. Depende.

Fuera del sistema embebido, a menudo tenemos una gran cantidad de memoria en nuestra computadora que hace que la administración de la memoria sea mucho menos crítica, estoy completamente en la optimización, pero ahora, es realmente algo que puede marcar la diferencia si comparamos el mismo programa con o sin memoria reorganizada y alineada?

Su aplicación tendrá una huella de memoria más pequeña y funcionará más rápido si está correctamente alineada. En la aplicación de escritorio típica, no importará fuera de casos raros / atípicos (como su aplicación que siempre termina con el mismo cuello de botella de rendimiento y requiere optimizaciones). Es decir, la aplicación será más pequeña y más rápida si está correctamente alineada, pero en la mayoría de los casos prácticos no debería afectar al usuario de una forma u otra.

¿La alineación de la memoria tiene otras ventajas? Leí en alguna parte que la CPU funciona mejor / más rápido con la memoria alineada porque eso requiere menos instrucciones para procesar (si uno de ustedes tiene un enlace para un artículo / punto de referencia al respecto), en ese caso, ¿la diferencia es realmente significativa? ¿Hay más ventajas que estos dos?

Puede ser. Es algo a tener en cuenta (posiblemente) al escribir código, pero en la mayoría de los casos simplemente no debería importar (es decir, todavía organizo mis variables miembro por huella de memoria y frecuencia de acceso, lo que debería facilitar el almacenamiento en caché), pero lo hago para facilidad de uso / lectura y refactorización del código, no para fines de almacenamiento en caché).

¿Tienes alguna idea de cómo funciona la alineación de la memoria exactamente en C ++ ya que parece tener algunas diferencias?

Leí sobre eso cuando salió la alineación de cosas (¿C ++ 11?) No me molesté desde entonces (estoy haciendo principalmente aplicaciones de escritorio y desarrollo de servidores back-end en estos días).

utnapistim
fuente