The need for storing contents of registers:
What happens to the values in registers when we make a function call? Assume x
in foo()
and y
in bar()
happen to be allocated to the same register r1
.
void foo() {
int x = 1;
bar(x);
x = x + 1; // the value of x after this is 2
}
void bar(int k) {
int y = 2;
y++;
}
See the problem? If not, look at this:
void foo() {
r1 = 1;
bar(r1);
r1 = r1 + 1; // the value of r1 should be 2 again
}
void bar(int k) {
r1 = 2;
r1 = r1 + 1;
}
Each function call makes an independent decision for which register to allocate a variable. bar()
then chooses to use the same register r1
to store a totally different variable, so then after the bar(r1)
call, the value of r1
would be 3 at the end of foo()
! Clearly not the right answer.
The problem here is that the functions are not preserving the values in the registers across the function call.
The question this lecture attempts to answer:
How can we reduce the number of function calls we need to save and restore register values?
There are static instructions and dynamic instructions. The dynamic instruction count is the actual number of instructions executed by the CPU for a specific program execution, whereas the static instruction count is the number of instruction the program has.
We usually use dynamic instruction count as if for example you have a loop in your program then some instructions get executed more than once. Also, in the presence of branches, some instructions may not be executed at all.
We want to use as little dynamic instructions as possible, even if this means more static instructions.
If the function foo()
calls bar()
, then foo()
is a caller and bar()
is a callee.
What if bar()
calls zoo()
after?
foo()
is callerbar()
is caller and calleezoo()
is the calleeNatural solution: store the variables before a function call, and restore them after a function call
foo() {
r0 = 5;
r4 = -1;
bar();
r3 = r0 + r4;
}
bar() {
r0 = 10;
r4 = 5;
}
Here's some pseudocode for the assembly code:
foo() {
r0 = 5;
r4 = -1;
save r0; i.e., str r0, [r13, #20]
save r4; i.e., str r4, [r13, #24]
bar();
restore r0; i.e., ldr r0, [r13, #20]
restore r4; i.e., ldr r4, [r13, #24]
r3 = r0 + r4;
}
bar() {
save r4; i.e., str r4, [r13, #8]
r0 = 10;
r4 = 5;
restore r4; i.e., ldr r4, [r13, #8]
}
If a variable is dead, you want to use caller save. Would you want to use callee save? Let's say that you have the following code:
foo() {
for(int i = 0; i < n; i++) {
bar();
}
}
If you performed callee save, then you would be using \(2n\) instructions rather than only \(2\) instructions.
foo() {
a = ...
b = ...
bar();
... = a;
... = b;
for(1 to 15) {
c = ...
d = ...
... = c;
printf();
... = d;
}
}
Let's say you have 2 caller and 2 callee registers.
c
live across the function call printf()
? No! The value never got read after this. The value is dead. If c
is dead, and you assign it to a caller save register, you don't have to save it.d
in caller save, you would have to execute \(2*15\) instructions. Let's try callee save. You would have to just save d
once and restore d
once.