В какой момент память обычно выделяется для локальных переменных в C++?

Я отлаживаю довольно странное переполнение стека, предположительно вызванное выделением слишком больших переменных в стеке, и я хотел бы уточнить следующее.

Предположим, у меня есть следующая функция:

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 килобайт) выделяются сразу после ввода функции или сначала будет выделен 1 килобайт, а затем в зависимости от условия дополнительно выделяются 1 или 512 килобайт?

5 ответов


ваши локальные (стековые) переменные выделяются в том же пространстве, что и кадры стека. При вызове функции указатель стека изменяется на" освободить место " для кадра стека. Обычно это делается за один звонок. Если вы используете стек с локальными переменными, вы столкнетесь с переполнением стека.

~512 Кбайт действительно слишком велики для стека в любом случае; вы должны выделить это в куче, используя std::vector.


на многих платформах / ABIs при вводе функции выделяется весь stackframe (включая память для каждой локальной переменной). На других, он общий для того чтобы нажать / Pop память бит за битом, по мере необходимости.

конечно, в случаях, когда весь stackframe выделяется на одном дыхании, разные компиляторы могут по-прежнему принимать решения о разных размерах кадра стека. В вашем случае некоторые компиляторы упустят возможность оптимизации и выделят уникальную память для локальная переменная, даже те, которые находятся в разных ветвях кода (как 1 * 1024 массив и 512 * 1024 один в вашем случае), где лучший оптимизирующий компилятор должен выделять только максимальную память, необходимую для любого пути через функцию (else путь в вашем случае, поэтому выделения блока 512kb должно быть достаточно). Если вы хотите знать, что делает ваша платформа, посмотрите на разборку.

но меня не удивило бы, если бы я увидел весь выделенный кусок памяти немедленно.


Я проверил на 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 в верхней части функции.

должен признаться, я немного разочарован, что b2 и b3 не сложить вместе в ИК, так как только один из них будет когда-либо использоваться.


эта оптимизация известна как" раскраска стека", потому что вы назначаете несколько объектов стека по одному адресу. Это область, которую, как мы знаем, LLVM может улучшить. В настоящее время LLVM делает это только для объектов стека, созданных распределителем регистров для слотов разлива. Мы хотели бы расширить это для обработки пользовательских переменных стека, но нам нужен способ захватить время жизни значения в IR.

существует грубый набросок того, как мы планируем это сделать здесь: http://nondot.org/sabre/LLVMNotes/MemoryUseMarkers.txt

работа по реализации этого продолжается, несколько частей реализованы в mainline.

-Крис


Как вы говорите, это зависит от компилятора, но вы можете рассмотреть возможность использования alloca чтобы преодолеть это. Переменные по-прежнему будут выделяться в стеке и по-прежнему автоматически освобождаться, когда они выходят за рамки, но вы контролируете, когда и если пространство стека выделено.

пока использование alloca обычно не рекомендуется, оно имеет свои пользы в ситуациях как вышеуказанное.