编译器如何为 C++ 中条件声明的自动变量分配内存?

Twi*_*ard 12 c++ memory stack-frame

假设我有一个函数,根据某些运行时条件创建昂贵的自动对象或创建便宜的自动对象:

void foo() {
   if (runtimeCondition) {
       int x = 0;
   } else {
       SuperLargeObject y;
   }
}
Run Code Online (Sandbox Code Playgroud)

当编译器为此函数的堆栈帧分配内存时,它是否会分配足够的内存来存储SuperLargeObject,并且如果导致 的条件为int真,则额外的内存将不会被使用?或者它会以其他方式分配内存吗?

chr*_*nte 12

这取决于您的编译器和优化设置。在未优化的构建中,大多数 C++ 编译器可能会为两个对象分配堆栈内存,并根据采用的分支使用其中之一。在优化的构建中,事情变得更加有趣:

如果两个对象(theint和 the 都SuperLargeObject没有被使用,并且编译器可以证明构造SuperLargeObject没有副作用,则两个分配都将被省略。

如果对象逃逸该函数,即它们的地址被传递给另一个函数,则编译器必须为它们提供内存。但由于它们的生命周期不重叠,因此它们可以存储在重叠的内存区域中。这是否真的发生取决于编译器。

正如您在这里所看到的,不同的编译器为这两个函数生成不同的程序集:(来自OP和参考的修改示例,全部针对x86-64编译)

void escape(void const*);

struct SuperLargeObject {
    char data[104];
};

void f(bool cond) {
    if (cond) {
        int x;
        escape(&x);
    }
    else {
        SuperLargeObject y;
        escape(&y);
    }
}

void g() {
    SuperLargeObject y;
    escape(&y);
}
Run Code Online (Sandbox Code Playgroud)

请注意,所有堆栈分配都是 8 的奇数倍,因为 x86-64 ABI 要求堆栈指针以 16 字节对齐,并且call返回地址的指令将推送 8 个字节(感谢 @PeterCordes 在 上向我解释了这一点)另一篇文章)。

国际商会

f(bool):
        sub       rsp, 120
        test      dil, dil
        lea       rax, QWORD PTR [104+rsp]
        lea       rdx, QWORD PTR [rsp]
        cmovne    rdx, rax
        mov       rdi, rdx
        call      escape(void const*)
        add       rsp, 120
        ret
g():
        sub       rsp, 104
        lea       rdi, QWORD PTR [rsp]
        call      escape(void const*)
        add       rsp, 104
        ret
Run Code Online (Sandbox Code Playgroud)

ICC 似乎分配了足够的内存来存储两个对象,然后根据运行时条件(使用cmov)在两个非重叠区域之间进行选择,并将所选指针传递给转义函数。

在引用函数中,g它仅分配 104 个字节,恰好是SuperBigObject.

海湾合作委员会

f(bool):
        sub     rsp, 120
        mov     rdi, rsp
        call    escape(void const*)
        add     rsp, 120
        ret
g():
        sub     rsp, 120
        mov     rdi, rsp
        call    escape(void const*)
        add     rsp, 120
        ret
Run Code Online (Sandbox Code Playgroud)

GCC 也分配 120 字节,但它将两个对象放置在同一地址,因此不发出任何cmov指令。

f(bool):
        sub     rsp, 104
        test    edi, edi
        mov     rdi, rsp
        call    escape(void const*)@PLT
        add     rsp, 104
        ret
g():
        sub     rsp, 104
        mov     rdi, rsp
        call    escape(void const*)@PLT
        add     rsp, 104
        ret
Run Code Online (Sandbox Code Playgroud)

Clang 还合并了两个分配,并将分配大小减少到必要的 104 字节。

不幸的是我不明白为什么它测试 function 中的条件f


您还应该注意,当编译器可以将一个或两个变量放入寄存器中时,根本不会分配内存,即使它们在整个函数中使用和重新分配也是如此。对于int's 和long's 以及其他小对象来说,这是最常见的情况,如果它们的地址不转义该函数。

  • 有趣的是,GCC 合并了分配,仍然分配 120 字节,但不使用额外的空间在 16 字节边界上对齐数组。如果有人能解释这种行为,我会很高兴。 (4认同)