Slide Set 10 - Stacks and Register Use

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 15

Stacks and register use

Prof Rajeev Barua


CS 3101 -- Slide Set 10

1
Allocating stack frame fields to registers
Until now we have assumed that all values in stack are stored in main memory.

Can we store some fields of the stack frame in registers?

● Yes, but we cannot store them permanently in registers since the number of stack
locations far exceeds the number of registers, and is unbounded with recursion.
➢ So typically each function uses registers to store stack locations AS IF ALL
REGISTERS WERE AVAILABLE FOR THE CURRENT FUNCTION.
➢ SHARING of registers across functions is ensured by saving the variables to
memory between functions.
■ This will be discussed next.

2
Inter-procedural register use
Let us say the same register R is used in two functions (f()=caller and g() = callee) to hold different
values.

● Then the variable in f() must be saved to memory at the function call from f() to g(), and restored at
return, so that modifications in callee are transparent to caller.
● Two alternatives for saving and restoring registers:
➢ Caller saves: Save all registers that are live across the call. (a variable is said to be live at a
certain program point, if it is written to before this point in time, and used after this point in time.)
➢ Callee saves: Save all registers that are written to in the callee. (Eg is on next slide)

Which of the two above register-saving conventions is better?

● The convention that saves fewer memory values is better, because it results in faster code.
● For any one call, either may be better.
● On average, both are about equally good.
● Once we choose one convention, it must be used everywhere in that ISA, otherwise code won’t work.

The register saving convention is usually recommended by the ISA, and must be followed by compilers to
ensure correct execution. 3
Example live ranges
Foo (int x) {

… /* Dead */

v←…

… /* Live */

…←v

… /* Dead */ This shows that instead of allocating variables to registers, the compiler should

v←… allocate live ranges to registers instead. This frees up registers in dead regions.

… /* Live */

…←v

… /* Dead */

Live: A variable is said to be live at a certain program point, if it could be written to in the past, and it’s current 4
value may be read in the future. Otherwise it is dead.
Example of callee saves
Main () {

Int a; /* Local variables in main() allocated to R3*/

a = 43;

foo(5, 6); /* actual arguments as shown. */

Printf (a) /* we want 43 to be printed not 9. But will be incorrect w/o register saving & restoring*/

foo(int x, int y) { /* x, y formal arguments */

Int c; /* Local variable in foo() also allocated to R3 */ ← we should save the value of c (R3)
to the stack here, in “saved registers”.
c = 9; /* R3 will be assigned to 9 */

return;← we should save the value of R3 to the stack here ← restore value of c (R3) here from
stack 5
}
Example of caller saves
Main () {

Int a; /* Local variables in main() allocated to R3 */

a = 43;
← we should save the value of a (R3) to the stack here to local variables (if
memory allocated), else saved registers (if register allocated)

foo(5, 6); /* actual arguments as shown. */


← restore value of a (R3) here from stack
Printf (a) /* we want 43 to be printed not 9. But will be incorrect w/o register saving & restoring*/

foo(int x, int y) { /* x, y formal arguments */

Int c; /* Local variable in foo() also allocated to R3 */

c = 9; /* R3 will be assigned to 9 */

return;← we should save the value of R3 to the stack here


6
}
Possible configurations of register-saving conventions
● Some ISAs use caller saves.
● Some ISAs use callee saves.
● In yet other ISAs, the register set is partitioned into two sets -- one callee saves, other caller saves.
➢ Example: The x86 ISA follows this model.
■ The compiler can take advantage of this to reduce run-time by using the following example
heuristics:
● If a variable is not live across a call, use a caller saves register, thus not needing a save.
● Try to allocate written-to variables in a function to caller saved registers, hoping that may
not need saving in the parent.
● If a variable is live across several calls to different functions, and not used in the middle,
then use caller saves to save it once before the first call and restore it after the last call,
instead of repeatedly across each call. /* Book's heuristic */

A combination of heuristics like the ones above is used.

Where are the registers saved?

To original memory locations (if the variable was allocated in memory) OR to the "saved registers" field in the
stack frame.
7
So what stack locations can be register allocated?
● Local variables: Yes. Mem location needed only if spilled.

● Arguments: Yes -- the first few fixed number of arguments (e.g., 4). Remaining arguments passed in memory.

Most functions have only a few arguments, so most arguments fit in registers.

➢ Why fix the argument registers in the ISA? Because otherwise separate compilation will not work!

■ Separate compilation is when each function is compiled separately.


■ It must be supported by all modern compilers, since otherwise programs spread across multiple files
will break if whole-program compilation is used.

Keep in mind that arguments are neither caller or callee saved, since they are meant for communication.

● Return address: The return address at a call site, if in a link register, need not be saved to stack if callee is a leaf

function.

● Temporaries: Yes -- just like locals.

● Saved registers: These are not a thing to allocate to registers, but the space where registers are saved. 8
Does passing arguments in registers save run-time?
Seeming paradox:

Suppose f(a1, ... an) receives its args in r1...rn.

Also suppose f() calls h(z).

⇒ z must also be passed in r1

⇒ Must save a1 to memory.

So did we save anything by passing via registers?

We might in general save time because:

● Some functions do not call other functions (leaf functions) -- there are many such functions!
● Arg a1 may be dead by the time h(z) is called ⇒ no saving needed.

Other reasons in the book are not in syllabus. (those are rare reasons)

Keep in mind that we must ALWAYS pass registers in arguments, regardless of the compiler’s optimization level. This is
because shared dynamic linked libraries (DLLs) expect arguments to always be in one place, not different places for 9
different callers of those DLL functions (like printf())
Example of argument being dead
Foo (int x) {

… /* x is live here */

… = x + 3; /* x becomes dead after this statement */

h(5);

… /* This code does not use read x */

Live: A variable is said to be live at a certain program point, if it has been written to in the past, and
it’s current value will be read in the future. Otherwise it is dead.

10
What if formal argument's address is taken?
Consider the following code: int *f(int x) {

Two problems: ...

■ If x is passed in a register, it has no address, so address-taken ... = &x

operator (&x) makes no sense. }

● Solution: if an argument’s address is taken, then write it to memory at the start of f().

This requires that the stack memory space for arguments be allocated even when
arguments are passed in registers, and that stack memory space is not used.

■ If x's address is returned from f(), then memory location is freed when f() exits! This
creates a invalid pointer with a freed address, called a dangling pointer! If this dangling pointer
is dereferenced in the parent of f(), that will result in a segmentation fault error message. 11
Partial solution in C

● All possible callers for f() (maybe all functions) always allocate space for all the outgoing
arguments (including first few) on the stack memory, but do not write anything to those
locations.
➢ Instead the callee writes the arguments to memory only when needed such as when an
argument's address is taken.

This is not a complete solution, since if &x escapes the parent as well (to the grandparent), then it
is still dangling.

⇒ C compilers typically do not prevent this problem.

12
A better solution: programmer changes program
If we really need to take the address of a formal parameter, the programmer is encouraged to use
call-by-reference to force allocation of the address in the parent procedure.

C and Java are call-by-value languages, which means argument values are passed, not their
addresses.

In call-by-reference, the address of an argument is passed instead of it’s value.

Examples of call by reference languages: Perl, Visual Basic.

● In a true call-by-reference languages, the programmer passes the value, but the address is
actually passed by the compiler.
● In C, there is no true call-by-reference, so pointers should be are passed as arguments
instead if we want to simulate call by reference.
➢ This pushes the job of allocating space to the programmer, who must allocate it in the
highest scope that uses that pointer (e.g., the parent or grandparent).

13
Why are variables allocated to memory?
Given that there are so many situations that variables are allocated to registers, it is good to look at
a list of reasons why a variable might be memory allocated:

● There is a list at top of page 133 of the textbook.


➢ Please cover it on your own.

14
Why are registers faster than memory?
Time of access:

● Main memory with cache miss: 80-200 cycles to access DRAM


● Main memory with cache hit: 1 cycle to access cache.

In most programs, we have a very high cache hit rate of over 95%.

Which means, most of the time, memory just takes 1 cycle to access.

● Register: 0 cycles additional for reading registers on top of the 1 cycle used to execute the
subsequent computation instruction that uses the register value.

So effectively, registers are faster than memory, because register reads are “free” in the instruction
that will use the register value. Whereas memory requires a separate instruction like a load or store
to access.

This is why registers are faster than memory locations, and therefore compilers aim to allocate as
many locations to registers as they can. 15

You might also like