Skip to content

Commit b9e51a6

Browse files
committed
Draft of projects.
1 parent 9413e0e commit b9e51a6

1 file changed

Lines changed: 193 additions & 1 deletion

File tree

www/project.scrbl

Lines changed: 193 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,169 @@ In addition the source code for your project, you must write a 2-page
1818
document which gives a summary of your work and describes how your
1919
project is implemented.
2020

21+
@section{a86 optimizer}
22+
23+
Our compiler is designed to be simple and easy to maintain. That
24+
comes at the cost of emitting code that often does needless work.
25+
Write an a86 optimizer, i.e., a program that takes in a list of a86
26+
instructions and produces an alternative list of instructions that
27+
have the same behavior, but will execute more efficiently.
28+
29+
This is a fairly open-ended project, which means you can take a simple
30+
approach, or you can do a deep-dive on assembly code optimization and
31+
try to do something very sophisticated.
32+
33+
For a maximum of 95% of the possible points, your optimizer should
34+
work on any a86 instructions produced by the
35+
@seclink["Iniquity"]{Iniquity} compiler. For 100%, your optimizer
36+
should work on any a86 instructions produced by the
37+
@seclink["Loot"]{Loot} compiler.
38+
39+
The most important aspect of the optimizer is it must preserve the
40+
meaning of the original source program. If running a program with or
41+
without optimization can produce different results, you will lose
42+
significant points.
43+
44+
The second important aspect of the optimizer is that it produces more
45+
efficient code (but this should never come at the expense of
46+
correctness---otherwise it's trivial to optimize every program!). You
47+
should design some experiments demonstrating the impact of your
48+
optimizations and measure the performance improvement of your optimizer.
49+
50+
Here are some ideas for what you can optimize:
51+
52+
@itemlist[
53+
54+
@item{Avoid stack references where possible.
55+
56+
For example, you might push something and immediately reference it:
57+
@racket[(seq (Push _r1) (Mov _r2 (Offset rsp 0)))], which is
58+
equivalent to @racket[(seq (Push _r1) (Mov _r2 _r1))]. The
59+
register-to-register move will be faster than accessing the memory on
60+
the stack.}
61+
62+
@item{Avoid stack pushes where possible.
63+
64+
In the previous example, it may be tempting to delete the
65+
@racket[Push], but that is only valid if that stack element is not
66+
referenced later before being popped. And even if the element is not
67+
referenced, we have to be careful about how the element is popped.
68+
69+
But if you know where the pop occurs and there's no intervening
70+
references in to the stack or other stack changes, then you can
71+
improve the code further, e.g. @racket[(seq (Push _r1) (Mov _r2
72+
(Offset rsp 0)) (Add rsp 8))] can become @racket[(seq (Mov _r2 _r1))].
73+
}
74+
75+
@item{Statically compute.
76+
77+
Sometimes the compiler emits code for computing something at run-time
78+
which can instead be computed at compile time. For example, the
79+
compiler might emit @racket[(seq (Mov _r 42) (Add _r 12))], but this
80+
can be simplified to @racket[(seq (Mov _r 54))].}
81+
82+
]
83+
84+
There are many, many other kinds of optimizations you might consider.
85+
To get a sense of the opportunities for optimization, try compiling
86+
small examples and looking at the assembly code produces. Try
87+
hand-optimizing the code, then try to abstract what you did by hand
88+
and do it programmatically.
89+
90+
@section{Source optimizer}
91+
92+
Another complimentary approach to making programs compute more
93+
efficiently is to optimize them at the level of source code. Write a
94+
source code optimizer, i.e. a program that takes in a program AST and
95+
produces an alternative AST that has the same behavior, but will
96+
execute more efficiently.
97+
98+
This is another fairly open-ended project, which means you can take a
99+
simple approach, or you can do a deep-dive on source code optimization
100+
and try to do something very sophisticated.
101+
102+
For a maximum of 95% of the possible points, your optimizer should
103+
work for the @seclink["Iniquity"]{Iniquity} language. For 100%, your
104+
optimizer should work for the @seclink["Loot"]{Loot} language (or later).
105+
106+
The most important aspect of the optimizer is it must preserve the
107+
meaning of the original source program. If running a program with or
108+
without optimization can produce different results, you will lose
109+
significant points.
110+
111+
The second important aspect of the optimizer is that it produces more
112+
efficient code (but this should never come at the expense of
113+
correctness—otherwise it’s trivial to optimize every program!). You
114+
should design some experiments demonstrating the impact of your
115+
optimizations and measure the performance improvement of your
116+
optimizer.
117+
118+
Here are some ideas for where you can optimize:
119+
120+
@itemlist[
121+
122+
@item{Avoid variable bindings where possible.
123+
124+
Sometimes a program may bind a variable to a value, but then use the
125+
variable only once, e.g. @racket[(let ((x (add1 7))) (add1 x))]. We
126+
can instead replace the variable occurrence with it's definition to
127+
get: @racket[(add1 (add1 7))]. Note that can must be taken to
128+
@emph{not} do this optimization if it changes the order in which
129+
effects may happen. For example, consider
130+
131+
@racketblock[
132+
(let ((x (read-byte)))
133+
(begin (read-byte)
134+
(add1 x)))
135+
]
136+
137+
This is not the same as:
138+
139+
@racketblock[
140+
(begin (read-byte)
141+
(add1 (read-byte)))
142+
]
143+
144+
because the latter adds one to the second byte of the input stream rather than the first.}
145+
146+
@item{Statically compute.
147+
148+
Sometimes parts of a program can be computed at compile-time rather
149+
than run-time. For example, @racket[(add1 41)] can be replaced with
150+
@racket[42]. Likewise, expressions like @racket[(if #f _e1 _e2)] can
151+
be replaced by @racket[_e2].}
152+
153+
@item{Inline function calls.
154+
155+
Suppose you have:
156+
157+
@racketblock[
158+
(define (f x) (add1 x))
159+
(if (zero? (f 5)) _e1 _e2)
160+
]
161+
162+
Since the expression @racket[(f 5)] is calling a known function, you
163+
should be able to transform this call into @racket[(let ((x 5)) (add1
164+
x))]. Using the previously described optimization, you can further
165+
optimize this to @racket[(add1 5)], which in turn can be simiplified
166+
to @racket[6]. You can keep going and notice that @racket[(zero? 6)]
167+
is just @racket[#f], so the whole program can be simplified to:
168+
169+
@racketblock[
170+
(define (f x) (add1 x))
171+
_e2
172+
]
173+
}
174+
175+
]
176+
177+
Note that the last example can get considerably more complicated in a
178+
language with first-class functions since it may not be possible to
179+
know statically which function is being called.
180+
181+
There are many other optimziations you might consider. Think about
182+
the kinds of expressions you might write and how they can be
183+
simplified, then figure out how to do it programmatically.
21184

22185
@section{Multiple return values}
23186

@@ -79,7 +242,29 @@ what the surrounding context expects, an error should be signalled.
79242

80243
]
81244

245+
The top-level expression may produce any number of values and the
246+
run-time system should print each of them out, followed by a newline:
247+
248+
@ex[
249+
(values 1 2 3)
250+
]
82251

252+
Note there is some symmetry here between function arity checking where
253+
we make sure the number of arguments matches the number of parameters
254+
of the function being called and the ``result arity'' checking that is
255+
required to implement this feature. This suggests a similiar approach
256+
to implementing this feature, namely designating a register to
257+
communicate the arity of the result, which should be checked by the
258+
surrounding context.
259+
260+
You will also need to design an alternative mechanism for
261+
communicating return values. Using a single register (@racket['rax])
262+
works when every expression produces a single result, but now
263+
expressions may produce an arbitrary number of results and using
264+
registers will no longer suffice. (Although you may want to continue
265+
to use @racket['rax] for the common case of a single result.) The
266+
solution for this problem with function parameters was to use the
267+
stack and a similar approach can work for results too.
83268

84269
@section{Exceptions and Exception Handling}
85270

@@ -360,4 +545,11 @@ Pushing your local repository to github ``submits'' your work. We
360545
will grade the latest submission that occurs before the deadline.
361546
}
362547

363-
}
548+
}
549+
550+
@section{Design your own}
551+
552+
You may also design your own project, however, you will need to submit
553+
a one-page write-up that documents what you plan to do and how you
554+
will evaluate whether it is successful. You must submit this document
555+
and have it approved by the instructor by Dec 7.

0 commit comments

Comments
 (0)