C++ 中的局部变量通常在啥时候分配内存?

Posted

技术标签:

【中文标题】C++ 中的局部变量通常在啥时候分配内存?【英文标题】:At what moment is memory typically allocated for local variables in C++?C++ 中的局部变量通常在什么时候分配内存? 【发布时间】:2011-10-28 16:14:46 【问题描述】:

我正在调试一个相当奇怪的堆栈溢出,据说是由于在堆栈上分配了太大的变量而引起的,我想澄清以下内容。

假设我有以下功能:

void function()

    char buffer[1 * 1024];
    if( condition ) 
       char buffer[1 * 1024];
       doSomething( buffer, sizeof( buffer ) );
     else 
       char buffer[512 * 1024];
       doSomething( buffer, sizeof( buffer ) );
    
 

我知道,它依赖于编译器,也取决于优化器的决定,但是为这些局部变量分配内存的典型策略是什么?

最坏的情况(1 + 512 KB)是在进入函数后立即分配还是先分配 1 KB,然后根据情况再分配 1 或 512 KB?

【问题讨论】:

我认为通常一次性分配所有可能需要的堆栈空间。 在这种情况下,最好将其拆分为单独的函数,这样每个函数都有自己的堆栈空间,而您的主 function() 不会一次全部分配。 【参考方案1】:

在许多平台/ABI 上,整个堆栈帧(包括每个局部变量的内存)都是在您输入函数时分配的。在其他情况下,根据需要逐位推送/弹出内存是很常见的。

当然,在一次性分配整个堆栈帧的情况下,不同的编译器可能仍会决定不同的堆栈帧大小。在您的情况下,某些编译器会错过优化机会,并为 每个 局部变量分配唯一内存,即使是位于代码不同分支中的那些(1 * 1024 数组和 @987654322 @ one 在你的情况下),一个更好的优化编译器应该只分配通过函数的任何路径所需的最大内存(在你的情况下是 else 路径,所以分配一个 512kb 块就足够了)。 如果您想知道您的平台是做什么的,请查看反汇编。

但是看到立即分配的整个内存块我不会感到惊讶。

【讨论】:

我可以确认 Visual Studio 至少在出现异常的情况下,它会为您输入函数时可能抛出的所有异常分配存储空间。递归这样的函数对你的堆栈来说是地狱。试试看——创建一个递归函数,在第 4000 次递归中抛出一个 2k 对象——它会因堆栈溢出而崩溃。 一般来说,Visual C++ 设法通过 then/else 重新使用与上述情况相同的堆栈空间。它仍然在函数序言中分配空间,但至少不会为两个缓冲区分配空间。如果您真的想推迟分配,您应该查看 alloca() 或 std::vector.【参考方案2】:

我查看了LLVM:

void doSomething(char*,char*);

void function(bool b)

    char b1[1 * 1024];
    if( b ) 
       char b2[1 * 1024];
       doSomething(b1, b2);
     else 
       char b3[512 * 1024];
       doSomething(b1, b3);
    

产量:

; ModuleID = '/tmp/webcompile/_28066_0.bc'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-unknown-linux-gnu"

define void @_Z8functionb(i1 zeroext %b) 
entry:
  %b1 = alloca [1024 x i8], align 1               ; <[1024 x i8]*> [#uses=1]
  %b2 = alloca [1024 x i8], align 1               ; <[1024 x i8]*> [#uses=1]
  %b3 = alloca [524288 x i8], align 1            ; <[524288 x i8]*> [#uses=1]
  %arraydecay = getelementptr inbounds [1024 x i8]* %b1, i64 0, i64 0 ; <i8*> [#uses=2]
  br i1 %b, label %if.then, label %if.else

if.then:                                          ; preds = %entry
  %arraydecay2 = getelementptr inbounds [1024 x i8]* %b2, i64 0, i64 0 ; <i8*> [#uses=1]
  call void @_Z11doSomethingPcS_(i8* %arraydecay, i8* %arraydecay2)
  ret void

if.else:                                          ; preds = %entry
  %arraydecay6 = getelementptr inbounds [524288 x i8]* %b3, i64 0, i64 0 ; <i8*> [#uses=1]
  call void @_Z11doSomethingPcS_(i8* %arraydecay, i8* %arraydecay6)
  ret void


declare void @_Z11doSomethingPcS_(i8*, i8*)

你可以在函数顶部看到3个alloca

我必须承认我有点失望,b2b3 在 IR 中没有折叠在一起,因为它们中只有一个会被使用。

【讨论】:

哇,太令人失望了——这比我预期的最坏情况还要糟糕。 我刚刚在 Visual C++ 10 上进行了测试 - 行为与您在 LLVM 上看到的相同。很伤心。没想到分配算法这么差。 @sharptooth:我只记得不合并 LLVM IR 中的变量是有意义的,因为它有助于分析,我不知道在最终程序集中它们是否会被合并。我已经在 llvm-dev 上询问过,我会根据结果更新答案。【参考方案3】:

这种优化称为“堆栈着色”,因为您将多个堆栈对象分配给同一个地址。这是我们知道 LLVM 可以改进的一个领域。目前 LLVM 仅对由寄存器分配器为溢出槽创建的堆栈对象执行此操作。我们也想扩展它以处理用户堆栈变量,但我们需要一种方法来捕获 IR 中值的生命周期。

这里有一个我们计划如何做到这一点的粗略草图: http://nondot.org/sabre/LLVMNotes/MemoryUseMarkers.txt

这方面的实施工作正在进行中,一些部分已在主线中实施。

-克里斯

【讨论】:

【参考方案4】:

您的本地(堆栈)变量分配在与堆栈帧相同的空间中。调用该函数时,堆栈指针更改为为堆栈帧“腾出空间”。它通常在一次调用中完成。如果使用局部变量使用堆栈,则会遇到堆栈溢出。

~512 kbytes 对于堆栈来说确实太大了;你应该使用std::vector在堆上分配它。

【讨论】:

不,如果堆栈分配导致问题,您应该使用vector在堆上分配它!【参考方案5】:

正如你所说,它依赖于编译器,但你可以考虑使用alloca 来克服这个问题。变量仍会在堆栈上分配,并且在超出范围时仍会自动释放,但您可以控制何时以及是否分配堆栈空间。

虽然use of alloca is typically discouraged,但在上述情况下确实有其用途。

【讨论】:

将 512kb 的数组放在堆上不是更好的选择吗?即使使用 alloca,您也使用了大量的堆栈空间。 @jalf,很可能是 512k。对于较小的数量,我可以设想它对于某些递归情况或堆可能很慢的性能关键情况是有意义的。 理论上是正确的。在实践中,对于性能关键的情况,我只是在堆上预先分配了那些 512kb。 ;) 最简单的解决方法是将局部变量放入单独的“子函数”中。调用函数中的一个分支将确定需要调用哪个子函数,并且堆栈指针将仅移动该子函数中局部变量所需的量。 @jalf 堆分配比堆栈“分配”要昂贵得多。如果您需要多个对象,则将它们分配在堆上会付出更高的代价。如果您只需要一个对象,只需将其设为静态即可。

以上是关于C++ 中的局部变量通常在啥时候分配内存?的主要内容,如果未能解决你的问题,请参考以下文章

动态内存分配(c++)

C++ 动态内存分配(6种情况,好几个例子)

java程序是在编译的时候分配空间的吗,如果不是那程序在啥时候给变量分配内存空间?

(转载)C++内存分配方式详解——堆栈自由存储区全局/静态存储区和常量存储区

C++数据存储方式

C++三种内存分配方式