了解 GCC 的 alloca() 对齐和看似错过的优化

Posted

技术标签:

【中文标题】了解 GCC 的 alloca() 对齐和看似错过的优化【英文标题】:Understanding GCC's alloca() alignment and seemingly missed optimization 【发布时间】:2019-03-02 16:55:02 【问题描述】:

考虑以下通过alloca() 函数在堆栈上分配内存的玩具示例:

#include <alloca.h>

void foo() 
    volatile int *p = alloca(4);
    *p = 7;

使用带有-O3 的gcc 8.2 编译上述函数会产生以下汇编代码:

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 包含地址rsprsp + 15(包括两者)之间的(仅)16 字节对齐 地址。

这种对齐强制是我不明白的第一件事:为什么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!!!

我认为通过利用 red zone(即,无需修改 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

这只是一个错过的优化还是我在这里遗漏了其他东西?

【问题讨论】:

在 macOS 上运行? macOS ABI 需要 16 字节堆栈对齐... @Macmade:该要求适用于call 之前。不要求函数始终保持 RSP 16 字节对齐。如果 gcc 必须为任何东西调整 RSP,它将使其 16 字节对齐,但如果它可以只为本地人使用红色区域,它将保持 RSP 不变(除了可能的推送/弹出)。 【参考方案1】:

x86-64 System V ABI 要求 VLA(C99 可变长度数组)以 16 字节对齐,对于 >= 16 字节的自动/静态数组也是如此。

看起来 gcc 将 alloca 视为 VLA,并且未能将常量传播到每个函数调用仅运行一次的 alloca。 (或者它在内部使用 alloca 作为 VLA。)

通用alloca / VLA 不能使用红色区域,以防运行时值大于 128 字节。 GCC 还使用 RBP 生成堆栈帧,而不是保存分配大小并稍后执行add rsp, rdx

因此,如果 size 是函数 arg 或其他运行时变量而不是常量,则 asm 看起来完全一样。这就是导致我得出这个结论的原因。


还有 alignof(maxalign_t) == 16 ,但 allocamalloc 可以满足为小于 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 成为编译时常量时发出更好的代码,您可以考虑简单地使用第一名。 GNU C++ 在 C++ 模式下支持 C99 风格的 VLA,但 ISO C++(和 MSVC)不支持。

或者可能使用if(__builtin_constant_p(size)) VLA version else alloca version ,但是VLA 的范围意味着您不能从检测到我们正在使用编译时常量size 内联的if 范围内返回VLA。所以你必须复制需要指针的代码。

【讨论】:

【参考方案2】:

这是(部分)在 gcc 中错过的优化。 Clang 按预期进行。

我说的部分原因是如果你知道你将使用 gcc,你可以使用内置函数(对 gcc 和其他编译器使用条件编译以获得可移植代码)。

__builtin_alloca_with_align是你的朋友;)

这是一个示例(更改后编译器不会将函数调用减少到单个 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    

如您所见,zoo 现在看起来像预期的那样,并且类似于 clang 代码。

【讨论】:

以上是关于了解 GCC 的 alloca() 对齐和看似错过的优化的主要内容,如果未能解决你的问题,请参考以下文章

禁用优化的 c alloca 函数的奇怪汇编代码 - gcc 使用 DIV 和 IMUL 为常数 16,并转换?

将 alloca() 用于可变长度数组是不是比在堆上使用向量更好?

试图了解 x86 上 alloca() 函数的汇编实现

gcc数据对齐之: howto 2.

GCC - 如何重新对齐堆栈?

llvm 代码中出现的 %"alloca point" 行的目的是啥?