【问题标题】:Understanding GCC's alloca() alignment and seemingly missed optimization了解 GCC 的 alloca() 对齐和看似错过的优化
【发布时间】: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 不变(除了可能的推送/弹出)。

标签: gcc assembly optimization x86-64 alloca


【解决方案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 代码。

    【讨论】:

      猜你喜欢
      • 2019-02-18
      • 2023-04-01
      • 2020-04-14
      • 2022-01-15
      • 2013-10-13
      • 1970-01-01
      • 1970-01-01
      • 2019-11-09
      • 2022-01-22
      相关资源
      最近更新 更多