dos cuestiones:
- si y cuándo mantener ciertos campos juntos es una optimización.
- Cómo hacerlo realmente.
La razón por la que podría ayudar, es que la memoria se carga en la memoria caché de la CPU en trozos llamados "líneas de caché". Esto lleva tiempo y, en general, cuanto más líneas de caché se carguen para su objeto, más tiempo tardará. Además, cuantas más cosas se eliminan del caché para hacer espacio, lo que ralentiza otro código de una manera impredecible.
El tamaño de una línea de caché depende del procesador. Si es grande en comparación con el tamaño de sus objetos, muy pocos objetos van a abarcar un límite de línea de caché, por lo que toda la optimización es bastante irrelevante. De lo contrario, puede salirse con la suya con solo tener parte de su objeto en caché, y el resto en la memoria principal (o caché L2, tal vez).Es bueno que las operaciones más comunes (las que acceden a los campos que se usan comúnmente) usen la menor cantidad posible de caché para el objeto, por lo que agrupar esos campos juntos te brinda una mejor oportunidad de que esto ocurra.
El principio general se llama "localidad de referencia". Cuanto más cerca estén las diferentes direcciones de memoria de los accesos de su programa, mayores serán sus posibilidades de obtener un buen comportamiento de caché. A menudo es difícil predecir el rendimiento por adelantado: diferentes modelos de procesadores de la misma arquitectura pueden comportarse de manera diferente, multi-threading significa que a menudo no se sabe qué va a estar en el caché, etc. Pero es posible hablar de lo que es probable pasar, la mayor parte del tiempo. Si quiere saber cualquier cosa, generalmente debe medirlo.
Tenga en cuenta que hay algunos problemas aquí. Si está utilizando operaciones atómicas basadas en CPU (que los tipos atómicos en C++ 0x generalmente lo harán), entonces es posible que la CPU bloquee toda la línea de caché para bloquear el campo. Entonces, si tiene varios campos atómicos juntos, con diferentes hilos ejecutándose en diferentes núcleos y operando en diferentes campos al mismo tiempo, encontrará que todas esas operaciones atómicas están serializadas porque todas bloquean la misma ubicación de memoria a pesar de que ' re operando en diferentes campos. Si hubieran estado operando en diferentes líneas de caché, entonces habrían trabajado en paralelo y funcionarían más rápido. De hecho, como señala Glen (a través de Herb Sutter) en su respuesta, en una arquitectura de caché coherente, esto sucede incluso sin operaciones atómicas, y puede arruinar por completo tu día. Por lo tanto, la localidad de referencia no es necesariamente. Lo bueno es que están involucrados varios núcleos, incluso si comparten el caché. Puede esperar que lo sea, debido a que las fallas de caché generalmente son una fuente de velocidad perdida, pero se equivocan terriblemente en su caso particular.
Ahora, aparte de distinguir entre campos usados comúnmente y menos usados, cuanto más pequeño es un objeto, menos memoria (y, por lo tanto, menos memoria caché) ocupa. Esta es una buena noticia, al menos en donde no tienes una gran controversia. El tamaño de un objeto depende de los campos en él y de cualquier relleno que deba insertarse entre los campos para garantizar que estén alineados correctamente para la arquitectura. C++ (a veces) impone restricciones al orden, qué campos deben aparecer en un objeto, según el orden en que se declaran. Esto es para facilitar la programación de bajo nivel. Por lo tanto, si el objeto contiene:
- un int (4 bytes, 4 Alineados)
- seguido por un char (1 byte, cualquier alineamiento)
- seguido de un int (4 bytes, 4- alineados)
- seguido por un char (1 byte, cualquier alineamiento)
entonces es probable que esto ocupará 16 bytes en la memoria. El tamaño y la alineación de int no es lo mismo en todas las plataformas, por cierto, pero 4 es muy común y esto es solo un ejemplo.
En este caso, el compilador insertará 3 bytes de relleno antes del segundo int, para alinearlo correctamente, y 3 bytes de relleno al final. El tamaño de un objeto tiene que ser un múltiplo de su alineación, de modo que los objetos del mismo tipo puedan colocarse adyacentes en la memoria. Eso es todo una matriz en C/C++, objetos adyacentes en la memoria. Si la estructura hubiera sido int, int, char, char, entonces el mismo objeto podría tener 12 bytes, porque char no tiene requisito de alineación.
Dije que si int se alinea en 4 depende de la plataforma: en ARM absolutamente tiene que ser así, dado que el acceso no alineado arroja una excepción de hardware. En x86 puede acceder a ints sin alinear, pero generalmente es más lento y IIRC no atómico. Entonces los compiladores usualmente (¿siempre?) 4-alinean las entradas en x86.
La regla de oro al escribir código, si le interesa el empaque, es observar el requisito de alineación de cada miembro de la estructura. A continuación, ordene los campos con los tipos más alineados primero, luego el siguiente más pequeño, y así sucesivamente hasta los miembros sin requisitos de alineación. Por ejemplo, si yo estoy tratando de escribir código portable que podría llegar a esto:
struct some_stuff {
double d; // I expect double is 64bit IEEE, it might not be
uint64_t l; // 8 bytes, could be 8-aligned or 4-aligned, I don't know
uint32_t i; // 4 bytes, usually 4-aligned
int32_t j; // same
short s; // usually 2 bytes, could be 2-aligned or unaligned, I don't know
char c[4]; // array 4 chars, 4 bytes big but "never" needs 4-alignment
char d; // 1 byte, any alignment
};
Si usted no sabe la alineación de un campo, o se está escribiendo código portable, pero quiere hacer lo mejor puede hacerlo sin grandes trucos, entonces supone que el requisito de alineación es el requisito más grande de cualquier tipo fundamental en la estructura, y que el requisito de alineación de los tipos fundamentales es su tamaño. Por lo tanto, si su estructura contiene uint64_t, o una longitud larga, entonces la mejor suposición es que está alineada en 8. Algunas veces estarás equivocado, pero estarás en lo cierto la mayor parte del tiempo.
Tenga en cuenta que los programadores de juegos como su blogger a menudo saben todo sobre su procesador y hardware, y por lo tanto no tienen que adivinar. Conocen el tamaño de la línea de caché, conocen el tamaño y la alineación de cada tipo, y conocen las reglas de disposición de estructuras utilizadas por su compilador (para tipos POD y no POD). Si son compatibles con múltiples plataformas, entonces pueden tener un caso especial para cada uno si es necesario. También pasan mucho tiempo pensando en qué objetos de su juego se beneficiarán de las mejoras en el rendimiento, y utilizando los perfiles para descubrir dónde están los cuellos de botella reales. Pero aún así, no es una mala idea tener algunas reglas generales que apliques, ya sea que el objeto lo necesite o no. Siempre que no haga que el código no esté claro, "poner campos comúnmente usados al comienzo del objeto" y "ordenar según el requisito de alineación" son dos buenas reglas.
Muy bien, vamos, ¿¡todos ustedes son un montón de sistemas incrustados, chicos aintcha !? –
Literalmente no tengo experiencia con sistemas integrados. Tan completamente que no soy 100%, sé lo que significa. Lo buscaré, pero no lo sé en este momento. – DevinB