提问者:小点点

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


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

void foo() {
   if (runtimeCondition) {
       int x = 0;
   } else {
       SuperLargeObject y;
   }
}

当编译器为这个函数的堆栈帧分配内存时,它会不会只分配足够的内存来存储SuperLargeObject,如果导致int的条件成立,额外的内存就会被闲置?还是会以其他方式分配内存?


共2个答案

匿名用户

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

如果两个对象(intSuperLargeObject都没有使用,并且编译器可以证明构造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);
}

请注意,所有堆栈分配都是8的奇数倍,因为x86-64ABI要求堆栈指针与16字节对齐,并且8字节由返回地址的call指令推送(感谢@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

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

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

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

不幸的是,我不明白为什么它会测试函数f中的条件。

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

匿名用户

您应该假设函数中任何位置声明的所有内存在进入函数时立即分配。体面的C编译器合并具有不重叠生命周期的对象的存储。

如果您在某个特定代码路径中有一个大对象,您希望在不采用该路径时避免分配,您必须执行以下操作之一:

>

  • 动态分配它,然后在该代码路径中释放它。

    使用C99可变长度数组分配它。

    使用alloca函数/运算符分配它,该运算符在许多编译器中作为传统扩展存在。

    将代码移动到单独的辅助函数中。但是,如果该函数是内联的,这不会有什么不同!来自内联函数的堆栈分配被合并到一个大堆栈框架中,就好像代码是内联编写的一样。确保使用编译器特定的魔法来声明此函数不内联。

    #ifdef __GNUC__
    #define NOINLINE __attribute__((noinline))
    #else
    #error port me
    #endif
    
    NOINLINE void foo_LargeObjectCase()
    {
       SuperLargeObject y;
    }
    
    void foo() {
       if (runtimeCondition) {
           int x = 0;
       } else {
           foo_SuperLargeObjectCase();
       }
    }
    

    我在TXR Lisp虚拟机中使用了上面的最后一种方法。VM为各种指令分派函数。有些函数的堆栈存储更多,有些更少。我已经声明了许多这样的函数notinline,这对观察到的堆栈帧大小产生了巨大的影响。

    当然,将代码移动到函数中可能会很不方便;如果代码需要访问foo的一些局部变量,您可能必须传递所有参数,甚至一些额外的参数。

    如果您担心函数的堆栈使用情况,gcc对此提供了有用的诊断方法。您可以使用-fstack-用法来获取有关函数堆栈使用情况的信息,和/或-Wstack-用法=N警告,如果某些函数的堆栈使用超过N字节,则会发出警告。

    真实故事:-fstack-用法帮助我发现一个在GNU C库中使用crypt_r函数的函数有一个超过128 KB的堆栈帧。这就是结构crypt_datacrypt_r的上下文缓冲区有多大!我将代码切换到malloc释放结构。

  • 相关问题