Function Call Mechanics Function Calls Function Calls
Reminder: Memory Organisation Memory Organisation Memory
HEAP Free Store
- used for objects of dynamic storage duration,
e.g. the contents of a
std::vector
- big, can be used for bulk storage (most of main memory)
- possible to allocate & deallocate any object on demand
- (de-)allocations in no particular order ⇒ fragmentation
- slow allocation: need to find contiguous unoccupied space for new objects
STACK
- used for objects of automatic storage duration: local variables, function parameters, etc.
- small (usually only a few MB)
- fast allocation: new objects are always put on top
- objects de-allocated in reverse order of their creation
- can't de-allocate objects below the topmost (= newest)
How Function Calls Work How Calls Work How?
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
Use the navigation buttons on the right/left to go forward/backwards.
This example assumes that no compiler optimizations like e.g., inlining (replacing function calls with the function body), return type optimization, etc., take place.
Also, the exact order in which things are put on the stack during
a function call (the calling convention
) depends on the platform
(CPU architecture + OS + compiler).
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
⋮ |
The program starts.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
y: 2 | |
⋮ |
The local variable y
is put on the stack.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
i: ? | |
y: 2 | |
⋮ |
The local variable i
is put on the stack.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
placeholder | |
i: ? | |
y: 2 | |
⋮ |
A placeholder for the return value of the function is put on the stack.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
return addr | |
placeholder | |
i: ? | |
y: 2 | |
⋮ |
The current instruction's memory address is put on the stack so that we know where to resume the program after leaving the called function.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
return addr | |
placeholder | |
i: ? | |
y: 2 | |
⋮ |
The frame pointer marks the beginning of the stack frame of the current function. Everything within the current stack frame will be treated as function-local. The frame pointer is needed because different function calls can have different stack frame sizes.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
return addr | |
placeholder | |
i: ? | |
y: 2 | |
⋮ |
Execution jumps to the memory address of the function square
.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
p: 2 | |
return addr | |
placeholder | |
i: ? | |
y: 2 | |
⋮ |
Function parameter p
is put on the stack;
its value is determined by the call argument (= the value of y
).
The order in which return address, placeholder, local parameters, etc. are put on the stack depends on the calling convention of the platform (CPU architecture + OS + compiler)
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
x: ? | |
p: 2 | |
return addr | |
placeholder | |
i: ? | |
y: 2 | |
⋮ |
Function-local variable x
is put on the stack.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
x: 4 | |
p: 2 | |
return addr | |
placeholder | |
i: ? | |
y: 2 | |
⋮ |
The result of expression p * p
is assigned to x
.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
x: 4 | |
p: 2 | |
return addr | |
↘ | 4 |
i: ? | |
y: 2 | |
⋮ |
The statement return x;
copies the value of x
into
the return value placeholder.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
x: 4 | |
p: 2 | |
return addr | |
4 | |
i: ? | |
y: 2 | |
⋮ |
Leaving function square
:
the stack's top position is decreased to below the stack frame;
which means that all function locals are popped from the stack
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
x: 4 | |
p: 2 | |
return addr | |
4 | |
i: ? | |
y: 2 | |
⋮ |
Execution returns to the call site by jumping to the previously stored return address.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
x: 4 | |
p: 2 | |
return addr | |
4 | |
↘ | i: 4 |
y: 2 | |
⋮ |
The assignment int i = …
causes the return value
to be copied into i
.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
x: 4 | |
p: 2 | |
return addr | |
4 | |
i: 4 | |
y: 2 | |
⋮ |
The return value is popped from the stack.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
x: 4 | |
p: 2 | |
return addr | |
k: 5 | |
i: 4 | |
y: 2 | |
⋮ |
Local variable k
is put on the stack.
int square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int i = square(y);
int k = i + 1;
}
STACK | |
x: 4 | |
p: 2 | |
return addr | |
k: 5 | |
i: 4 | |
y: 2 | |
⋮ |
The program ends.
All its associated variables are popped from the stack.
No References To Locals No Ref To Locals Return Refs?
What if we changed the return type to int&
?
int& square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int& i = square(y);
int k = i + 1;
}
int& square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int& i = square(y);
int k = i + 1;
}
STACK | |
x: 4 | |
p: 2 | |
return addr | |
placeholder | |
i: ? | |
y: 2 | |
⋮ |
Stack content right before returning from square
:
- function-local variable
x
- function parameter
p
- address of next instruction after function call
- placeholder for return value of
square
- local variables
y
andi
ofmain
int& square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int& i = square(y);
int k = i + 1;
}
STACK | |
x: 4 | |
p: 2 | |
return addr | |
↘ | address of x |
i: ? | |
y: 2 | |
⋮ |
The statement return x;
copies the address of x
into
the return value placeholder.
int& square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int& i = square(y);
int k = i + 1;
}
STACK | |
x: 4 | |
p: 2 | |
return addr | |
address of x | |
i: ? | |
y: 2 | |
⋮ |
Leaving function square
:
the stack's top position is decreased to below the stack frame;
which means that all function locals are popped from the stack
Execution returns to the call site by jumping to the previously stored return address.
int& square (int p) {
int x;
x = p * p;
return x;
}
int main () {
int y = 2;
int& i = square(y);
int k = i + 1;
}
STACK | |
x: 4 | |
p: 2 | |
return addr | |
address of x | |
i: INVALID | |
y: 2 | |
⋮ |
The assignment int& i = …
causes the return value
(= a memory address of an integer) to be copied into reference i&
.
The memory location of x
however is above the stack's current top
position.
Any subsequent stack allocation will cause it to be overwritten with other
values.
⇒ Undefined Behavior
The runtime behavior of such a program is undefined/non-deterministic,
since it might sometimes work (if the memory of x
is not overwritten)
and sometimes not.
Modern C++ compilers perform several optimizations (especially at higher optimization levels -O2 and -O3) that make function calls much faster.
Return Value Optimization (RVO)
Applies to statements like return Type{};
or return Type{argument,…};
.
No extra placeholder for the return value is allocated, no copy takes
place. Instead, the external object res
is directly constructed
at the call site.
This optimization is mandatory, i.e., guaranteed to be performed as of C++17.
Point foo (…) {
…
return Point{…};
}
Point res = foo();
Named Return Value Optimization (NRVO)
Applies to statements like return local_variable;
.
No extra placeholder for the return value is allocated and no copy
takes place.
Instead, the local object loc
and the external object
res
are treated as one and the same.
This results in only one allocation at the call site.
This optimization is not mandatory, but almost all modern compilers will perform it, if possible.
Point foo (…) {
Point loc;
…
return loc;
}
Point res = foo();
Inlining
Calls to small/short functions are replaced with the code of the function.
|
|
Inlining can only happen, if the compiler "sees" not only the function declaration but also its full definition, which is not necessarily the case, if we compile different parts of a program separately (more in chapter Separate Compilation ).
This is one source of C++'s performance advantages. Inlining is a lot harder or sometimes impossible in many other languages like Java, C#, etc. with always-on polymorphism which means that all/most function/method calls can only be resolved at runtime.
Comments…