Странная сборка из массива 0-инициализация

Вдохновленный вопросом Разница в инициализации и обнулении массива в c/С++?, я решил фактически изучить сборку, в моем случае, оптимизированной версии для Windows Mobile Professional (процессор ARM, из компилятора Microsoft Optimizing). То, что я нашел, было несколько неожиданным, и мне интересно, может ли кто-нибудь пролить свет на мои вопросы, касающиеся этого.

Эти два примера рассматриваются:

byte a[10] = { 0 };

byte b[10];
memset(b, 0, sizeof(b));

Они используются в одной и той же функции, поэтому стек выглядит следующим образом:

[ ] // 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)

Сгенерированная сборка с моими комментариями:

; 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

Теперь меня смущают две вещи:

  • Какая точка строк 02 и 05? Почему бы просто не дать & a [0] и 10 байтов для memset?
  • Почему нет байтов заполнения 0-инициализированных? Это только для заполнения в структурах?

Изменить: мне было слишком любопытно, чтобы не проверить конструктивный случай:

struct Padded
{
    DWORD x;
    byte y;
};

Ассемблер для 0-инициализации:

; 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]

Здесь мы видим в строке 04, что заполнение действительно происходит, так как используется str (в отличие от strb). Правильно?

Ответ 1

Причина строк 2 и 5 заключается в том, что вы указали 0 в инициализаторе массива. Компилятор будет инициализировать все константы, а затем выложить остальные, используя memset. Если вы должны были положить два нули в ваш инициализатор, вы увидите его strw (вместо слова by byte), а затем memset 8 байтов.

Что касается заполнения, он используется только для выравнивания доступа к памяти - данные не должны использоваться при обычных обстоятельствах, поэтому memsetting это расточительно.

Изменить: для записи я могу ошибаться в предположении strw выше. 99% моего опыта ARM - это обратный код, созданный GCC/LLVM на iPhone, поэтому мое предположение не переносится на MSVC.

Ответ 2

Оба бита кода без ошибок. Две упомянутые строки не являются умными, но вы просто доказываете, что этот компилятор испускает субоптимальный код.

Базы для заполнения обычно инициализируются, если это упрощает сборку или ускоряет работу кода. Например, если у вас есть отступы между двумя заполненными нулями элементами, часто бывает проще нуль-заполнить дополнение. Кроме того, если у вас есть дополнение в конце, и ваш memset() оптимизирован для многобайтовых записей, может быть и быстрее перезаписать это дополнение.

Ответ 3

Некоторые быстрые тесты показывают, что компилятор Microsoft x86 генерирует разные сборки, если список инициализаторов пуст, по сравнению с тем, когда он содержит нуль. Возможно, их компилятор ARM тоже. Что произойдет, если вы это сделаете?

byte a[10] = { };

Вот список сборки, который я получил (с параметрами /EHsc /FAs /O2 на Visual Studio 2008). Обратите внимание, что включение нуля в список инициализаторов заставляет компилятор использовать неприглаженные обращения к памяти для инициализации массива, в то время как версия списка инициализаторов и версия memset() используют доступ к выровненной памяти:

; 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