0

If I define a variable anywhere in a function (not at the beginning), when the program is compiled and executed to this function, will space be allocated to the defined variable first or will it be allocated when it runs to the defined statement? If it runs in order, Will it reduce some overhead when problems arise?

like so:

if(!(Size && Packs))
{
    ret = false;
    return SendAck(ret);
}
uint8_t  *pSrc = (uint8_t *)pRcv->data;
uint8_t  crc = 0;
wangxuran
  • 1
  • 1
  • In the abstract model the C standard uses, for objects with automatic storage duration, memory is allocated upon entry to the associated block for objects that are not variable length arrays and when execution reaches the declaration for objects that are variable length arrays. The actual implementation by a compiler may differ. E.g., for blocks nested inside functions, the compiler may allocate memory upon function entry rather than upon block entry. – Eric Postpischil Mar 24 '22 at 12:41
  • One difference is that in `{ label: stuff; int x; x = 3; goto label; }`, `x` still exists after the jump to `label`, but, in `{ label: stuff; int x[n]; x[0] = 3; goto label; }`, `x` ceases to exist (in the abstract model) when the jump is executed. – Eric Postpischil Mar 24 '22 at 12:43
  • @EricPostpischil Thank you so much for answering this closed question. After reading your reply I am even more confused, You mean that the objects with automatic storage duration are allocated when the execution reaches the block, but this answer( [link](https://stackoverflow.com/a/15385838/16702341) _italic_ **bold** `code` )seems to say that they are allocated when the execution reaches declaration. – wangxuran Mar 25 '22 at 13:31

1 Answers1

0

The Standard C Computing Model

The C standard specifies memory reservation requirements in terms of storage duration and lifetime. Objects declared inside functions without extern, static, or _Thread_local have automatic storage duration. (Other storage durations, not discussed here, are static, thread, allocated, and temporary.) This includes parameters of functions, since they are declared inside the function declaration.

Each declaration inside a function has an associated block. Blocks are groups of statements (sometimes just a single statement). A compound statement bracketed with { and } is a block, each selection statement (if, switch) and each loop statement (for, while, do) is a block, and each substatement of those statements is a block. The block associated with a declaration inside a function is the innermost block it is in. For a function parameter, its associated block is the compound statement that defines the function.

For an automatic object that is not a variable length array, its lifetime starts when execution enters the block it is in, and it ends when execution of the block ends. (Calling a function suspends execution of the block b ut does not end it.) So in:

{
Label:
    foo();
    int A;
}

A exists as soon as execution reaches Label, because execution of A’s block has started.

This means that, as soon as the block is entered, all automatic objects in it other than variable length arrays should have memory reserved for them.

This fact is generally of little use, as there is no way to refer to A at Label. However, if we do this:

{
    int i = 0;
    int *p;
Label:
    if (0 < i)
        *p += foo();
    int A = 0;
    p = &A;
    if (++i < 3)
        goto Label;
    bar(A);
}

then we can use A at Label after the first iteration because p points to it. We could imagine motivation for code like this could arise in a loop that needs to treat its first iteration specially. However, I have never seen it used in practice.

For an automatic object that is a variable length array, its lifetime starts when execution reaches its declaration and ends when execution leaves the scope of the declaration. So with this code:

int N = baz();
{
    int i = 0;
Label:
    foo();
    int A[N];
    if (++i < 3)
        goto Label;
}

A does not exist at Label. Its lifetime begins each time execution reaches the declaration int A[N]; and ends, in the first few iterations, when the goto Label; transfers execution out of the scope of the declaration or, in the last iteration, when execution of the block ends.

Practical Implementation

In general-purpose C implementations, automatic objects are implemented with a hardware stack. A region of memory is set aside to be used as a stack, and a particular processor register, call the stack pointer, keeps track of how much is in use, by recording the address of the current “top” of stack. (For historic reasons, stacks usually start at high addresses and grow toward lower addresses, so the logical top of a stack is at the bottom of its used addresses.)

Because the lifetimes of automatic objects other than variable length arrays start when execution of their associate blocks begins, a compiler could implement this by adjusting the stack pointer whenever entering or ending a block. This can provide memory efficiency in at least two ways. Consider this code:

if (foo(0))
{
    int A[100];
    bar(A, x, 0);
}
if (foo(1))
{
    int B[100];
    bar(B, x, 1);
}
if (foo(2))
{
    int C[1000];
    bar(C, x, 2);
}

Because A and B do not exist at the same time, the compiler does not have to reserve memory for both of them when the function starts. It can adjust the stack pointer when each block is entered and ended. And for the large array C, the space might never be reserved at all; a compiler could choose to allocate space for 1000 int only if the block is actually entered.

However, I do not think GCC and Clang are taking advantage of this. In practice, I think they generally figure out the maximum space will be needed at any one time in the function and allocate that much space on the stack and use it through the function. This does include optimizations like using the same space for A and B, since they are never in use at the same time, but it does not include optimizating for the possibility that C is never used. However, I could be wrong; I have not checked on this compiler behavior lately.

In the cases of variable length arrays, the compiler generally cannot plan the memory use in advance, since it does not know the array size. So, for a variable length array, space has to be reserved for it on the stack when its declaration is reached.

Additionally, note that the compiler does not have to implement the computing model the C standard uses literally. It can make any optimizations that get the same observable behavior. This means that, for example, if it can tell part of an array is not used, it does not have to allocate memory for that at all. This means the answer to your question, “… will space be allocated to the defined variable first or will it be allocated when it runs to the defined statement?”, is that a compiler designer may choose either method, as long as the observable behavior of the resulting program matches what the C standard specifies.

The observable behavior includes data written to files, input/output interactions, and accesses to volatile objects.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312