Понимание выравнивания alloca() в GCC и, казалось бы, пропущенной оптимизации
Рассмотрим следующий игрушечный пример, который выделяет память в стеке с помощью alloca()
функция:
#include <alloca.h>
void foo() {
volatile int *p = alloca(4);
*p = 7;
}
Компиляция функции выше с помощью gcc 8.2 с -O3
приводит к следующему коду сборки:
foo:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
leaq 15(%rsp), %rax
andq $-16, %rax
movl $7, (%rax)
leave
ret
Честно говоря, я бы ожидал более компактный ассемблерный код.
16-байтовое выравнивание для выделенной памяти
Инструкция andq $-16, %rax
в приведенном выше коде приводит к rax
содержащий (только) 16-байтовый выровненный адрес между адресами rsp
а также rsp + 15
(оба включительно).
Это соблюдение выравнивания - первое, что я не понимаю: почему alloca()
выровнять выделенную память по 16-байтовой границе?
Возможна ли пропущенная оптимизация?
В любом случае давайте рассмотрим, что мы хотим, чтобы память выделялась alloca()
быть выровненным по 16 байтов. Тем не менее, в приведенном выше коде сборки, имея в виду, что GCC предполагает выравнивание стека по 16-байтовой границе в момент выполнения вызова функции (т.е. call foo
), если обратить внимание на состояние стека внутри foo()
только после нажатия rbp
регистр:
Size Stack RSP mod 16 Description
-----------------------------------------------------------------------------------
------------------
| . |
| . |
| . |
------------------........0 at "call foo" (stack 16-byte aligned)
8 bytes | return address |
------------------........8 at foo entry
8 bytes | saved RBP |
------------------........0 <----- RSP is 16-byte aligned!!!
Я думаю, что, используя в своих интересах красную зону (то есть, нет необходимости изменять rsp
) и тот факт, что rsp
уже содержит 16-байтовый выровненный адрес, вместо него можно использовать следующий код:
foo:
pushq %rbp
movq %rsp, %rbp
movl $7, -16(%rbp)
leave
ret
Адрес, содержащийся в реестре rbp
выровнен по 16 байтам, поэтому rbp - 16
также будет выровнен по 16-байтовой границе.
Более того, создание нового фрейма стека можно оптимизировать, так как rsp
не изменяется:
foo:
movl $7, -8(%rsp)
ret
Это просто пропущенная оптимизация или я что-то здесь упускаю?
2 ответа
Системный V ABI x86-64 требует, чтобы VLA (массивы переменной длины C99) были выровнены по 16 байтов, то же самое для автоматических / статических массивов, которые> = 16 байтов.
Похоже gcc лечит alloca
как VLA, и не в состоянии сделать постоянное распространение в alloca
это выполняется только один раз за вызов функции. (Или что он внутренне использует alloca
для VLA.)
Универсальный alloca
/ VLA не может использовать красную зону, если значение времени выполнения превышает 128 байтов. GCC также создает кадр стека с RBP вместо сохранения размера выделения и выполнения add rsp, rdx
потом.
Таким образом, asm выглядит точно так же, как если бы размер был функцией arg или другой переменной времени выполнения вместо константы. Вот что привело меня к такому выводу.
Также alignof(maxalign_t) == 16
, но alloca
а также malloc
может удовлетворить требование возврата памяти, используемой для любого объекта без 16-байтового выравнивания для объектов размером менее 16 байт. Ни один из стандартных типов не имеет требований к выравниванию, более широких, чем их размер в x86-64 SysV.
Вы правы, он должен быть в состоянии оптимизировать это так:
void foo() {
alignas(16) int dummy[1];
volatile int *p = dummy; // alloca(4)
*p = 7;
}
и скомпилировать его в movl $7, -8(%rsp)
; ret
ты предложил.
alignas(16)
может быть необязательным здесь для alloca.
Если вам действительно нужен gcc для выдачи лучшего кода, когда постоянное распространение делает аргумент alloca
константу времени компиляции, вы могли бы просто рассмотреть использование VLA. GNU C++ поддерживает VLA в стиле C99 в режиме C++, а ISO C++ (и MSVC) - нет.
Или возможно использовать if(__builtin_constant_p(size)) { VLA version } else { alloca version }
, но область действия VLA означает, что вы не можете вернуть VLA из области действия if
который обнаруживает, что мы встроены в константу времени компиляции size
, Таким образом, вам придется дублировать код, который нуждается в указателе.
Это (частично) пропущенная оптимизация в gcc. Clang делает это, как ожидалось.
Я сказал частично, потому что если вы знаете, что будете использовать gcc, вы можете использовать встроенные функции (используйте условную компиляцию для gcc и других компиляторов, чтобы иметь переносимый код).
__builtin_alloca_with_align
твой друг ;)
Вот пример (изменен, так что компилятор не будет сокращать вызов функции до single ret):
#include <alloca.h>
volatile int* p;
void foo()
{
p = alloca(4) ;
*p = 7;
}
void zoo()
{
// aligment is 16 bits, not bytes
p = __builtin_alloca_with_align(4,16) ;
*p = 7;
}
int main()
{
foo();
zoo();
}
Разобранный код (с objdump -d -w --insn-width=12 -M intel
)
Clang выдаст следующий код (clang -O3 test.c
) - обе функции похожи
0000000000400480 <foo>:
400480: 48 8d 44 24 f8 lea rax,[rsp-0x8]
400485: 48 89 05 a4 0b 20 00 mov QWORD PTR [rip+0x200ba4],rax # 601030 <p>
40048c: c7 44 24 f8 07 00 00 00 mov DWORD PTR [rsp-0x8],0x7
400494: c3 ret
00000000004004a0 <zoo>:
4004a0: 48 8d 44 24 fc lea rax,[rsp-0x4]
4004a5: 48 89 05 84 0b 20 00 mov QWORD PTR [rip+0x200b84],rax # 601030 <p>
4004ac: c7 44 24 fc 07 00 00 00 mov DWORD PTR [rsp-0x4],0x7
4004b4: c3 ret
GCC этот (gcc -g -O3 -fno-stack-protector
)
0000000000000620 <foo>:
620: 55 push rbp
621: 48 89 e5 mov rbp,rsp
624: 48 83 ec 20 sub rsp,0x20
628: 48 8d 44 24 0f lea rax,[rsp+0xf]
62d: 48 83 e0 f0 and rax,0xfffffffffffffff0
631: 48 89 05 e0 09 20 00 mov QWORD PTR [rip+0x2009e0],rax # 201018 <p>
638: c7 00 07 00 00 00 mov DWORD PTR [rax],0x7
63e: c9 leave
63f: c3 ret
0000000000000640 <zoo>:
640: 48 8d 44 24 fc lea rax,[rsp-0x4]
645: c7 44 24 fc 07 00 00 00 mov DWORD PTR [rsp-0x4],0x7
64d: 48 89 05 c4 09 20 00 mov QWORD PTR [rip+0x2009c4],rax # 201018 <p>
654: c3 ret
Как вы можете видеть, зоопарк теперь выглядит ожидаемым и похож на код лязга.