Понимание выравнивания 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    

Как вы можете видеть, зоопарк теперь выглядит ожидаемым и похож на код лязга.

Другие вопросы по тегам