26

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

假设我有以下功能:

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?

4

5 回答 5

15

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

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

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

于 2011-08-17T07:10:49.323 回答
11

我检查了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*)

您可以alloca在函数顶部看到 3。

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

于 2011-08-17T08:02:44.313 回答
10

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

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

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

-克里斯

于 2011-08-17T18:20:13.157 回答
4

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

在任何情况下,~512 KB 对于堆栈来说确实太大了;您应该使用std::vector.

于 2011-08-17T08:50:18.293 回答
0

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

虽然通常不鼓励使用 alloca,但在上述情况下它确实有其用途。

于 2011-08-17T07:51:54.230 回答