c++ - matrices - Extraño ensamblaje de la matriz 0-inicialización
matrices en c++ (3)
Algunas pruebas rápidas indican que el compilador x86 de Microsoft genera un ensamblaje diferente si la lista de inicializadores está vacía, en comparación con cuando contiene un cero. Quizás su compilador ARM también lo haga. ¿Qué pasa si haces esto?
byte a[10] = { };
Aquí está el listado de ensambles que obtuve (con opciones /EHsc /FAs /O2
en Visual Studio 2008). Tenga en cuenta que incluir un cero en la lista de inicializadores hace que el compilador use accesos de memoria no alineados para inicializar la matriz, mientras que la versión vacía de la lista de inicializadores y la versión memset()
usan accesos de memoria alineados:
; unsigned char a[10] = { };
xor eax, eax
mov DWORD PTR _a$[esp+40], eax
mov DWORD PTR _a$[esp+44], eax
mov WORD PTR _a$[esp+48], ax
; unsigned char b[10] = { 0 };
mov BYTE PTR _b$[esp+40], al
mov DWORD PTR _b$[esp+41], eax
mov DWORD PTR _b$[esp+45], eax
mov BYTE PTR _b$[esp+49], al
; unsigned char c[10];
; memset(c, 0, sizeof(c));
mov DWORD PTR _c$[esp+40], eax
mov DWORD PTR _c$[esp+44], eax
mov WORD PTR _c$[esp+48], ax
Inspirado por la pregunta ¿ Diferencia en initalizar y poner a cero una matriz en c / c ++? , Decidí examinar realmente el ensamblaje de, en mi caso, una compilación optimizada para Windows Mobile Professional (procesador ARM, del compilador de optimización de Microsoft). Lo que encontré fue algo sorprendente, y me pregunto si alguien puede arrojar algo de luz sobre mis preguntas al respecto.
Estos dos ejemplos son examinados:
byte a[10] = { 0 };
byte b[10];
memset(b, 0, sizeof(b));
Se usan en la misma función, por lo que la pila se ve así:
[ ] // padding byte to reach DWORD boundary
[ ] // padding byte to reach DWORD boundary
[ ] // b[9] (last element of b)
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ] // b[0] = sp + 12 (stack pointer + 12 bytes)
[ ] // padding byte to reach DWORD boundary
[ ] // padding byte to reach DWORD boundary
[ ] // a[9] (last element of a)
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ] // a[0] = sp (stack pointer, at bottom)
El conjunto generado con mis comentarios:
; byte a[10] = { 0 };
01: mov r3, #0 // r3 = 0
02: mov r2, #9 // 3rd arg to memset: 9 bytes, note that sizeof(a) = 10
03: mov r1, #0 // 2nd arg to memset: 0-initializer
04: add r0, sp, #1 // 1st arg to memset: &a[1] = a + 1, since only 9 bytes will be set
05: strb r3, [sp] // a[0] = r3 = 0, sets the first element of a
06: bl memset // continue in memset
; byte b[10];
; memset(b, 0, sizeof(b));
07: mov r2, #0xA // 3rd arg to memset: 10 bytes, sizeof(b)
08: mov r1, #0 // 2nd arg to memset: 0-initializer
09: add r0, sp, #0xC // 1st arg to memset: sp + 12 bytes (the 10 elements
// of a + 2 padding bytes for alignment) = &b[0]
10: bl memset // continue in memset
Ahora, hay dos cosas que me confunden:
- ¿Cuál es el punto de las líneas 02 y 05? ¿Por qué no simplemente dar & a [0] y 10 bytes a memset?
- ¿Por qué los bytes de relleno de 0 no se inicializan? ¿Eso es solo para rellenar las estructuras?
Editar: Tenía mucha curiosidad por no probar el caso struct:
struct Padded
{
DWORD x;
byte y;
};
El ensamblador para 0-inicializándolo:
; Padded p1 = { 0 };
01: mov r3, #0
02: str r3, [sp]
03: mov r3, #0
04: str r3, [sp, #4]
; Padded p2;
; memset(&p2, 0, sizeof(p2));
05: mov r3, #0
06: str r3, [sp]
07: andcs r4, r0, #0xFF
08: str r3, [sp, #4]
Aquí vemos en la línea 04 que efectivamente se produce un relleno, ya que se usa str
(en lugar de strb
). ¿Derecha?
Ambos bits de código no tienen errores. Las dos líneas mencionadas no son inteligentes, pero estás probando que este compilador está emitiendo un código que no es óptimo.
Los bytes de relleno generalmente solo se inicializan si eso simplifica el ensamblaje o acelera el código. Por ejemplo, si tiene relleno entre dos miembros llenos a cero, a menudo es más fácil completar el relleno a cero también. Además, si tiene relleno al final y su memset () está optimizado para escrituras de varios bytes, puede ser más rápido sobrescribir ese relleno también.
La razón para las líneas 2 y 5 es porque ha especificado un 0 en el inicializador de la matriz. El compilador inicializará todas las constantes y luego rellenará el resto utilizando memset. Si tuviera que poner dos ceros en su inicializador, lo vería strw (palabra en lugar de byte) y luego memset 8 bytes.
En cuanto al relleno, solo se usa para alinear los accesos a la memoria; los datos no se deben usar en circunstancias normales, por lo que configurarlos es un desperdicio.
Editar: Para el registro, puedo estar equivocado sobre la suposición strw anterior. El 99% de mi experiencia ARM es revertir el código generado por GCC / LLVM en el iPhone, por lo que mi suposición puede no trasladarse a MSVC.