On this page:
9.1 Assignment Summary
9.2 Language Diagram
9.3 Register Allocation
9.4 Undead and Conflict Analysis
9.4.1 Undeadness Analysis
9.4.2 Conflict Analysis
9.5 Optimizing assign-homes
7.5.0.17

9 Compiler 3: Register Allocation (Corrected)

9.1 Assignment Summary

The goal of this assignment is to introduce (1) "optimizing" compilation (2) register allocation, perhaps the most important "optimization". In this assignment, you will replace the assign-homes pass with assign-homes-opt, a version that tries hard to put variables into registers instead of in the frame.

This assignment is due Friday January 31, 2020 at 11:59pm.

Assignment Checklist

You should find a new repository in your https://github.students.cs.ubc.ca account named a3_<team ids> with a code skeleton and a support library. You should complete the assignment in that git repository and push it to the GitHub Students instance.

You should first copy your solution to Compiler 2: Values-lang to x64 into the starter code provided. You can start from the code base of any of your team members.

If we patch the code skeleton or library after the release, the most recent version will be available here at a3-skeleton.rkt and a3-compiler-lib.rkt, and the functional graph library a3-graph-lib.rkt. The name of the skeleton is a3-skeleton.rkt to avoid accidentally overwriting your files, but your file in the Git repository should be named a3.rkt.

9.2 Language Diagram

9.3 Register Allocation

To assign abstract locations (variables) to physical locations efficiently, we need to know when any two variables are in conflict, i.e., cannot be assigned to the same physical location because they might both be used at the same time.

We start by designing a language, Conflict-lang, in which we assume someone has told us all the variables that are in use and which other variables they are in conflict with. This is all the information we need to complete register allocation in one step, so we can make progress by assuming we have it, and figuring out how to get it later. In Conflict-lang, the program is annotated with: (1) a list of local variables declarations, like in Loc-locals-lang, and (2) a conflict graph, represented as an association list from a variable to the set of variables with which it conflicts. The language is essentially the same as Loc-locals-lang except for the addition of the conflict graph to the info field.

  p ::= (begin info s ...)
     
  info ::= 
((locals (aloc ...))
 (conflicts ((aloc (aloc ...)) ...))
 any ...)
     
  s ::= (set! aloc int64)
  | (set! aloc aloc)
  | (set! aloc (binop aloc aloc))
  | (halt aloc)
     
  binop ::= *
  | +

The (conflicts ((aloc (aloc ...)) ...)) form in the info field represents a conflict graph. Interpreted as an undirected graph, the variables as represented as nodes (also known as vertexes), and conflicts between variables are represented as an edge the variable to each of the variables in the associated set. If there is an edge between any two nodes, then they are in conflict. Interpreted as a dictionary, this form maps each variable to a set of its conflicts.

As in the previous assignment, the info field also contains a declaration of the (locals (aloc ...)) abstract locations that may be used in the program, and (as usual) possibly other non-required but useful information.

The register assignment algorithm takes a Conflict-lang program and produces a Loc-assigned-lang program, annotating the program with an assignment of variables to physical locations in the info field.

The core algorithm has a straight-forward recursive description, recurring over the list of locals and producing an assignment, i.e., an association list mapping variables to physical locations.
  • If the list of variables is empty, return the empty assignment.

  • Choose a low-degree variable from the input list of variables, if one exists. Otherwise, pick an arbtrary variable (such as the first one in the list).

    A low-degree variable is one with fewer than k conflicts. We pick k to be the number of registers available for use during register allocation.

  • Recur with the chosen variable removed from the input list and the conflict graph. The recursive call should return an assignment for all the remaining variables.

  • Attempt to select a register for the chosen variable. You cannot select registers to which conflicting variables were assigned by the recursive call. This attempt succeeds if a low-degree variable was chosen, and might fail otherwise (but it depends on which registers got allocated in the recursive call).
    • If you succeed in assigning a register, then add the assignment for the chosen variable to the result of the recursive call.

    • Otherwise, we cannot assign the choosen variable to a register. Instead, we spill it, i.e., we allocate a fresh frame location using a displacement mode operand.

This algorithm is an adaptation of the optimistic register allocation described in "Improvements to graph coloring register allocation" (ACM TOPLAS 6:3, 1994) by Preston Briggs, et al.

We can simplify the implementation of this algorithm by separting it into two parts: first, sort all variables in degree order, then assign each register in sorted order. See sort.

Exercise 1: Design and implement the parameter? current-assignable-registers, which stores the list of k registers available to the register allocator.

The default should be all registers except the following. You may not use the register rax, since it is used to patch instructions, or rbp, since it contains the frame pointer.

Exercise 2: Design and implement the function assign-registers, which performs graph-coloring register allocation. The source language is Conflict-lang and the target language is Loc-assigned-lang. The pass should attempt to fit each of the variables declared in (locals (aloc ...)) into a free register, and if one cannot be found, should allocate the variable a fresh frame location.

We provide a functional graph library, a3-graph-lib.rkt, for working with the conflict graph.

This algorithm is somewhat tedious to write purely functionally. You may use a single mutable variable in your implementation. Use box, unbox, and set-box! for an idomatic way of using mutable variables in Racket.

It will be difficult to keep enough variables live at one time to test spilling. You should use parameterize and current-assignable-registers to test spilling.

Below we give some examples. However, your register allocator may produce different assignments for the same program and still be correct. This is because the register allocation algorithm makes arbitrary choices in some places. Do not overfit to these examples.

Examples:
> (assign-registers
   '(begin ((locals (x.1))
            (conflicts ((x.1 ()))))
      (set! x.1 42)
      (halt x.1)))

'(begin ((assignment ((x.1 r15)))) (set! x.1 42) (halt x.1))

> (parameterize ([current-assignable-registers '(r9)])
    (assign-registers
     '(begin ((locals (x.1))
              (conflicts ((x.1 ()))))
             (set! x.1 42)
             (halt x.1))))

'(begin ((assignment ((x.1 r9)))) (set! x.1 42) (halt x.1))

> (parameterize ([current-assignable-registers '()])
    (assign-registers
     '(begin ((locals (x.1))
              (conflicts ((x.1 ()))))
        (set! x.1 42)
        (halt x.1))))

'(begin ((assignment ((x.1 (rbp - 8))))) (set! x.1 42) (halt x.1))

> (assign-registers
   '(begin ((locals (a.1 b.2 c.3 d.4 e.5))
            (conflicts ((a.1 (b.2 c.3 d.4 e.5))
                        (b.2 (a.1 c.3 e.5))
                        (c.3 (a.1 b.2 e.5))
                        (d.4 (a.1 e.5))
                        (e.5 (a.1 b.2 c.3 d.4)))))
     (set! a.1 1)
     (set! c.3 2)
     (set! b.2 a.1)
     (set! b.2 (+ a.1 c.3))
     (set! d.4 (* b.2 c.3))
     (halt d.4)))

'(begin

   ((assignment ((a.1 r12) (b.2 r13) (c.3 r14) (d.4 r14) (e.5 r15))))

   (set! a.1 1)

   (set! c.3 2)

   (set! b.2 a.1)

   (set! b.2 (+ a.1 c.3))

   (set! d.4 (* b.2 c.3))

   (halt d.4))

> (parameterize ([current-assignable-registers '(rbx rcx r9 r10)])
    (assign-registers
     '(begin ((locals (a.1 b.2 c.3 d.4 e.5))
              (conflicts ((a.1 (b.2 c.3 d.4 e.5))
                          (b.2 (a.1 c.3 e.5))
                          (c.3 (a.1 b.2 e.5))
                          (d.4 (a.1 e.5))
                          (e.5 (a.1 b.2 c.3 d.4)))))
        (set! a.1 1)
        (set! c.3 2)
        (set! b.2 a.1)
        (set! b.2 (+ a.1 c.3))
        (set! d.4 (* b.2 c.3))
        (halt d.4))))

'(begin

   ((assignment ((b.2 rbx) (a.1 rcx) (c.3 r9) (d.4 r9) (e.5 r10))))

   (set! a.1 1)

   (set! c.3 2)

   (set! b.2 a.1)

   (set! b.2 (+ a.1 c.3))

   (set! d.4 (* b.2 c.3))

   (halt d.4))

> (assign-registers
   '(begin ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1))
            (conflicts
             ((x.3 (z.5 p.1 y.4 v.1 w.2))
              (w.2 (z.5 p.1 y.4 v.1 x.3))
              (v.1 (w.2 x.3))
              (y.4 (t.6 z.5 p.1 w.2 x.3))
              (p.1 (t.6 z.5 y.4 w.2 x.3))
              (z.5 (t.6 p.1 y.4 w.2 x.3))
              (t.6 (z.5 p.1 y.4)))))
       (set! v.1 1)
       (set! w.2 46)
       (set! x.3 v.1)
       (set! p.1 7)
       (set! x.3 (+ x.3 p.1))
       (set! y.4 x.3)
       (set! p.1 4)
       (set! y.4 (+ y.4 p.1))
       (set! z.5 x.3)
       (set! z.5 (+ z.5 w.2))
       (set! t.6 y.4)
       (set! p.1 -1)
       (set! t.6 (* t.6 p.1))
       (set! z.5 (+ z.5 t.6))
       (halt z.5)))

'(begin

   ((assignment

     ((v.1 r15) (w.2 r11) (x.3 r14) (y.4 r12) (z.5 r13) (t.6 r14) (p.1 r15))))

   (set! v.1 1)

   (set! w.2 46)

   (set! x.3 v.1)

   (set! p.1 7)

   (set! x.3 (+ x.3 p.1))

   (set! y.4 x.3)

   (set! p.1 4)

   (set! y.4 (+ y.4 p.1))

   (set! z.5 x.3)

   (set! z.5 (+ z.5 w.2))

   (set! t.6 y.4)

   (set! p.1 -1)

   (set! t.6 (* t.6 p.1))

   (set! z.5 (+ z.5 t.6))

   (halt z.5))

When debugging your register allocator, you might try comparing your allocator to the graph coloring algorithm provided by Racket Generic Graph Library. This provides an implementation of graph coloring, although it does not perform spilling so it will not work in general. You cannot use this library in the implementation of your compiler; it may only be used for testing. Unfortunately, it uses a different representation of graphs, so you will need to write conversion procedures.

You may also choose to preserve additional info fields in the output of assign-registers. We define Loc-assigned-lang v2 below, which assumes that you preserve the conflicts and locals info fields. Note that Loc-assigned-lang v2 also meets the specification for Loc-assigned-lang, so every Loc-assigned-lang v2 program is a Loc-assigned-lang program.
  p ::= (begin info s ... (halt aloc))
     
  info ::= 
((assignment ((aloc rloc) ...))
 (locals (aloc ...))
 (conflicts ((aloc (aloc ...)) ...))
 any ...)
     
  s ::= ; Same as Loc-assigned-lang

If you do, you can use check-assignment in your compiler pipeline to detect bugs in the register allocator. check-assignment expects a Loc-assigned-lang v2 programs and returns a Loc-assigned-lang program, or an error if a bad assignment is detected.

Examples:
> (check-assignment
  '(begin ((locals (x.1 y.1))
           (conflicts ((x.1 (y.1)) (y.1 (x.1))))
           (assignment ((x.1 r9) (y.1 r9))))
          (set! x.1 42)
          (set! y.1 42)
          (set! x.1 (+ x.1 y.1))
          (halt x.1)))

check-assignment: Produced bad assignment:

  y.1 and x.1 both assigned to r9

> (check-assignment
   '(begin ((locals (x.1 y.1))
            (conflicts ((x.1 (y.1)) (y.1 (x.1))))
            (assignment ((x.1 r9))))
           (set! x.1 42)
           (set! y.1 42)
           (set! x.1 (+ x.1 y.1))
           (halt x.1)))

check-assignment: Some locals not assigned homes:

  homeless locals: y.1

9.4 Undead and Conflict Analysis

For the next two passes, we switch perspectives and work top-down, as it’s difficult to explain them bottom-up.

We have just assumed that we know which variables are in conflict. This allowed us to make progress, but now now we need to discharge that assumption and build conflict graphs.

True Definition of Conflict Any variable that gets a new value during an instruction is in conflict with every variable that (1) has a different value and (2) will still be used after that instruction.

Unfortunately, due to Rice’s Theorem, we cannot compute this information. Rice’s Theorem is a huge buzz kill. It tells us, formally, that we cannot decide anything interesting about any Turing-complete program. The halting program is an instance of Rice’s Theorem. We cannot statically figure out the value of every variable; if we could, compiling would not be necessary. We also cannot know which variables are live. We can only ever approximate this information.

Loc-locals-lang is a simple enough language that we can tell whether a variable is dead or alive. Later, when we add new instructions, we will modify the undead analysis and find variables that aren’t necessarily live or dead, and must be assumed to be undead. This is also necessary if we want to handle linking, or separate compilation.

Instead, we assume we have information about which variables might be used after an instruction. We ignore their values, and approximate conflicts. We compute a conflict graph by creating a conflict between every variable that gets a new value and every variable that might be live and might have a different value.

To figure out conflicts, we’ll need to figure out which variables might be live, or, are not definitely dead.

9.4.1 Undeadness Analysis

A variable (with a particular value) that definitely will be used is considered live, and variable (with a particular value) that definitely won’t be used is considered dead. Due to Rice’s Theorem, we know it’s generally impossible to tell whether a variable is dead or alive. This means that when writing an analysis, we must assume partial knowledge. We ignore liveness entirely.

We also cannot know the values of variables. Instead, we focus on assignments to variables, which change the values of variables.

We assume that any variable that gets used, or might get used, might not be dead, i.e., we assume it is undead, and consider a variable dead only when we have conclusive proof—like witnessing an instruction driving a new value through its heart.

We collect the undead variables into sets. The undead-out set of an instruction is a set of (aloc ...). The set represents the variables that, after executing the instruction associated with the set, are undead.

To determine whether a variable is in the undead-out set for an instruction, we analyze the program by looping over the instruction sequence backwards, starting with the last statement. We don’t need to analyze the program backwards, but it’s faster. The loop takes an instruction and its undead-out set. We analyze each instruction with its undead-out set and compute an undead-in set for the instruction, which is the same as the undead-out set of the preceding instruction. That is, the undead-in set for an instruction s_i is the undead-out set for the instruction s_{i-1} in the program.

Each iteration of the loop performs the following analysis on a particular instruction. We start making by assuming the undead-in set is the same as the undead-out set, then update it depending on what happens in the instruction. If a variable is used in the instruction, it ought to be livewe don’t actually know, but it’s acting like its liveand is added to the undead-in set. If a variable is assigned, i.e., its value is overwritten, in the instruction, it is definitely dead at that point and removed from the undead-in set.

To start the loop, we need a default undead-out set for the last instruction; the default undead-out set for Loc-locals-lang is empty. In general, the default set may not be empty, because we may have to assume that some values are live after the program. For example, in Paren-x64, we assume rax is live out.

The analysis collects the undead-out sets for each instructions so that later passes can associate each instruction with its undead-out set. There are many ways to associate the undead-out sets with instructions. A simple way is to create a data structure that maps each set to an instruction.

Since our programs are simple lists of instructions, a list of sets is a good representation of the undead-out sets. The first element of the list represents undead-out set for the first instruction, the second element represents the undead-out set for the second instruction, and so on. A list (undead-out-set? ...) together with a list of instructions (s ...) can be traversed together using the template for for two lists simultaneously:
(define (fn-for-ss-and-undead-outs ss undead-outs)
  (match (cons ss undead-outs)
    [(cons '() '())
     (... case-for-empty ...)]
    [(cons `(,s ,rest-ss ...) `(,undead-out ,rest-undead-outs ...))
     (... (fn-for-s-and-undead-out s undead-out)
          (fn-for-ss-and-undead-outs rest-ss rest-undead-outs))]))

Most compilers call these live-out or live-after sets. This suggests that variables are definitely alive, and that the analysis is computing sets of variables that are definitely alive, neither of which is true. Undead is not exactly the same as not-definitely-dead, except in horror movies and video games, but it’s more suggestive of reality for compilers.

We collect the undead info in a new info field. Below, we design Undead-loc-lang. The language is essentially the same as Conflict-lang except for the info field.

  p ::= (begin info s ...)
     
  info ::= 
((locals (aloc ...))
 (undead-out ((aloc ...) ...))
 any ...)
     
  s ::= (set! aloc int64)
  | (set! aloc aloc)
  | (set! aloc (binop aloc aloc))
  | (halt aloc)
     
  binop ::= *
  | +

Exercise 3: Design and implement the function undead-analysis, which decorates a program with its list of undead-in sets; only the info field of the program is modified. The source language is Loc-locals-lang and the target language is Undead-loc-lang.

Examples:
> (undead-analysis
   '(begin ((locals (x.1)))
           (set! x.1 42)
           (halt x.1)))

'(begin ((locals (x.1)) (undead-out ((x.1) ()))) (set! x.1 42) (halt x.1))

> (undead-analysis
   '(begin ((locals (a.1 b.2 c.3 d.4 e.5)))
      (set! a.1 1)
      (set! c.3 2)
      (set! b.2 a.1)
      (set! b.2 (+ a.1 c.3))
      (set! d.4 (* b.2 c.3))
      (halt d.4)))

'(begin

   ((locals (a.1 b.2 c.3 d.4 e.5))

    (undead-out ((a.1) (a.1 c.3) (a.1 c.3) (c.3 b.2) (d.4) ())))

   (set! a.1 1)

   (set! c.3 2)

   (set! b.2 a.1)

   (set! b.2 (+ a.1 c.3))

   (set! d.4 (* b.2 c.3))

   (halt d.4))

> (undead-analysis
   '(begin ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1)))
       (set! v.1 1)
       (set! w.2 46)
       (set! x.3 v.1)
       (set! p.1 7)
       (set! x.3 (+ x.3 p.1))
       (set! y.4 x.3)
       (set! p.1 4)
       (set! y.4 (+ y.4 p.1))
       (set! z.5 x.3)
       (set! z.5 (+ z.5 w.2))
       (set! t.6 y.4)
       (set! p.1 -1)
       (set! t.6 (* t.6 p.1))
       (set! z.5 (+ z.5 t.6))
       (halt z.5)))

'(begin

   ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1))

    (undead-out

     ((v.1)

      (v.1 w.2)

      (x.3 w.2)

      (p.1 x.3 w.2)

      (x.3 w.2)

      (y.4 x.3 w.2)

      (p.1 y.4 x.3 w.2)

      (x.3 w.2 y.4)

      (w.2 z.5 y.4)

      (y.4 z.5)

      (t.6 z.5)

      (p.1 t.6 z.5)

      (t.6 z.5)

      (z.5)

      ())))

   (set! v.1 1)

   (set! w.2 46)

   (set! x.3 v.1)

   (set! p.1 7)

   (set! x.3 (+ x.3 p.1))

   (set! y.4 x.3)

   (set! p.1 4)

   (set! y.4 (+ y.4 p.1))

   (set! z.5 x.3)

   (set! z.5 (+ z.5 w.2))

   (set! t.6 y.4)

   (set! p.1 -1)

   (set! t.6 (* t.6 p.1))

   (set! z.5 (+ z.5 t.6))

   (halt z.5))

An important corner case to consider is what happens when unused variables appear in a program.

Examples:
> (undead-analysis
   '(begin ((locals (x.1 y.1)))
      (set! y.1 42)
      (set! x.1 5)
      (halt x.1)))

'(begin

   ((locals (x.1 y.1)) (undead-out (() (x.1) ())))

   (set! y.1 42)

   (set! x.1 5)

   (halt x.1))

> (undead-analysis
   '(begin ((locals (x.1 y.1)))
      (set! x.1 5)
      (set! y.1 42)
      (halt x.1)))

'(begin

   ((locals (x.1 y.1)) (undead-out ((x.1) (x.1) ())))

   (set! x.1 5)

   (set! y.1 42)

   (halt x.1))

In a realistic compiler, unused variables should be removed be an optimization.

Challenge 1: Design and implement the function bury-dead, which removes assignments to unused abstract locations. The source language is Undead-loc-lang and the target language is Undead-loc-lang. (Optimizations often have the same source and target language.)

An assignment to a variable is dead if it is not in the undead-out set for the instruction.

9.4.2 Conflict Analysis

Now, we can figure out which variables are in conflict.

We create a conflict graph from the undead-out sets as follows. Any variable that is defined during an instruction is in conflict with every variable (except itself) in the undead-out set associated with that instruction. For example, the variable x.1 is defined in (set! x.1 (+ x.1 x.2)), while the variables x.2 and x.1 are referenced. No variable can conflict with itself, since it always has the same value as itself. We must assume that all variables in the undead-out set have different values. We know that x.1 cannot be in the same physical location as any variables in the undead-out set. If x.2 is undead-out at this instruction, and we try to put x.1 and in the same physical location as x.2, then the value of x.2 would have been overwritten by the value of (+ x.1 x.2).

We can do slightly better, as one instruction does tell us that two values will be the same. A move instruction is an instruction that simply defines the value of one variables to be that of another, such as (set! x.1 x.2). We do not consider these two variables in conflicts. Even if x.2 is in the undead-out set, it’s value has not changed.

Approximation of Conflict
  • Any variable defined during a non-move instruction is in conflict with every variable (except itself) in the undead-out set associated with the instruction.

  • Any variable defined during a move instruction is in conflict with every variable in the undead-out set associated with the instruction, except itself and the variable referenced in the move.

Exercise 4: Design and implement the function conflict-analysis, which decorates a program with its conflict graph. The source language is Undead-loc-lang and the target language is Conflict-lang program.

For working with sets, you may want to use Sets.

Examples:
> (conflict-analysis
   '(begin ((locals (x.1))
            (undead-out ((x.1) ())))
      (set! x.1 42)
      (halt x.1)))

'(begin ((locals (x.1)) (conflicts ((x.1 ())))) (set! x.1 42) (halt x.1))

> (conflict-analysis
   '(begin ((locals (a.1 b.2 c.3 d.4 e.5))
            (undead-out ((a.1) (c.3 a.1) (c.3 a.1) (b.2 c.3) (d.4) ())))
      (set! a.1 1)
      (set! c.3 2)
      (set! b.2 a.1)
      (set! b.2 (+ a.1 c.3))
      (set! d.4 (* b.2 c.3))
      (halt d.4)))

'(begin

   ((locals (a.1 b.2 c.3 d.4 e.5))

    (conflicts ((e.5 ()) (d.4 ()) (c.3 (a.1 b.2)) (b.2 (c.3)) (a.1 (c.3)))))

   (set! a.1 1)

   (set! c.3 2)

   (set! b.2 a.1)

   (set! b.2 (+ a.1 c.3))

   (set! d.4 (* b.2 c.3))

   (halt d.4))

> (conflict-analysis
   '(begin ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1))
            (undead-out
             ((v.1)
              (v.1 w.2)
              (w.2 x.3)
              (p.1 w.2 x.3)
              (w.2 x.3)
              (y.4 w.2 x.3)
              (p.1 y.4 w.2 x.3)
              (y.4 w.2 x.3)
              (z.5 y.4 w.2)
              (z.5 y.4)
              (t.6 z.5)
              (t.6 z.5 p.1)
              (t.6 z.5)
              (z.5)
              ())))
           (set! v.1 1)
           (set! w.2 46)
           (set! x.3 v.1)
           (set! p.1 7)
           (set! x.3 (+ x.3 p.1))
           (set! y.4 x.3)
           (set! p.1 4)
           (set! y.4 (+ y.4 p.1))
           (set! z.5 x.3)
           (set! z.5 (+ z.5 w.2))
           (set! t.6 y.4)
           (set! p.1 -1)
           (set! t.6 (* t.6 p.1))
           (set! z.5 (+ z.5 t.6))
           (halt z.5)))

'(begin

   ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1))

    (conflicts

     ((p.1 (t.6 z.5 y.4 w.2 x.3))

      (t.6 (z.5 p.1))

      (z.5 (t.6 p.1 y.4 w.2))

      (y.4 (z.5 p.1 w.2 x.3))

      (x.3 (p.1 y.4 w.2))

      (w.2 (z.5 p.1 y.4 v.1 x.3))

      (v.1 (w.2)))))

   (set! v.1 1)

   (set! w.2 46)

   (set! x.3 v.1)

   (set! p.1 7)

   (set! x.3 (+ x.3 p.1))

   (set! y.4 x.3)

   (set! p.1 4)

   (set! y.4 (+ y.4 p.1))

   (set! z.5 x.3)

   (set! z.5 (+ z.5 w.2))

   (set! t.6 y.4)

   (set! p.1 -1)

   (set! t.6 (* t.6 p.1))

   (set! z.5 (+ z.5 t.6))

   (halt z.5))

9.5 Optimizing assign-homes

We can now assign registers for all Loc-locals-lang programs from last week. By assigning variables to registers, patch-instructions will be able to produce much faster code because it will not need to move values to and from memory on every single instruction.

Exercise 5: Define assign-homes-opt, which can be used as a drop-in replacement for assign-homes. The interface is the same as assign-home: the source language is Loc-locals-lang and the target language is Loc-assigned-lang.

Run your test suite and ensure you get the same outputs whether you use assign-homes or assign-homes-opt.

Examples:
> (define p
    '(begin ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1)))
       (set! v.1 1)
       (set! w.2 46)
       (set! x.3 v.1)
       (set! p.1 7)
       (set! x.3 (+ x.3 p.1))
       (set! y.4 x.3)
       (set! p.1 4)
       (set! y.4 (+ y.4 p.1))
       (set! z.5 x.3)
       (set! z.5 (+ z.5 w.2))
       (set! t.6 y.4)
       (set! p.1 -1)
       (set! t.6 (* t.6 p.1))
       (set! z.5 (+ z.5 t.6))
       (halt z.5)))
> (assign-homes-opt p)

'(begin

   ((assignment

     ((v.1 r15) (w.2 r12) (x.3 r13) (y.4 r14) (z.5 r13) (t.6 r14) (p.1 r15))))

   (set! v.1 1)

   (set! w.2 46)

   (set! x.3 v.1)

   (set! p.1 7)

   (set! x.3 (+ x.3 p.1))

   (set! y.4 x.3)

   (set! p.1 4)

   (set! y.4 (+ y.4 p.1))

   (set! z.5 x.3)

   (set! z.5 (+ z.5 w.2))

   (set! t.6 y.4)

   (set! p.1 -1)

   (set! t.6 (* t.6 p.1))

   (set! z.5 (+ z.5 t.6))

   (halt z.5))

> (assign-homes p)

'(begin

   ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1))

    (assignment

     ((v.1 (rbp - 8))

      (w.2 (rbp - 16))

      (x.3 (rbp - 24))

      (y.4 (rbp - 32))

      (z.5 (rbp - 40))

      (t.6 (rbp - 48))

      (p.1 (rbp - 56)))))

   (set! v.1 1)

   (set! w.2 46)

   (set! x.3 v.1)

   (set! p.1 7)

   (set! x.3 (+ x.3 p.1))

   (set! y.4 x.3)

   (set! p.1 4)

   (set! y.4 (+ y.4 p.1))

   (set! z.5 x.3)

   (set! z.5 (+ z.5 w.2))

   (set! t.6 y.4)

   (set! p.1 -1)

   (set! t.6 (* t.6 p.1))

   (set! z.5 (+ z.5 t.6))

   (halt z.5))

> (define (loc-assigned-execute f)
    (lambda (p)
      (parameterize ([current-pass-list
                      (list
                       f
                       replace-locations
                       patch-instructions
                       check-paren-x64
                       generate-x64
                       wrap-x64-boilerplate
                       wrap-x64-run-time)])
        (execute p))))
> (define loc-lang-execute-opt (loc-assigned-execute assign-homes-opt))
> (define loc-lang-execute (loc-assigned-execute assign-homes))
> (loc-lang-execute-opt p)

42

> (loc-lang-execute p)

42

Exercise 6: Create two versions of your compiler by defining two functions, compiler-a2 and compiler-a3. The source language should be Values-lang and the target should be x64-linux, represented as a string. compiler-a2 should use assign-homes, while compiler-a3 should use assign-home-opt instead. You should use parameterize and current-pass-list.

Write a paragraph comparing the two compilers. Consider the following questions:
  • What are benefits and the disadvantages of each?

  • Is one "optimal", and if so, in what sense?

  • Try running the same programs compiled with each compiler. Are there any surprises?