- ok so
- obviously not enough time for another project. not fair to you
- the grading breakdown will remain the same,
*but* - if you are unhappy with your proj1 grade - or you think you did poorly on proj2…
- you can resubmit them and get them regraded
**if you do so, let me know**

- recall:
- global optimizations work on CFG
- specify optimizations as:
- some “knowledge” we want to “prove”
- set of “instruction rules” to pass along/change that “knowledge” for each instruction
- set of “block rules” to pass along that “knowledge” between BBs

- if we’re careful, we can design a
**terminating algorithm**- finite number of states traversed
**monotonically**(“you can never go back”) - might require a few visitations of each BB but eventually it settles down

- finite number of states traversed

- last time we looked at global constant propagation (GCP)
- not a terribly
*useful*optimization but a very*simple*one good for illustrating the algorithm

- not a terribly
- GCP is a
**forward analysis**- knowledge is pushed from entry block forward through instructions and the CFG

- but now we’ll look at a
**backwards analysis**algorithm:**liveness**- and once we do that, we can get global dead code elimination “for free!”
- …and also use that for register allocation

## Liveness

- a very useful property to determine for all the variables in a function is their
**liveness**- a variable is
**live**if its value will be used again in the future - (remember: a
**use**of a variable means we*get*its value)`fn lifey(param: int): int { let ret = param + 10; // after this line, param is never used again. println_i(ret); // but ret is, all the way to the end. return ret; }`

- a variable is
- a variable’s
**liveness**is the time**from first assignment to last use**- this
*isn’t*the same thing as*lifetime:*that’s the time**from allocation to deallocation** - it’s possible for a variable’s lifetime and liveness to be the same (like
`ret`

above) - but it’s also possible - and common - for its liveness to be
*smaller*than its lifetime! - essentially liveness captures “where a variable is still useful”
- outside that range, we can “cheat” a bit and do things like reuse its space for other things

- this
- this
**liveness analysis**will be very useful in register allocation, among other things- what if you assign something to a variable and never use it? might be a bug…
- Rust uses it as an important part of its borrow checking!

- there are two ways to think about liveness:
- if you look
*at one variable,*its liveness spans a**range of instructions**(and basic blocks) - if you look
*before/after an instruction,*some**subset**of all the variables is live

- if you look
- the latter can be derived from the former
- so we’ll talk about how we determine liveness for
*one*variable - and that can be easily extended to multiple variables

- so we’ll talk about how we determine liveness for

## Determining Liveness

- the start of a variable’s liveness is easy to find: where it’s
**first assigned to**- but how do we know when that ends? what’s the “last use”?

- it makes more sense to define this property
**backwards.**let’s say that**for some variable**`x`

:*before and after*every instruction in the CFG, it can be**live**or**dead**- instruction rules:
- if an instruction
**uses**`x`

, then`x`

is**live**before it. - else if an instruction
**assigns**`x`

, then`x`

is**dead**before it.*note:*`x = x + 1`

both uses and assigns it, but the “uses” rule takes precedence.

- else, propagate liveness
*from after to before.*- (so if
`x`

is dead after, it’s dead before; if it’s live after, it’s live before.)

- (so if

- if an instruction
- block rule: for a block
`B`

:- if
`x`

is**live**at the beginning of**any successor of**then`B`

,`x`

is**live**at the**end of**`B`

. - otherwise,
`x`

is**dead**at the end of`B`

.

- if

- let’s try to express this a little more compactly…
- let’s write
`i.in[x]`

to mean “whether or not`x`

is live*before*instruction`i`

” - similarly, let’s write
`i.out[x]`

to mean “whether or not`x`

is live*after*instruction`i`

”- we can also use
`B`

instead of`i`

to mean a block - a block’s “in” is its first instruction’s “in” (i.e.
`B.in[x] == B.instructions[0].in[x]`

) - similarly, a block’s “out” is its last instruction’s “out”

- we can also use
- so the rules are:
- if
`i`

uses`x`

, then`i.in[x] = true`

- else if
`i`

assigns`x`

, then`i.in[x] = false`

- else,
`i.in[x] = i.out[x]`

`B.out[x] = any(S.in[x] == true, for S in B.successors)`

- (amazingly this is almost valid Python code)

- if

- let’s write

## Algorithm time!

- how to calculate liveness:
- set
`i.in[x] = false`

and`i.out[x] = false`

for every instruction - starting from the
**exit**of the function:- apply the instruction rules in reverse order

- keep going until
**no more changes are made.**done.- let’s try this on
`lifey`

from above!

- let’s try this on

- set
- “the” exit
- in our IR, a function can have
*multiple*exits (like a recursive function) - in practice, this can be solved by making a “dummy” exit block and setting each
`return`

block’s successor to that block- these are “impossible edges” - they don’t represent a real control flow path
- they’re just there to make the CFG data structure simpler.

- in our IR, a function can have
**this algorithm terminates:**- finite number of states (
`false`

,`true`

) - monotonic progression (a
`false`

can become a`true`

, but never the other way)`i.in[x] = false`

does*not*break that rule- you could skip that assignment, because
`i.in[x]`

defaults to`false`

- finite number of states (
- how would you implement this for
**multiple variables**in an efficient way?- the liveness of each variable really only needs
*one bit*of storage - we can represent a set of active variables as a pattern of bits
- e.g. if we have variables
`x`

,`y`

, and`z`

, then`010`

would mean`y`

is live and`x`

and`z`

are dead

- e.g. if we have variables
- fortunately our IR lists
*all the local variables*in one place- and our IR instructions refer to them by index (0, 1, 2…)

- the liveness of each variable really only needs
- what about a situation like this:
`let x = 10; x = 20; println_i(x);`

- that’s weird… it determines that
`x`

is*dead*before`x = 20`

**this is correct:**the algorithm is telling us that we assigned a value to`x`

*but never used that value!*- this might be a place where we can give the programmer a warning or error

- that’s weird… it determines that
- weirdly, a single variable can have
*multiple liveness ranges:*`let x = 10; println_i(x); println_s("hmm"); x = 20; println_i(x);`

- for the
`println_s`

line,`x`

is “dead” - in these cases we can say that each liveness range is
*a different variable*- this can have implications for register allocation

**single static assignment**(SSA) is a way of automatically renaming variables such that every variable is only ever assigned one time- it’s an extension of that “single assignment” thing I mentioned last time
- but it’s more complex because now you have to deal with multiple control flow paths
- and phi functions
- and aaaaaaa
- but it’s worthwhile because it simplifies stuff like this

- for the

## Register allocation

- the number one goal of codegen is
**correctness**- …but it’s really easy to make correct code that is really slow

- a naive mapping from IR to target language would put
**every variable in memory**- whenever we need to use a variable, we load from it or store into it

- this was a totally valid approach in the 1960s: then,
**memory was faster than the CPUs using it**- but that hasn’t been the case for a
*long, long time.*

- but that hasn’t been the case for a
- our goal is to
**minimize memory accesses**by keeping as many values in registers as possible at any time- which values are “allowed” to be in registers?
- are some better suited than others?
- what about when we call functions?
- are there any source language features which limit this?
- what about the realities of the target architecture - special-purpose registers and such?
- !saaa

- well. the
*core problem*of register allocation is not too complicated:- a function has
variables.*n* - the target has
registers.*r* - if
, easy: assign each variable to a register. done.*n*<=*r* - if
, put the*n*>*r*extra variables in memory. done.*n - r*

- a function has
- of course, even this is a little too simplistic…
- our IR has a
*lot*of temporary variables, many of them only used once - if only we had some information telling us
**which variables were in use at any given time**- oh! right.
**liveness.**

- oh! right.

- our IR has a

## The RIG

- from the liveness info, we can build the
**RIG - the Register Interference Graph:**- make a node for each variable.
- look at the “live variables” sets at
**every point**in the CFG.- if two variables are
**live in the same set,**add an edge between them in the RIG.

- if two variables are

- at the end, we have a graph where
**two variables are connected**if their**liveness ranges overlap**- which means they
**must be put into different registers.**

- which means they
- consider this code:
`fn riggy(n: int, indent: bool) { for i in 0, n { let obj = this.get(i); let msg = obj.toString(); if indent { msg = " " + msg; } println_s(msg); } }`

- the compiler uses 6 variables:
`this, n, indent, i, obj,`

and`msg`

- 4 of these variables are live for the whole function (
`this, n, indent, i`

) `obj`

and`msg`

are live for shorter times, but importantly,**their liveness does not overlap**- the RIG will be an
*almost*fully-connected graph, but there will be**no edge between**`obj`

and`msg`

- which means they can share a register!

- the compiler uses 6 variables:
- what if we rewrote it like this?
`fn riggy2(n: int, indent: bool) { if indent { for i in 0, n { println_s(" " + this.get(i).toString()); } } else { for j in 0, n { // j to avoid confusion with the other i println_s(this.get(j).toString()); } } }`

- a little repetitive, but the RIG is much more interesting. 10 locals, but many of them are live for a very short time.
`i`

and`j`

have no edge between them`t4, t5, t6, t8, t9`

have no edges between them (whoa!)- and
`indent`

now only has edges with`this`

and`n`

!- (you can probably tell that arguments can never share registers, for obvious reasons…)

- so we could say the following:
`this`

,`n`

, and`indent`

each get their own register`i`

and`j`

can share*one*register`t4, t5, t6, t8, t9`

can*all*share one register- so that’s 5 registers total!

## Finishing it off

- so far we’ve only done two steps:
- compute liveness
- compute the RIG

- the third step is to
**color the RIG****graph coloring**is the idea of assigning “colors” to every node in a graph such that:**no two nodes with an edge between them**are the**same color**- so connecting red to green is fine, but red to red is not

- a graph is
*k-colorable*if it can be colored with*k*colors- the
**smallest**for a graph is its*k**chromatic number* - so if the chromatic number is 4, that means you have to use
*at least*4 colors to color it

- the
- this sounds like a weird, abstract problem to solve but it comes up
*all. the. time.*

- in our case,
**the “colors” are the target machine’s registers.**- any variables that
**share an edge**cannot be in the same register… so they can’t be the same color.

- any variables that
- not gonna go into the whole algorithm cause graph coloring is
**NP-complete**but we can use a simple heuristic:- if a node has
**<**neighbors in a graph…*k*- and you
**remove that node and its edges…** - and the resulting graph is
*k*-colorable… - then the original graph is
*k*-colorable as well.

- and you
- you keep plucking out nodes with <
*k*neighbors and put them on a stack until the graph is empty - then you pop them from the stack and assign each a color as they come off

- if a node has
- so if we have
*r*registers, and the RIG is*r-colorable*, then boom, register allocation solved!- …but what if it isn’t? D:
- in that case, we have to
**spill:**we have to store one or more values**in memory**(on the stack). - essentially, we’ll remove it from the RIG.
- (but we’ll have to add new temporaries to hold its value between loads/stores.)
- (but those temporaries will be live for much smaller ranges.)

*which one do we spill?***there’s no wrong answer!**if you remove enough things from a graph, it will become*r*-colorable- but some choices are better than others in practice
- e.g. spilling a variable used in a tight loop is a bad idea cause it causes lots of memory accesses
- spilling a long-lived variable that’s seldom-used is a good idea cause it eliminates a lot of edges