Понимание выравнивания alloca () в GCC и, казалось бы, пропущенной оптимизации - PullRequest
0 голосов
/ 26 сентября 2018

Рассмотрим следующий игрушечный пример, который выделяет память в стеке с помощью функции 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 ]

0 голосов
/ 27 сентября 2018

Это (частично) пропущенная оптимизация в gcc.Clang делает это, как и ожидалось.

Я сказал частично, потому что, если вы знаете, что будете использовать gcc, вы можете использовать встроенные функции (используйте условную компиляцию для gcc и других компиляторов, чтобы иметь переносимый код).

__builtin_alloca_with_align ваш друг ;)

Вот пример (изменен, так что компилятор не будет сокращать вызов функции до одиночного повтора):

#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    

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

0 голосов
/ 26 сентября 2018

ABI System V x86-64 требует, чтобы VLA (массивы переменной длины C99) были выровнены по 16 байтов, то же самое для автоматических / статических массивов, которые> = 16 байтов.

Похоже, лечит gccalloca как VLA, и не в состоянии выполнить постоянное распространение в alloca, который запускается только один раз за вызов функции.(Или для внутреннего использования alloca для VLA.)

Универсальный alloca / VLA не может использовать красную зону, если значение времени выполнения превышает 128 байтов.GCC также создает кадр стека с RBP вместо сохранения размера выделения и выполнения add rsp, rdx позже.

Таким образом, асм выглядит точно так же, как если бы размер был функцией 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 для выдачи лучшего кода при постоянном распространенииделает arg равным alloca константой времени компиляции, вы можете просто рассмотреть , используя VLA.GNU C ++ поддерживает VLA в стиле C99 в режиме C ++, но ISO C ++ (и MSVC) этого не делает.

Или, возможно, использует if(__builtin_constant_p(size)) { VLA version } else { alloca version }, но область действия VLA означает, что вы не можете вернуть VLA из области видимости.if, который обнаруживает, что мы встроены в константу времени компиляции size.Поэтому вам нужно будет продублировать код, для которого нужен указатель.

...