6

In the first answer here, the following was mentioned about the stack memory in C++:

When a function is called, a block is reserved on the top of the stack for local variables and some bookkeeping data.

This makes perfect sense on the top-level, and makes me curious about how smart compilers are when allocating this memory in and of itself, given the context of this question: Since braces themselves are not a stack frame in C (I assume this holds true for C++ as well), I want to check whether compilers optimize reserved memory based on variable scopes within a single function.

In the following I'm assuming that the stack looks like this before a function call:

--------
|main()|
-------- <- stack pointer: space above it is used for current scope
|      |
|      |
|      |
|      |
--------

And then the following after invoking a function f():

--------
|main()|
-------- <- old stack pointer (osp)
|  f() |
-------- <- stack pointer, variables will now be placed between here and osp upon reaching their declarations
|      |
|      |
|      |
|      |
--------

For example, given this function

void f() {
  int x = 0;
  int y = 5;
  int z = x + y;
}

Presumably, this will just allocate 3*sizeof(int) + some extra overhead for bookkeeping.

However, what about this function:

void g() {
  for (int i = 0; i < 100000; i++) {
    int x = 0;
  }
  {
    MyObject myObject[1000];
  }
  {
    MyObject myObject[1000];
  }
}

Ignoring compiler optimizations which may elide a lot of stuff in the above since really they do nothing, I'm curious about the following in the second example:

  • For the for loop: will the stack space be large enough to fit all 100000 ints?
  • On top of that, will the stack space contain 1000*sizeof(MyObject) or 2000*sizeof(MyObject)?

In general: does the compiler take variable scope into account when determining how much memory it will need for the new stack frame, before invoking a certain function? If this is compiler-specific, how do some well-known compilers do it?

Community
  • 1
  • 1
Jimmy
  • 435
  • 1
  • 5
  • 18
  • 3
    A pair of `{}` is one scope. The loop reuses the same memory for `x`, and the two `myObject` arrays don't exist at the same time. – LogicStuff Mar 21 '16 at 11:03
  • 1
    Why would it need to allocate the space for `100000` ints, when it can reuse the same space? Same goes for arrays. – Algirdas Preidžius Mar 21 '16 at 11:04
  • 1
    The compiler examines each scope of the function and the space reserved is the maximum of space of all scopes that can exist at the same time. – Jabberwocky Mar 21 '16 at 11:06
  • Stack space is pre-allocated, the compiler just uses it until it runs out and you have an overflow. – Jonathan Potter Mar 21 '16 at 11:13
  • There's no such thing as stack frame in the C++ language. It is an implementation detail. A compiler is free to create a frame for any braces it wants, or conversely, not create a frame for any function it wants. – n. m. could be an AI Mar 21 '16 at 11:21
  • 2
    @n.m. At the same time, not all questions about C++ have to ask about the language only. Asking about implementation details of compilers, or just general principles how compilers normally deal with a language feature, is fine as well. – Angew is no longer proud of SO Mar 21 '16 at 11:29
  • @Angew It's OK to ask about implementation details, as long as they are clearly understood to be implementation details. I'm not sure it is being clearly understood. For example, why ask what a non-optimizing compiler would be doing? Who cares? My guess is that "non-optimizing compiler" is being confused with a "C++ abstract machine", which has no concept of stack or stack frame. – n. m. could be an AI Mar 21 '16 at 11:34
  • "It's OK to ask about implementation details, as long as they are clearly understood to be implementation details. I'm not sure it is being clearly understood." I understand that perfectly fine. You can tell from the question which asks about the compiler. I probably shouldn't have used "IF this is compiler-specific" in the body. "For example, why ask what a non-optimizing compiler would be doing?" I'm not. I'm asking about optimizing compilers. The only reason I mentioned ignoring elision is in order to keep the examples simple with as little code as possible. – Jimmy Mar 21 '16 at 12:57
  • And I'm just asking out of curiosity about low-level details. I know this has no effect on day-to-day coding. – Jimmy Mar 21 '16 at 13:00

2 Answers2

4

The compiler will allocate space as needed (typically for all items at the beginning of the function), but not for each iteration in the loop.

For example, what Clang produces, as LLVM-IR

define void @_Z1gv() #0 {
  %i = alloca i32, align 4
  %x = alloca i32, align 4
  %myObject = alloca [1000 x %class.MyObject], align 16
  %myObject1 = alloca [1000 x %class.MyObject], align 16
  store i32 0, i32* %i, align 4
  br label %1

; <label>:1:                                      ; preds = %5, %0
  %2 = load i32, i32* %i, align 4
  %3 = icmp slt i32 %2, 100000
  br i1 %3, label %4, label %8

; <label>:4:                                      ; preds = %1
  store i32 0, i32* %x, align 4
  br label %5

; <label>:5:                                      ; preds = %4
  %6 = load i32, i32* %i, align 4
  %7 = add nsw i32 %6, 1
  store i32 %7, i32* %i, align 4
  br label %1

; <label>:8:                                      ; preds = %1
  ret void
}

This is the result of:

class MyObject
{
public:
    int x, y;
};

void g() {
  for (int i = 0; i < 100000; i++) 
  {
    int x = 0; 
  } 
  {
    MyObject myObject[1000]; 
  } 
  {
    MyObject myObject[1000]; 
  } 
} 

So, as you can see, x is allocated only once, not 100000 times. Because only ONE of those variables will exist at any given time.

(The compiler could reuse the space for myObject[1000] for x and the second myObject[1000] - and probably would do so for an optimised build, but in that case it would also completely remove these variables as they are not used, so it wouldn't show very well)

Mats Petersson
  • 126,704
  • 14
  • 140
  • 227
  • And in terms of the stack pointer: will it just be incremented by `max(2*sizeof(int), 1000*sizeof(MyObject))` upon reaching `g()`? Since only those variables can exist at the same time. I don't think that's clear from the assembly. – Jimmy Mar 21 '16 at 13:04
  • Most likely, yes, but it may be the sum of all local variables - will almost certainly be that in a non-optimised build [which is what my code shows] – Mats Petersson Mar 21 '16 at 13:28
  • Of course, in an optimised build `i` and `x` will most likely reside in registers rather than on stack. – Mats Petersson Mar 21 '16 at 13:30
2

In a modern compiler, the function is first transformed to a flow graph. In every arc of the flow, the compiler knows how many variables are live - that is to say holding a visible value. Some of those will live in registers, and for the others the compiler will need to reserve stack space.

Things get a bit more complicated as the optimizer gets further involved, because it may prefer not to move stack variables around. That's not free.

Still, in the end the compiler has all the assembly operations ready, and can just count how many unique stack addresses are used.

MSalters
  • 173,980
  • 10
  • 155
  • 350