├── .gitignore ├── .vscode └── settings.json ├── README.md ├── lessons ├── 00-overview.md ├── 01-local-opt.md ├── 02-dataflow.md ├── 03-ssa.md ├── 04-loops.md ├── 05-memory.md ├── 06-interprocedural.md └── README.md ├── project.md ├── reading ├── beyond-induction-variables.md ├── braun-ssa.md ├── buildit.md ├── copy-and-patch.md ├── doop.md ├── ir-survey.md ├── lazy-code-motion.md ├── linear-scan-ssa.md ├── optimal-inlining.md └── sparse-conditional-constant-prop.md └── syllabus.md /.gitignore: -------------------------------------------------------------------------------- 1 | notes.md 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdown.validate.enabled": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CS 265: Compiler Optimization 2 | 3 | Welcome to CS 265, Fall 2024 edition! 4 | 5 | - Instructor: [Max Willsey](https://mwillsey.com) 6 | - Time: Tuesdays and Thursdays, 2:00pm-3:30pm 7 | - Location: Soda 405 8 | - Office hours: After class (TTh 3:30-4:30pm), 725 Soda 9 | 10 | This course uses my fork 11 | of the [bril compiler infrastructure](https://github.com/mwillsey/bril/). 12 | 13 | Some students have opted to make their projects public! 14 | [Check them out here!](./project.md#public-projects) 15 | 16 | ## Other Course Pages 17 | 18 | - [bCourses/Canvas](https://bcourses.berkeley.edu/courses/1538171) (enrollment required) 19 | - [Syllabus](./syllabus.md) 20 | - [Project Information](./project.md) 21 | - [Bril](https://github.com/mwillsey/bril/) (Max's fork) 22 | 23 | ## Schedule 24 | 25 | Schedule is under construction and subject to change. 26 | 27 | If the topic says "_OH, no class_", 28 | that means I will be available during the normal class time for office hours, 29 | in addition to the normal office hours after class. 30 | 31 | | Week | Date | Topic | Due | 32 | |-----:|--------|--------------------------------------------------------------------------------------------------------------|---------------------------------------------------------| 33 | | 1 | Aug 29 | [Overview](lessons/00-overview.md) | | 34 | | 2 | Sep 3 | [Local Optimizations](lessons/01-local-opt.md) | Course Survey ([bCourses][]) | 35 | | | Sep 5 | [Local Optimizations](lessons/01-local-opt.md) | | 36 | | 3 | Sep 10 | [Dataflow](lessons/02-dataflow.md) | | 37 | | | Sep 12 | _no class_ | | 38 | | 4 | Sep 17 | [Dataflow](lessons/02-dataflow.md) | [Task 1](lessons/01-local-opt.md#task) due | 39 | | | Sep 19 | ["Constant Propagation with Conditional Branches"](./reading/sparse-conditional-constant-prop.md) | Reading reflection ([bCourses][]) | 40 | | 5 | Sep 24 | [SSA](lessons/03-ssa.md) | | 41 | | | Sep 26 | ["Simple and Efficient Construction of Static Single Assignment Form"](./reading/braun-ssa.md) | Reading reflection ([bCourses][]) | 42 | | 6 | Oct 1 | [Loops](lessons/04-loops.md) | [Task 2](lessons/02-dataflow.md#task) due | 43 | | | Oct 3 | ["Lazy Code Motion"](./reading/lazy-code-motion.md) | Reading reflection ([bCourses][]) | 44 | | 7 | Oct 8 | [Loops](./lessons/04-loops.md#induction-variables) | | 45 | | | Oct 10 | ["Beyond Induction Variables"](./reading/beyond-induction-variables.md) | Reading reflection ([bCourses][]) | 46 | | 8 | Oct 15 | [Memory](./lessons/05-memory.md) | [Task 3](lessons/04-loops.md#task) due | 47 | | | Oct 17 | ["Strictly Declarative Specification of Sophisticated Points-to Analyses"](./reading/doop.md) | Reading reflection ([bCourses][]) | 48 | | 9 | Oct 22 | [Interprocedural Optimization](./lessons/06-interprocedural.md) | | 49 | | | Oct 24 | ["Understanding and exploiting optimal function inlining"](./reading/optimal-inlining.md) | Reading reflection ([bCourses][]) | 50 | | 10 | Oct 29 | _OH, no class_ | [Task 4](lessons/05-memory.md#task) due | 51 | | | Oct 31 | ["Intermediate Representations in Imperative Compilers: A Survey"](./reading/ir-survey.md) | Reading reflection ([bCourses][]) | 52 | | 11 | Nov 5 | _Election day, no class_ | [Project Proposals](./project.md#project-proposals) | 53 | | | Nov 7 | ["BuildIt: A Type-Based Multi-stage Programming Framework for Code Generation in C++"](./reading/buildit.md) | Reading reflection ([bCourses][]) | 54 | | 12 | Nov 12 | _no class_ | | 55 | | | Nov 14 | ["Copy-and-patch compilation"](./reading/copy-and-patch.md) | Reading reflection ([bCourses][]) | 56 | | 13 | Nov 19 | _no class, extra OH_ | | 57 | | | Nov 21 | ["Linear Scan Register Allocation on SSA Form"](./reading/linear-scan-ssa.md) | Reading reflection ([bCourses][]) | 58 | | 14 | Nov 26 | _no class, no OH_ | [Project Check-ins due](./project.md#project-check-ins) | 59 | | | Nov 28 | _no class, no OH_ | | 60 | | 15 | Dec 3 | _no class_ | | 61 | | | Dec 5 | " | | 62 | | 16 | Dec 10 | _RRR, no class_ | | 63 | | | Dec 12 | " | | 64 | | 17 | Dec 17 | _Finals Week_ | [Project Reports](./project.md#project-report) | 65 | 66 | ## Resources 67 | 68 | These notes are by no means meant to be a comprehensive resource for the course. 69 | Here are some other resources 70 | (all freely available online) 71 | that you may find helpful. 72 | I have looked at many of these resources in preparing this class, 73 | and I recommend them for further reading. 74 | 75 | - Other courses 76 | - CMU's 77 | [15-411](https://www.cs.cmu.edu/~fp/courses/15411-f14/schedule.html) by Frank Pfenning (notes); 78 | [15-745](http://www.cs.cmu.edu/afs/cs/academic/class/15745-s19/www/syllabus.html) by Phil Gibbons (slides) 79 | - Cornell's [CS 6120](https://www.cs.cornell.edu/courses/cs6120/) 80 | by Adrian Sampson (videos) 81 | - Stanford's [CS 243](https://suif.stanford.edu/~courses/cs243/) 82 | by Monica Lam (slides) 83 | - The book _[Static Program Analysis](https://cs.au.dk/~amoeller/spa/)_ by Anders Møller and Michael I. Schwartzbach 84 | (available online for free). 85 | - The survey paper "[Compiler transformations for high-performance computing](https://dl.acm.org/doi/10.1145/197405.197406)" (1994) 86 | for a comprehensive look at many parts of an optimizing compiler, especially loop transformations. 87 | 88 | [bCourses]: https://bcourses.berkeley.edu/courses/1538171 -------------------------------------------------------------------------------- /lessons/00-overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Welcome to CS 265! 4 | 5 | See the [syllabus](../syllabus.md) for logistics, policies, and course schedule. 6 | 7 | ## Course Goals 8 | 9 | This is an advanced compilers class targeted at students who have experience building some level of compilers or interpreters. 10 | We will focus on the "mid-end" of the compiler, which operates over an intermediate representation (IR) of the program. 11 | The lines between front/mid/back-end don't have to be totally stark. 12 | A compiler may have more than one IR (sometimes many, see the [Nanopass Compiler Framework](https://nanopass.org/)). 13 | For the purpose of this class, let's define the "ends" of a compiler as follows. 14 | 15 | - Front-end 16 | - Responsible for ingesting surface syntax, processing it, and translating it into an intermediate representation suitable for analysis and optimization 17 | - Examples: lexing, parsing, type checking, macro/template processing, elaborating language features into a reduced set of features. 18 | - Mid-end 19 | - Many modern compilers include a "mid-end" portion of the compiler where analysis and optimization is focused. 20 | - The goal of the mid-end is to present a reduced surface area to make compiler tasks easier to work with. 21 | - Mid-ends are typically agnostic to details about the target machine. 22 | - Back-end 23 | - The back-end is responsible for lowering the compiler's intermediate representation into something the target machine can understand. 24 | - This layer is typically per target machine, so it will refer to specifics about the target machine architecture. 25 | - Examples: instruction selection, legalization, register allocation, instruction scheduling. 26 | 27 | We will focus on the mid-end, 28 | and we'll touch on some aspects of the backend. 29 | Compiler front-ends are not really going to be covered in this course. 30 | You are free to incorporate any part of the compiler in your course [project](../project.md). 31 | 32 | ### Quick note: what's an optimization? 33 | 34 | As the focus of this class is implementing compiler optimizations, 35 | it's worth defining what an optimization is. 36 | In most other contexts, optimization is a process of finding the best solution to a problem. 37 | In compilers, 38 | an optimization is more like an embetterment; 39 | it makes things "better" in some way 40 | but we often don't make any guarantees about how much better it is (or if it's better at all!) 41 | or how far from the best solution we are. 42 | In other words, 43 | compiler optimizations are typically thought of as "best effort". 44 | (Though there is the topic of superoptimization, which is a search for the best solution.) 45 | 46 | Okay, so what's better? 47 | For our purposes in this class, we'll define "better" as "faster". 48 | And we'll define "faster" as "fewer instructions executed". 49 | But of course, this definition of "faster" misses 50 | many details that will affect the actual runtime of a program 51 | on real hardware 52 | (e.g., cache locality, branch prediction, etc.). 53 | We will touch on those topics, 54 | but they are not the focus of this class 55 | as we are running our code on a simple virtual machine. 56 | Faster may not be the only thing you care about as well. 57 | You may care about code size, 58 | or memory usage, 59 | or power consumption, 60 | or any number of other things. 61 | We will touch on some of these topics as well, 62 | but the course infrastructure is designed 63 | to focus on the "fewer instructions executed" metric, 64 | as well as code size. 65 | 66 | Also important, optimizations better preserve.... something! 67 | But what? 68 | The virtual machine in this class runs your program 69 | and not only returns the result of the program, 70 | but also returns some statistics about the program and its execution. 71 | It's fair to say those are part of the interpreters semantics. 72 | Do you want to preserve the result of the program? 73 | So what's worth preserving? 74 | Typically this discussion is framed in terms of "observable behavior". 75 | This is useful, 76 | since it captures the idea of preserving important side effects of the program. 77 | For example, 78 | if you are optimizing an x86 program, 79 | you probably want to preserve not only its result but its effect on the status registers, 80 | memory, and other parts of the machine state that you consider "observable". 81 | But you might not care about the cache state, 82 | branch prediction state (or maybe you do care about this in a security context!), 83 | etc. 84 | 85 | Even the notion of preservation is not sufficient in many contexts. 86 | Instead of a symmetric notion of equivalence, 87 | we may need to a _directional_ notion of equivalence. 88 | Consider the following example: 89 | how would you relate `x / x` to `1`? 90 | Are they equivalent? No! 91 | Well, what are they then? 92 | It goes back to your notion of "observable behavior". 93 | If you consider division by zero to be an observable behavior, 94 | then `x / x` is not equivalent to `1`; 95 | unless you can prove that `x` is not zero via some analysis. 96 | If you consider division by zero to be undefined behavior (like LLVM), 97 | then `x / x` is _still_ not equivalent to `1`. 98 | Why not? 99 | Well, you certainly would never want to replace 100 | the latter with the former, 101 | since that's clearly making the program "less defined" in some sense. 102 | In these cases, 103 | we could say that `1` _refines_ `x / x`. 104 | Refinement is a transitive, reflexive, but _not_ symmetric relation. 105 | See this [blog post on Alive2](https://blog.regehr.org/archives/1722) 106 | for more info. 107 | 108 | For this class, 109 | we will mostly punt on these (important!) issues 110 | to aid you getting started as soon as possible. 111 | We will mostly focus on executing fewer instructions, 112 | and we will look at code size as well. 113 | In terms of preserving behavior, 114 | we will mostly care about preserving the result of the program. 115 | Our programs "return" their result via printing, 116 | so we will have to care about effects like order of print statements. 117 | Along the way, 118 | we will have to care about preserving effects like the state of memory. 119 | On other effects like crashing, 120 | you may decide to preserve them or not. 121 | 122 | 123 | ## Topics Overview 124 | 125 | Here are some of the planned topics for the course. 126 | 127 | ### Representing Programs 128 | 129 | #### ASTs 130 | 131 | There are many ways to represent a program! 132 | You are probably familiar with the abstract syntax tree (AST), 133 | as that is a common representation many tools that interact with programs use. 134 | 135 | There are many design decisions that go into choosing an AST representation. 136 | These might affect memory layout, adding annotations to the tree, 137 | or the ability reconstruct the concrete syntax from the tree. 138 | 139 | Designing and working with ASTs is common and important! 140 | But it is not the focus of this class. 141 | I'll do that part for you, 142 | so you can focus on working with the IR. 143 | 144 | #### IRs 145 | 146 | There's no formal definition of what an intermediate representation (IR) is; 147 | it's whatever is after the front-end and before the back-end. 148 | Typically, IRs follow a few principles: 149 | - They typically make explicit things like typing, sizing of values, and other 150 | details that might be implicit in the source language. 151 | - They may reduce the number of language constructs to a smaller set. 152 | - For example, the front-end may support `if`s, `while`s, and `for`s, but the IR may only support `goto`s. 153 | - They may also normalize the program into some form that is easier to analyze. 154 | - We will study Static Single Assignment (SSA) form later in class. 155 | 156 | Very roughly, 157 | IRs could be classified as either **linear** or **graph-based**. 158 | 159 | A linear IR is one where the program is represented as a sequence of instructions. 160 | These instructions 161 | use and define values, 162 | and they mostly "run" one after the other, 163 | with some control flow instructions to jump around. 164 | These IRs include some notion of a virtual machine that they run on: 165 | the Python and WebAssembly virtual machines are stack-based, 166 | the machine model for other IRs including LLVM and Cranelift are register-based. 167 | Some, like the JVM, are a mix of both. 168 | The virtual machine gives the IR an operational semantics. 169 | 170 | A graph-based IR is one where the program is represented as a graph, of course. 171 | The nodes in the graph represent values (or instructions), 172 | and the edges represent how values flow from one to another. 173 | A simple dataflow graph is of course a graph-based IR, 174 | but it is limited to only representing computation of values. 175 | More complex graph-based IRs can represent control flow as well, 176 | sometimes by including a machine model. 177 | Sometimes they do not require a machine model and can be denotationally defined. 178 | A canonical graph-based IR 179 | is the [Program Dependence Graph](https://dl.acm.org/doi/10.1145/24039.24041), 180 | and many IRs that it inspired 181 | including 182 | [Sea of Nodes](https://www.oracle.com/technetwork/java/javase/tech/c2-ir95-150110.pdf), 183 | [Program Expression Graphs](https://dl.acm.org/doi/10.1145/1480881.1480915), 184 | and 185 | [RVSDGs](https://dl.acm.org/doi/abs/10.1145/3391902). 186 | 187 | 188 | Many IRs will mix these paradigms by grouping sets of instructions into blocks, 189 | which are then organized into a graphically into a _control flow graph_. 190 | In this model, 191 | the point is the group instructions into a block that can be reasoned about 192 | in a restricted way. 193 | The blocks are then organized into the outer graph that captures how the blocks are connected. 194 | 195 | In this class, we will focus on a "typical" IR that groups instructions into _basic blocks_. 196 | The SSA form interacts with blocks in a particular way, 197 | so we will spend some time understanding how to convert a program into SSA form. 198 | If you are familiar with SSA form, 199 | you may know the $\phi$-function, which is a way to merge values from different control flow paths. 200 | Some IRs instead use _block arguments_ as a way to pass values between blocks. 201 | This approach is taken in some modern compilers 202 | like [Cranelift](https://github.com/bytecodealliance/wasmtime/blob/main/cranelift/docs/ir.md), 203 | [MLIR](https://mlir.llvm.org/docs/LangRef/#blocks), 204 | and [Swift](https://github.com/swiftlang/swift/blob/main/docs/SIL.rst#basic-blocks). 205 | If you aren't familiar with SSA form, 206 | $\phi$-functions, or block arguments, 207 | don't worry! 208 | We will cover them in coming lessons. 209 | 210 | ### Local Optimizations 211 | 212 | We'll start by looking at local optimizations. 213 | These are local in the sense that they operate on a single basic block at a time, 214 | i.e., they don't reason about control flow. 215 | 216 | These include optimizations like constant folding, copy propagation, and dead code elimination. 217 | We'll learn about value numbering and how it can subsume these optimizations. 218 | 219 | ### Transformation 220 | 221 | As part of local optimizations, 222 | you'll probably add some _peephole optimizations_, 223 | a classic family of optimizations that look at a small set of instructions 224 | and replace them with something "better". 225 | 226 | Examples of peephole optimizations include: 227 | ``` 228 | x * 2 -> x << 1 229 | x + 0 -> x 230 | ``` 231 | 232 | This a big part of many compilers, 233 | so we will also discuss various frameworks to implement and reason about 234 | these optimizations. 235 | You may choose to implement your optimization in such a framework, 236 | or you may choose to implement transformations directly. 237 | 238 | ### Static Analysis 239 | 240 | We will quickly learn that many optimizations depend on knowing something about the program. 241 | Much of the infrastructure in modern compilers is dedicated to proving facts about the program 242 | rather than transforming it. 243 | 244 | We will learn about dataflow analysis, pointer analysis, and other static analysis techniques 245 | that can be used to prove facts about the program. 246 | Like transformations, 247 | these techniques can also be encompassed by some frameworks. 248 | We will read about some of these frameworks, 249 | and you may choose to use them in your work or implement the analyses directly. 250 | 251 | ### Loop Optimizations 252 | 253 | Many optimizations in compilers focus on loops, 254 | as they execute many times and are a common source of performance bottlenecks. 255 | We will study some loop optimizations, 256 | but some will not be implementable in the course framework. 257 | Many loop optimizations are focused on improving cache locality, 258 | such as loop interchange and loop tiling. 259 | Our virtual machine does not model the cache, 260 | so these optimizations will not be measurable in the course framework. 261 | 262 | ### Other Stuff 263 | 264 | Depending on time and interest, 265 | we may cover some other topics. 266 | I would like to do something in the realm of automatic parallelization, 267 | either automatic vectorization or via a GPU-like SIMT model. 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /lessons/01-local-opt.md: -------------------------------------------------------------------------------- 1 | # Local Optimization 2 | 3 | Further reading: 4 | - Rice's [COMP512](https://www.clear.rice.edu/comp512/Lectures/02Overview1.pdf) 5 | - Cornell's [CS 6120](https://www.cs.cornell.edu/courses/cs6120/2023fa/lesson/3/) 6 | - CMU's [15-745](https://www.cs.cmu.edu/afs/cs/academic/class/15745-s19/www/lectures/L3-Local-Opts.pdf) 7 | - ["Value Numbering"](https://www.cs.tufts.edu/~nr/cs257/archive/keith-cooper/value-numbering.pdf), Briggs, Cooper, and Simpson 8 | 9 | This assigment has an associated [task](#task) you should begin after reading this lesson. 10 | 11 | ## Dead code elimination 12 | 13 | Let's start with (trivial, global) dead code elimination as a motivating example. 14 | 15 | "Dead code" is code that doesn't affect the "output" of a program. 16 | Recall our discussion from [lesson 0](00-overview.md) 17 | about the "obervability" of the "output" of a program 18 | (sorry about all the scare quotes). 19 | In fact, 20 | we'll see today that the output of a (part of a) program 21 | is a tricky concept to pin down, 22 | and we'll constrain ourselves to local optimizations 23 | for now. 24 | 25 | Let's start with a simple example: 26 | 27 | ```c 28 | int foo(int a) { 29 | int x = 1; 30 | int y = 2; 31 | x = x + a; 32 | return x; 33 | } 34 | ``` 35 | 36 | Typically, 37 | we won't be working with the surface syntax of a program, 38 | we'll be working with a lower-level intermediate representation (IR). 39 | Most (including the one we'll use) 40 | are oriented around a sequence of instructions. 41 | 42 | Here's the same code in instruction form: 43 | ``` 44 | x = 1 45 | y = 2 46 | x = x + a 47 | return x 48 | ``` 49 | 50 | In this example, the assignment to variable `y` is dead code. 51 | It doesn't affect the output of the program, 52 | which is here clearly constrained as a straight line function 53 | that outputs a single value `x`. 54 | 55 | Let's make things a bit more interesting: 56 | 57 | ```c 58 | int foo(int a) { 59 | int x = 1; 60 | int y = 2; 61 | if (x > 0) { 62 | y = x; 63 | } 64 | x = x + a; 65 | return x; 66 | } 67 | ``` 68 | 69 | And in instruction form: 70 | ``` 71 | x = 1 72 | y = 2 73 | c = x > 0 74 | branch c L1 L2 75 | L1: 76 | y = x 77 | L2: 78 | x = x + a 79 | return x 80 | ``` 81 | 82 | Can you think of a way to eliminate the dead code at label `L1`? 83 | 84 | Let's start by defining what we mean by "dead code" 85 | a bit more precisely so as to avoid the observability issue. 86 | For now, an instruction is dead if both 87 | - it is pure (has no side effects other than its assignment) 88 | - its result is never used 89 | 90 | For today, 91 | we'll only look at instructions that are pure; 92 | so no calls, no stores, etc. 93 | Handling those will pose a challenge to the algorithms 94 | we'll see today and we'll defer them to later. 95 | 96 | It remains to show that a variable is never used. 97 | A conservative approach (overapproximation is theme we'll see a lot) 98 | is to say that a variable is used if 99 | there is no instruction that uses it. 100 | 101 | Here's a simple algorithm to eliminate dead code 102 | based on this definition: 103 | ```py 104 | used = {} 105 | for instr in func: 106 | used += instr.args 107 | 108 | for instr in func: 109 | if instr.dest not in used and is_pure(instr): 110 | del instr 111 | ``` 112 | 113 | Great! We've eliminated the dead code in the above example. 114 | 115 | Can you think of some examples where this algorithm would fail 116 | to eliminate all the dead code? 117 | 118 | Here are some different ways our code might be incomplete: 119 | 1. There's more dead code! This can be resolved by iterating until convergence. 120 | ``` 121 | a = 1 122 | b = 2 123 | c = a + a 124 | d = b + b 125 | return c 126 | ``` 127 | 2. There's a "dead store" (a variable is assigned to but never used). 128 | ``` 129 | a = 1 130 | a = 2 131 | return a 132 | ``` 133 | 3. A variable is used _somewhere_, but in a part of the code that won't actually run. 134 | ```c 135 | int a = 1; 136 | int b = ...; 137 | if (a == 0) { 138 | use(b); 139 | } 140 | ``` 141 | 142 | We handled the first case by simply iterating our algorithm. 143 | The third case is a bit more challenging, and we'll see how to handle it later in the course. 144 | 145 | How about the second case? 146 | The third case hints that reasoning about control flow is important. 147 | So perhaps we can make our lives easier by reasoning not doing that! 148 | It's clear that the example in 2 is "easy" in some sense 149 | because we are looking at straight-line code. 150 | So let's try to make our lives easier by looking _only_ at straight-line code, 151 | even in the context of a larger program. 152 | 153 | ## Control flow graphs 154 | 155 | We'd like to reason about straight-line code whenever possible. 156 | But even simple programs have instructions that break up this straight-line flow. 157 | 158 | A control flow graph (CFG) is a way to represent the flow of control in a program. 159 | It's a directed graph where the nodes 160 | are "atomic" blocks of code 161 | the edges represent control flow between them. 162 | 163 | One way to construct a CFG is to place every instruction in its own block. 164 | Blocks are connected by edges 165 | to the instructions that might follow them. 166 | A simple assignment instruction 167 | will have one _successor_, 168 | one out-going edge to the next instruction. 169 | A jump will also have one successor, to the target of the jump. 170 | A branch will have two successors, one for each branch. 171 | 172 | Blocks in a control flow graph are defined by a 173 | "single-entry, single-exit" property. 174 | That is, 175 | there is only one point at which to enter the block (the top) 176 | and one point at which to exit the block (the bottom). 177 | In other words, 178 | if any part of a block is executed, 179 | the whole block is executed. 180 | 181 | When we talk about basic blocks, 182 | in a CFG, 183 | we typically mean _maximal_ basic blocks. 184 | These are blocks that are as large as possible 185 | while still maintaining the single-entry, single-exit property. 186 | In our instruction-oriented IR, 187 | this means that a basic block is a sequence of instructions 188 | that may end with a _terminator_ instruction: a jump, branch, or return instruction (the exit point). 189 | Terminators may _not_ appear in the middle of a basic block. 190 | 191 | Here's our example from before again: 192 | ``` 193 | x = 1 194 | y = 2 195 | c = x > 0 196 | branch c L1 L2 197 | L1: 198 | y = x 199 | L2: 200 | x = x + a 201 | return x 202 | ``` 203 | 204 | In this example, the basic blocks are the entry block, `L1`, and `L2`. 205 | Why can't `L1` be combined with `L2`? 206 | After all, `L1` only has one successor, can't it be folding into `L2`? 207 | No, that would violate the single-entry property on the resulting block. 208 | We want to guarantee that all of the instructions in a block are executed together or not at all. 209 | 210 | ## Local Dead Code Elimination 211 | 212 | Now that we are equipped with control flow graphs 213 | with maximal basic blocks, 214 | we can focus on it as a unit of analysis. 215 | 216 | Recall our problem case for dead code elimination: 217 | ``` 218 | a = 1 219 | a = 2 220 | return a 221 | ``` 222 | 223 | Our global algorithm failed to eliminate the dead code (the first assignment to `a`) in this case. 224 | 225 | 226 | ```py 227 | unused: {var: inst} = {} 228 | for inst in block: 229 | # if it's used, it's not unused 230 | for use in inst.uses: 231 | del unused[use] 232 | if inst.dest 233 | # if this inst defines something 234 | # we can kill the unused definition 235 | if unused[inst.dest]: 236 | remove unused[inst.dest] 237 | # mark this def as unused for now 238 | unused[inst.dest] = inst 239 | ``` 240 | 241 | Be careful to process uses before defs (in a forward analysis like this one). 242 | Otherwise, instructions like `a = a + 1` will be cause problems. 243 | 244 | Note that we still have to iterate until convergence. 245 | 246 | Is this as good as our global algorithm above? 247 | Certainly it's better in some use cases (like the "dead store" example). 248 | 249 | No! Consider this case with clearly dead instruction, but one that isn't "clobbered" by a later instruction: 250 | 251 | ```c 252 | int foo(int a) { 253 | if (a > 0) { 254 | int x = 1; 255 | } 256 | return 0 257 | } 258 | ``` 259 | 260 | So you need both for now. 261 | 262 | ## Local Value Numbering 263 | 264 | Value numbering is a very general optimization 265 | that be extended implement a number of other optimizations, including: 266 | - Copy propagation 267 | - Common subexpression elimination 268 | - Constant propagation 269 | - Constant folding 270 | - Dead code elimination 271 | - This part is a little tricky, we'll see why later. 272 | 273 | How does LVN accomplish all of this? 274 | These optimizations can be see as reasoning about the "final value" 275 | of the block of code, 276 | and figuring out a way to compute that value with fewer instructions. 277 | The problem with reasoning about the "final value" is that 278 | the instruction-based IR doesn't make it easy to see what the "final value" is. 279 | 280 | - Problem 1: variable name obscure the values being used 281 | - graphs can help with this, we will see this later in the course. 282 | - "clobbered" variables will make things tricky 283 | - we are stuck here for now, but we will look at SSA form later in the course 284 | - LVN's approach, "run" the program and see what values are used! 285 | - build a sort of a symbolic state that keeps track of the values of variables 286 | - Problem 2: we don't know what variables will be used later in the program 287 | - later lecture for non-local value numbering 288 | 289 | Here is some pseudocode for a local value numbering pass. 290 | Note that this is avoiding important edge cases related to clobbered variables! 291 | Think about how you'd handle those cases. 292 | 293 | ```py 294 | # some times all represented as one big table 295 | # here the 4 maps force you to think about the uniqueness requirements of the keys 296 | val2num: dict[value, num] = {} 297 | num2val: dict[num, value] = {} 298 | var2num: dict[var, num] = {} 299 | num2var: dict[num, var] = {} 300 | 301 | def add(value) -> num: 302 | # do the value numbering in val2num, num2val 303 | 304 | for inst in block: 305 | # canonicalize the instruction's arguments by getting the 306 | # value numbers currently held by those vars 307 | args = [var2num[arg] for arg in inst.args] 308 | 309 | # create a new value by packaging this instruction with 310 | # the value numbers of its arguments 311 | value = [inst.op] + args 312 | 313 | # look up the value number of this value 314 | num = val2num.get(value) 315 | 316 | if num is None: 317 | # we've never seen this value before 318 | num = add(value) 319 | else: 320 | # we've seen this value before 321 | # replace this instruction with an id 322 | inst.op = "id" 323 | inst.args = [num2var[num]] 324 | 325 | if inst.dest in var2num: 326 | # be careful here, what if dest is already in var2num? 327 | # one option is to introduce temp variables 328 | # another, more conservative option is to remove the overwritten value 329 | # from all of the maps and point it to the new value 330 | else: 331 | var2num[inst.dest] = value 332 | num2var[num] = inst.dest 333 | ``` 334 | 335 | In this approach, 336 | we are reconstructing each instruction based on the state at that time. 337 | Not waiting until the end and reconstruction a whole program fragment. 338 | One consequence of this is that we can't do DCE (we don't know what vars are used later). 339 | in fact we will introduce a bunch of temp vars that might be dead. 340 | So this instance of LVN should be followed by a DCE pass. 341 | 342 | Example: 343 | ``` 344 | a = 1 345 | b = 2 346 | c = 3 347 | sum1 = a + b 348 | sum2 = a + b 349 | prod = sum1 * sum2 350 | return prod 351 | ``` 352 | 353 | ### Extensions of LVN 354 | 355 | #### Stronger lookup 356 | 357 | The important part of value numbering is when you "lookup" to see if you've seen a value before. 358 | A typical approach is to use a hash table to see if you've done the same computation before. 359 | But determining if two computations are the same can be done in a much less conservative way! 360 | 361 | You could add things like commutativity, or even other properties of the operations (say `mul x 2` is the same as `add x x`). 362 | 363 | #### Seeing through computations 364 | 365 | Still, we aren't interpreting the instructions in a super useful way. 366 | For many computations, we can actually evaluate the result! 367 | - `id` can be evaluated regardless if it's value is constant 368 | - arithmetic operations can be evaluated if their arguments are constants 369 | - some arith ops can be evaluated if _some_ of their arguments are constants 370 | - e.g., `add x 0`, `mul x 1`, `mul x 0`, `div x 1`, `sub x 0` 371 | - what other ops can be evaluated in certain cases? 372 | - `eq x x`? 373 | 374 | #### Clobbered variables 375 | 376 | Our pass above hints at a conservative approach we took to clobbered variables, 377 | in which clobbered variables clobber the values from the state, so they can't be reused later! 378 | Here's a simple example: 379 | ``` 380 | x = a + b 381 | x = 0 382 | y = a + b 383 | return y 384 | ``` 385 | 386 | You can take another approach that introduces temporary variables to handle clobbered variables. 387 | Give it a try if you feel up to it! 388 | We will see later that SSA (single static assignment) form 389 | is a way to ensure that we can deal with this issue once and for all. 390 | 391 | #### Dead code elimination 392 | 393 | Currently our LVN pass introduces a bunch of dead code and relies on a DCE pass to clean it up. 394 | One way to view value numbering is that it's building up a graph of computations 395 | done in a block, 396 | and re-emiting instructions corresponding to that graph as it goes. 397 | Could we wait until the end of a block and try to emit the whole block at once, 398 | just including necessary instructions? 399 | Not without knowing what variables are used later in the program... 400 | 401 | #### Extended Basic Blocks 402 | 403 | What if a basic block `A` goes straight into `B` (and `B` has no other predecessors)? 404 | Surely we can still do _something_ that looks like value numbering? 405 | In fact, many local analyses can be extended beyond just basic blocks by looking at _extended basic blocks_. 406 | 407 | An extended basic block is a set of basic blocks 408 | a sort of single-entry, _multiple-exit_ property. 409 | In other words, 410 | it's a set of basic blocks 411 | with a distinguished entry point such that: 412 | - the entry basic block may have multiple predecessors 413 | - but all others must have only one predecessor, which must be in the set 414 | 415 | 416 | Here's an example EBB: 417 | ```mermaid 418 | flowchart TD 419 | A --> B 420 | B --> C 421 | B --> D 422 | A --> E 423 | ``` 424 | 425 | It is essentially a tree rooted at the entry block. 426 | Any block in an EBB _dominates_ all its children. 427 | We'll return to a more formal definition of dominance later in the course, 428 | but essentially it means that any path to a node in the EBB must go through all its ancestors. 429 | This allows us to essentially reason about each path in the EBB as a single unit. 430 | In the context of value numbering, 431 | we could pass the state of A's value numbering to B and then to D. 432 | But we couldn't then pass it to C, because D doesn't run before C! 433 | So we could optimize this EBB by looking at the path from the root to each node: 434 | - Optimize A 435 | - pass the state into lvn(B) 436 | - pass the state into lvn(C) 437 | - throw away the effects of C (or re-run lvn(A, B)), then do lvn(D) 438 | - throw away the effects of B (or re-run lvn(A)), then do lvn(E) 439 | 440 | This technique can be employed to eek out a little more from your local optimizations, 441 | and hints a bit at the more general techniques global versions later in the course. 442 | 443 | # Task 444 | 445 | This course uses my fork 446 | of the [bril compiler infrastructure](https://github.com/mwillsey/bril/). 447 | 448 | Your task from this lesson is to get familiar with the bril ecosystem and implement the 449 | 3 optimizations we discussed: 450 | 1. Trival global dead code elimination 451 | 2. Local dead code elimination 452 | - See [here](https://github.com/mwillsey/bril/tree/main/examples/test/tdce) for some test cases relevant to dead code optimization. 453 | 3. Local value numbering 454 | - You must implement LVN that performs common subexpression elimination. 455 | - You may (but don't have to) implement some of the extensions discussed above (folding, commutativity, etc.). 456 | - You may choose how to handle "clobbered" variables. Just be sure to document your choice. 457 | - See [here](https://github.com/mwillsey/bril/tree/main/examples/test/lvn) for some test cases relevant to value numbering. 458 | 459 | For this and other assignments, **you need not handle all of the cases identically to the examples given 460 | in the bril repo**. 461 | Your optimizer should be correct (and it's up to you to argue how you know it is), 462 | but it may optimize less or more or differently 463 | than the example code. 464 | You should, however, 465 | be running your optimized code through the bril interpreter 466 | to ensure it outputs the same result as the original code. 467 | The [remove_nops example](https://github.com/mwillsey/bril/tree/main/examples/remove_nops) 468 | shows how to set up `brench` to run the bril interpreter on the original and optimized code and compare the results. 469 | 470 | If you are ahead of the game (e.g., you already know about dataflow analysis), 471 | you are encouraged to implement more aggressive optimizations or a more general pass that subsumes the ones above. 472 | Just be sure to include why you made the choices you did in your written reflection. 473 | 474 | Submit your written reflection on bCourses. It should include some kind of output from the bril tools! A plot, a csv, a table, etc. 475 | 476 | Include two short bril programs in your reflection: 477 | 1. A program that you can optimize very well with your passes. Show the unoptimized and optimized versions. 478 | 2. A program that you can't optimize with your passes, but you can in your head. What's the issue? What would you need to do to optimize it? 479 | 480 | This task (and others) are meant to be open-ended and exploratory. 481 | The requirements above are the minumum to get a 1-star grade, but you are encouraged to go above and beyond! 482 | -------------------------------------------------------------------------------- /lessons/02-dataflow.md: -------------------------------------------------------------------------------- 1 | # Dataflow 2 | 3 | Resources: 4 | - There are many, many resources on dataflow analysis, and the terminology is pretty consistent. 5 | The [Wikipedia page](https://en.wikipedia.org/wiki/Data-flow_analysis) is a good starting point. 6 | - The excellent book [_Static Program Analysis_](https://cs.au.dk/~amoeller/spa/spa.pdf) (free online) 7 | is a detailed and authoritative reference. Chapter 5 deals with dataflow analysis. 8 | - Monica Lam's CS 243 slides 9 | ([part 1](https://suif.stanford.edu/~courses/cs243/lectures/L2.pdf), 10 | [part 2](https://suif.stanford.edu/~courses/cs243/lectures/L3.pdf)) 11 | from Stanford. 12 | - Susan Horwitz's [CS704 notes](https://pages.cs.wisc.edu/~horwitz/CS704-NOTES/2.DATAFLOW.html). 13 | - Martin Rinard's [6.035 slides](https://ocw.mit.edu/courses/6-035-computer-language-engineering-sma-5502-fall-2005/80d42de10044c47032e7149d0eefff66_11dataflowanlys.pdf) from MIT OCW. 14 | - Lecture [notes](https://www.cs.tau.ac.il//~msagiv/courses/pa07/lecture2-notes-update.pdf) from Mooly Sagiv's course at Tel Aviv University, 15 | including coverage of the theoretical underpinnings of dataflow analysis like partial orderings, lattices, and fixed points. 16 | 17 | ## Control Flow Graphs 18 | 19 | We introduced control flow graphs (CFGs) in the [previous lesson](./01-local-opt.md#control-flow-graphs), 20 | but for the purpose of limiting our reasoning to a single block. 21 | Now we'll begin to consider the graph as a whole, 22 | and the relationships between blocks. 23 | 24 | ```mermaid 25 | flowchart TD 26 | A --> B & C 27 | B --> C 28 | C 29 | ``` 30 | 31 | Recall the definitions of predecessors and successors of a basic block; 32 | we'll need those later. 33 | 34 | ## Constant Propagation 35 | 36 | Our optimizations from the previous lesson were local, in that they only considered a single block. 37 | If we zoom in on constant propagation, 38 | we could only propagate constants within a single block. 39 | Consider the following example: 40 | 41 | ```c 42 | y = 1; 43 | if (...) { ... } 44 | use(y) 45 | ``` 46 | 47 | In this case, even if nothing happens to `y` in the `if` block, 48 | we can't propagate the constant `1` to the `use(y)` statement. 49 | 50 | ```c 51 | y = 1; 52 | if (...) { y = y + 2; } 53 | use(y) 54 | ``` 55 | 56 | ```c 57 | y = 1; 58 | if (...) { y = 1; } 59 | use(y) 60 | ``` 61 | 62 | ### As An Analysis 63 | 64 | In value numbering, 65 | we were statically executing the program 66 | and building up a state that represented the values of variables. 67 | We were also simultaneously 68 | modifying the program based on that state in the straight-line fashion. 69 | 70 | For now, 71 | let's set aside the idea of modifying the program, 72 | and just focus on computing the state that represents what's happening in the program. 73 | 74 | What is the state of constant propagation? 75 | Something about the values of variables at each point in the program, 76 | and whether they are constants or not. 77 | Perhaps a map from variables to constants. 78 | If a variable is not (or cannot be proven to be) a constant, 79 | we can just leave it out of the map. 80 | 81 | Let's try this analysis on on some straight-line code. 82 | 83 | | instruction | `x` | `y` | `z` | 84 | |-------------|-----|-----|-----| 85 | | `y = 1` | | 1 | | 86 | | `z = x + 1` | | 1 | | 87 | | `x = 2` | 2 | 1 | | 88 | | `x = z` | | 1 | | 89 | | `y = y + 1` | | 2 | | 90 | 91 | Note how we left the initial values of `x` and `z` as blank. 92 | Similar to our value numbering analysis, 93 | we don't know anything about the values coming into this segment of the program. 94 | 95 | ### Extended Basic Blocks 96 | 97 | How can we make this analysis more global? 98 | 99 | Recall from last time that we can extend a local analysis to 100 | one over [extended basic blocks](./01-local-opt.md#extended-basic-blocks) 101 | by passing the state from the end of one block to the beginning of the next. 102 | The same holds for our constant propagation analysis. 103 | For a linear path of basic blocks 104 | (every path in a tree that forms an EBB), 105 | we can pass the state from the end of one block to the beginning of the next. 106 | 107 | ```mermaid 108 | flowchart TD 109 | A --> B 110 | A --> C 111 | A["**A** 112 | x = 2 113 | y = 1 114 | z = a 115 | "] 116 | B["**B** 117 | z = x + 1 118 | "] 119 | C["**C** 120 | y = x + y 121 | "] 122 | ``` 123 | 124 | Try writing out the "output" state for our constant propagation analysis 125 | at the end of each block. 126 | 127 | ### Joins/Meets 128 | 129 | The key property of EBBs that allows this extension 130 | is that every non-rooted node has a single predecessor. 131 | That means there's no ambiguity about which state to use when started the analysis at that node; 132 | you use the state from the parent in the tree (the only predecessor in the CFG). 133 | 134 | What if we have a node with multiple predecessors? 135 | 136 | ```mermaid 137 | flowchart TD 138 | A --> B 139 | A --> C 140 | B --> C 141 | 142 | A["**A** 143 | x = 1 144 | y = 1 145 | branch ... 146 | "] 147 | B["**B** 148 | y = y + 2; 149 | "] 150 | C["**C** 151 | use(y) 152 | "] 153 | ``` 154 | 155 | Blocks `A` and `B` form an EBB, and we can compute their states: 156 | - `A` says `x = 1, y = 1` 157 | - `B` says `x = 1, y = 3` (using constant folding as well) 158 | 159 | When we get to `C`, what state are we allowed to "start with"? 160 | Surely we can do the same as our local analysis and start with the empty state 161 | where no variables are known to be constants. 162 | But that seems to be a waste of information, 163 | since all of `C`'s predecessors agree on at least one value (`x = 1`). 164 | The key is have some way to combine the information when a block has multiple predecessors. 165 | For our constant propagation analysis, 166 | we can take the intersection of the states from the predecessors. 167 | 168 | ## Dataflow Analysis 169 | 170 | Dataflow analysis is well-studied and understood 171 | framework for analyzing programs. 172 | It can express a wide variety of analyses, 173 | including constant propagation and folding 174 | like we saw above. 175 | Let's try to fit in our constant propagation analysis into the dataflow framework. 176 | 177 | Here are the ingredients of a dataflow analysis: 178 | 1. A fact (or set of facts) you want to know at every point in the program. 179 | - Point in the program here means beginning or end of a block in the CFG, 180 | but you could also consider every instruction. 181 | 2. An initial fact for the beginning of the program. 182 | 3. A way to compute the fact at the end of a block from the facts at the beginning of the block. 183 | - This is sometimes called the transfer function. 184 | - Sometime this is notated as $f$, so $out(b) = f(in(b))$. 185 | - For some analyses, you compute the beginning fact from the end fact (or both ways!). 186 | - For constant propagation, the transfer function is the same as the one we used for BBs/EBBs; 187 | a limited version of our value numbering from before. 188 | 3. A way to relate the input/output facts of a block to the inputs/outputs of its neighbors. 189 | - Typically this is phrased as a "join" or "meet" function that combines the facts from multiple predecessors. 190 | - $in(b) = meet_{p \in \text{pred}(b)} out(p)$ 191 | - For constant propagation, the meet function is the intersection of the maps. 192 | 5. An algorithm to compute the facts at every program point such that the above equations hold. 193 | 194 | Let's set aside how to solve the dataflow problem for now, 195 | and just focus on the framework. 196 | 197 | How does constant propagation fit into this framework? 198 | 1. The fact we want to know is the mapping of variables to constants. 199 | 2. The initial fact is the empty map. 200 | 3. The transfer function is the same as our local analysis. 201 | - This is one of the cool parts of dataflow analysis: you are still only reasoning about a single block at a time! 202 | 4. The meet function is the intersection of the maps. 203 | - If the maps disagree, we just say the variable is not a constant (delete it from the map). 204 | 205 | Once you apply the dataflow framework, 206 | you can define a simple, local analysis for something like constant propagation/folding, 207 | and extend it to a whole CFG. 208 | The solver's that we'll discuss later can be generic across instances of dataflow problems, 209 | so one solver implementation can be used for many different analyses! 210 | 211 | Try writing out the dataflow equations for the constant propagation analysis 212 | we did above on the CFG with blocks `A`, `B`, and `C`. 213 | 214 | ## Live Variables 215 | 216 | Let's now switch gears to another dataflow analysis problem: liveness. 217 | This analysis tells us which variables are "live" at each point in the program; 218 | that is, which variables might be needed in the future. 219 | 220 | Consider the following code: 221 | ```c 222 | x = 1; 223 | y = 2; 224 | z = 3; 225 | if (...) { 226 | y = 1; 227 | use(x); 228 | } 229 | use(y); 230 | ``` 231 | 232 | And the corresponding CFG: 233 | ```mermaid 234 | flowchart TD 235 | A["**A** 236 | x = 1 237 | y = 2 238 | z = 3 239 | "] 240 | B["**B** 241 | y = 1 242 | use(x) 243 | "] 244 | C["**C** 245 | return y; 246 | "] 247 | A --> B 248 | A --> C 249 | B --> C 250 | ``` 251 | 252 | Live variables are those that might be used in the future. 253 | Live variables is a backwards analysis: 254 | so we will compute the live variables for the 255 | beginning of each block from those live at the end of the block. 256 | To get started, 257 | no variables are live at the end of the function: `out(C) = {}`. 258 | Now we can compute `in(C)` by seeing that `y` is used in the return statement, 259 | so `in(C) = {y}`. 260 | 261 | Now for blocks `A` and `B`. 262 | This is a backwards analysis, 263 | so we compute the `in` from the `out` of the block, 264 | and the out is derived from the `in`s of the successors. 265 | So we can't compute `out(A)` just yet. 266 | 267 | But `B` is ready to go, it only has one successor `C`, so `out(B) = in(C) = {y}`. 268 | We can compute `in(B)` from `out(B) = {y}` and the block itself: 269 | `B` uses `x`, but it re-defines `y`, so `in(B) = out(B) + {x} - {y} = {x}`. 270 | 271 | Now we can compute `out(A)` by combining `in(B) = {x}` and `in(C) = {y}`. 272 | Liveness is said to be a "may" analysis, 273 | because we're interested in the variables that _might_ used in the future. 274 | In these kinds of analyses, 275 | the meet operation is typically set union. 276 | Thus, we can compute `out(A) = in(B) U in(C) = {x} U {y} = {x, y}`. 277 | Finally `in(A)` is `out(A) - {x, y, z} = {}` since `A` defines `x`, `y`, and `z`. 278 | 279 | So to fit liveness into the dataflow framework: 280 | 1. The fact we want to know is the set of live variables at the beginning/end of each block. 281 | 2. The initial fact is the empty set. 282 | - This is a backwards analysis, so the initial fact is the set of live variables at the end of the program. 283 | 3. The transfer function is as above. 284 | - This is a backwards analysis, we compute the `in` from the `out`. 285 | - Live variables is one of a class of dataflow problems called "gen/kill" or [bit-vector problems](https://en.wikipedia.org/wiki/Data-flow_analysis#Bit_vector_problems). 286 | - In these problems, the transfer function can be broken down further into two parts: 287 | - `gen` is the set of variables that are used in the block. 288 | - `kill` is the set of variables that are defined in the block. 289 | - special care need to be taken for variables that are both used and defined in the block. 290 | - `in(b) = gen(b) U (out(b) - kill(b))` 291 | 4. The meet function is set union. 292 | - This is a backwards analysis, so the meet function combines the successors' `in` sets to form the `out` set for a block. 293 | - This is a "may" analysis, so we use union to represent the fact a variable might be live in any of the successors. 294 | 295 | 296 | ## Solving dataflow 297 | 298 | So far we haven't seen how to solve a dataflow problem. 299 | We've sort of done it by hand, running the transfer function when its input was "ready". 300 | This works for acyclic CFGs, you can just topologically sort the blocks and run the transfer functions in that order. 301 | In this way, you never have to worry about the inputs the transfer function changing; 302 | you run every block's transfer function exactly once. 303 | 304 | But what about CFGs with loops? 305 | 306 | The key to solving dataflow problems is to recognize that they are fixed-point problems. 307 | For a forward analysis on block `b`, 308 | we construct `in(b)` from the `out` sets of the predecessors. 309 | then we construct `out(b) = f(in(b))`. 310 | This in turn changes the input to the successors, 311 | so we repeat the process until the `in` and `out` sets stabilize. 312 | Another way to think about it is solving a system of equations. 313 | Each block `b` induces 2 equations: 314 | - $in(b) = meet_{p \in \text{pred}(b)} out(p)$ 315 | - $out(b) = f(in(b))$ 316 | 317 | Perhaps surprisingly, 318 | a very simple technique can be used to solve these equations 319 | (given some conditions on the transfer function and the domain; we'll get to those later). 320 | We do, however, have to run some transfer functions multiple times, 321 | since the inputs to the transfer function can change. 322 | A naive (but correct) solution is to simply iterate over the blocks, 323 | updating the `in` and `out` sets until they stabilize: 324 | ```python 325 | for b in blocks: 326 | in[b] = initial 327 | while things changed: 328 | for b in blocks: 329 | out[b] = f(b, in[b]) 330 | for b in blocks: 331 | in[b] = meet(out[p] for p in pred[b]) 332 | ``` 333 | 334 | This iterative approach does require some properties of the transfer function and the domain. 335 | If you take as a given that this algorithm is correct, 336 | perhaps you can deduce what those properties are! 337 | 338 | 1. The algorithm specifies neither the order in which the blocks are processed, 339 | nor the order of the predecessors for each block. 340 | - This suggests the meet function doesn't care about the order of its inputs. 341 | - The meet function is associative and commutative. 342 | 2. The stopping condition above is very simple: keep going until the `in`s and `out`s stabilize. 343 | - The dataflow framework uses a _partial order_ on the facts to ensure that this algorithm terminates. 344 | - Termination requires that the transfer function is _monotonic_, that is, 345 | a "better" input to the transfer function produces a "better" output: 346 | if $a \leq b$ then $f(a) \leq f(b)$. 347 | - We saw above that the meet function is associative and commutative. 348 | Typically, we constrain the domain to form a [lattice](https://en.wikipedia.org/wiki/Lattice_(order)), 349 | where the meet operation is indeed the meet of the lattice. 350 | - See the above link for more details on lattices, 351 | but the key point is that `meet(a, b)` returns the greatest lower bound of `a` and `b`; the biggest element that is less than both `a` and `b`. 352 | - In the context of dataflow analysis, where lower is typically "less information", 353 | the meet operation gives you the best (biggest) set of facts that is less than its inputs. 354 | - For now, termination also requires that the lattice has _finite height_, 355 | so you cannot have an infinite chain of elements that are all less than each other. 356 | We can lift this restriction later with so-called _widening_ operators. 357 | - Interval analysis is an example of an analysis that requires widening. 358 | 359 | We can exploit these properties to make the algorithm more efficient. 360 | In particular, we can use a worklist algorithm to avoid recomputing the `out` sets of blocks that haven't changed. 361 | 362 | Worklist algorithm (for forward analysis): 363 | ```python 364 | for b in blocks: 365 | in[b] = initial 366 | out[b] = initial 367 | 368 | worklist = blocks 369 | while b := worklist.pop(): 370 | in[b] = meet(out[p] for b in b.predecessors) 371 | out[b] = f(b, in[b]) 372 | if out[b] changed: 373 | worklist.extend(b.successors) 374 | ``` 375 | 376 | The worklist algorithm is more-or-less the typical way to solve dataflow problems. 377 | Note that it still does not specify the order in which blocks are processed! 378 | While of course that doesn't matter for correctness, 379 | it can affect the efficiency of the algorithm. 380 | Consider a CFG that's a DAG. 381 | If you topologically sort the blocks, 382 | you can run the algorithm in either a single pass, 383 | or in a very silly way if you proceed in the reverse order. 384 | The ordering typically used in practice is 385 | the [reverse postorder](https://en.wikipedia.org/wiki/Data-flow_analysis#Ordering) of the blocks. 386 | 387 | 392 | 393 | ### Solutions to Dataflow 394 | 395 | The algorithms above find a fixed point, but what does that mean? 396 | Essentially, it's which execution paths are accounted for in the analysis. 397 | 398 | The best one is the _ideal_ solution: 399 | the meet of all of the programs paths that actually execute. 400 | This is not computable in general, 401 | because you don't know which paths will be taken. 402 | So the best you can do statically is called the Meet Over all Paths (MOP) solution, 403 | which is the meet of all paths in the CFG. 404 | Note there are infinitely many paths in a CFG with loops, 405 | so you cannot compute this directly! 406 | 407 | The algorithms above compute a fixed point to the dataflow equations. 408 | This is typically referred to as the _least fixed point_, 409 | because the solution is the smallest set of facts that satisfies the dataflow equations. 410 | When the dataflow problem is distributive (see below), 411 | the least fixed point is the MOP solution. 412 | 413 | ## Liveness With Loops 414 | 415 | Let's walk through an example using the naive algorithm to do live variable analysis on the following program with a loop: 416 | 417 | ```c 418 | int foo(int y, int a) { 419 | int x = 0; 420 | int z = y; 421 | while (x < a) { 422 | x = x + 1; 423 | y = y + 1; 424 | } 425 | return x; 426 | } 427 | ``` 428 | 429 | And in the IR: 430 | ``` 431 | .entry: 432 | x = 0 433 | z = y 434 | jmp .header 435 | .header: 436 | c = x < a 437 | br c .loop .exit 438 | .loop: 439 | x = x + 1 440 | y = y + 1 441 | jmp .header 442 | .exit: 443 | return x 444 | ``` 445 | 446 | Since live variables is a backwards analysis, 447 | we'll start with the empty set at the end of the program 448 | use the naive algorithm over the blocks in reverse order. 449 | 450 | | Block | Live Out 1 | Live In 1 | Live Out 2 | Live In 2 | Live Out 3 | Live In 3 | 451 | |-----------|------------|-----------|------------|-----------|------------|-----------| 452 | | `.exit` | [] | x | " | " | " | " | 453 | | `.loop` | x | x, y | x, y, a | x, y, a | " | " | 454 | | `.header` | x, y | x, y, a | x, y, a | x, y, a | " | " | 455 | | `.entry` | x, y | y, a | x, y, a | y, a | " | " | 456 | 457 | There are some interesting things to note about this execution. 458 | 459 | First, note how the worklist algorithm could have saved 460 | revisiting some blocks once they stabilized, e.g., `.exit`. 461 | Since `.exit` has no successors, its `out` set will never change, 462 | so nobody would ever add it back to the worklist. 463 | 464 | ### Optimistic vs Pessimistic 465 | 466 | Second, observe the intermediate values of the `in` and `out` sets, 467 | especially for the `.loop` and `.header` blocks. 468 | They are "wrong" at some point in the analysis! 469 | 470 | In analyses like constant propagation, 471 | we are constantly trying to "improve" the facts at each point in the program. 472 | We start out knowing that nothing is a constant, 473 | and we try to prove that some things are constants. 474 | We call such an analysis _pessimistic_. 475 | It assumes the worst case scenario, and takes conservative steps along the way. 476 | At any point, you could stop the analysis and the facts would be true, 477 | just maybe not as precise as they could be if you kept going. 478 | 479 | Liveness is an _optimistic_ analysis. 480 | It starts with a (very) optimistic view of the facts: 481 | that nothing is live! 482 | This is of course unsound if you stopped the analysis at that point; 483 | it would allow you to say that everything is dead! 484 | In an optimistic analysis, 485 | reaching a fixed point is necessary for the correctness of the analysis. 486 | 487 | The dataflow framework computes optimistic and pessimistic analyses in the same way, 488 | but it's good to remember which one you're working with, 489 | especially if you attempt to use the partial results of the analysis for optimization. 490 | 491 | ### Strong Liveness 492 | 493 | Another thing to note about the above example is 494 | that "live" code is not necessarily "needed" code. 495 | The variable `y` is never used for anything "important", 496 | but it is used in the loop (to increment itself), 497 | so it's marked as live. 498 | The loop itself becomes a sort of "sink" which makes 499 | any self-referential assignment. 500 | 501 | An alternative to the standard liveness analysis is _strong liveness_. 502 | Where liveness computes if a variable is used in the future, 503 | strong liveness computes if a variable is used in a "meaningful" way. 504 | In the above example, `y` would not be considered strongly live. 505 | As we discussed in the [first lecture](./00-overview.md), 506 | "meaningful" is a bit of a loaded term, 507 | but here it's clear that `y` is not used in a way that affects the return value of the function. 508 | Strong liveness starts from a set of instructions that are the source 509 | of the "meaningful" uses of variables: 510 | these typically include return statements, 511 | (effectful) function calls, 512 | branches, 513 | and other effects like input/output operations. 514 | Using a variable in a standard operation like addition or multiplication 515 | is not inherently "meaningful", 516 | but it can be if the result is used in a "meaningful" way. 517 | So using a variable `x` in strong liveness only makes `x` live 518 | if the operation is inherently "meaningful" or if the operation defines a variable that is used in a "meaningful" way. 519 | 520 | In our above example, 521 | `y` is live but not strongly live. 522 | Consider implementing strong liveness as an exercise; it will make your dead code elimination more effective! 523 | 524 | ## Properties of dataflow problems 525 | 526 | In addition to optimistic/pessimistic, 527 | dataflow problems can be classified in a few other ways. 528 | One common taxonomy is based on the direction of the analysis 529 | and the meet function. 530 | 531 | | | Forwards | Backwards | 532 | |----------|----------------------------------------------------------------------|-----------------------| 533 | | **May** | Reaching definitions | Live variables | 534 | | **Must** | Available expressions
Constant prop/fold
Interval analysis | Very busy expressions | 535 | 536 | Another property commonly discussed is the distributivity of the transfer function over the meet operation: 537 | `f(a meet b) = f(a) meet f(b)`. 538 | If this property holds, the analysis is said to be _distributive_, 539 | and can be computed in a more efficient way in some cases. 540 | It also guarantees that the solution found by solving the dataflow equations is the Meet Over all Paths (MOP) solution. 541 | Dataflow problem framed in the gen/kill setting are typically distributive. 542 | Constant propagation is a good example of a _non_-distributive analysis. 543 | Consider the following simple blocks: 544 | - `X: a = 1; b = 2` 545 | - `Y: a = 2; b = 1` 546 | - `Z: c = a + b`, where `Z`'s predecessors are `X` and `Y`. 547 | In this case: 548 | $$f_Z(out_X \cap out_Y) = \emptyset$$ 549 | But: 550 | $$f_Z(out_X) \cap f(out_Y) = 551 | \{a \mapsto 1, b \mapsto 2, c \mapsto 3 \} \cap 552 | \{a \mapsto 2, b \mapsto 1, c \mapsto 3 \} = 553 | \{c \mapsto 3 \} 554 | $$ 555 | 556 | # Task 557 | 558 | Turn in on bCourses a written reflection of the below task. 559 | 560 | Do not feel obligated to explain exactly how each analysis works (I already know!), 561 | instead focus on the design decisions you made in implementing the analysis, 562 | and presenting evidence of its effectiveness and correctness. 563 | 564 | - Implement a global constant propagation/folding analysis (and optimization!). 565 | - This probably won't entirely subsume the LVN you did in the previous tasks, since you did common subexpression elimination in that. 566 | - But you should test and see! 567 | - Implement a global liveness analysis and use it to global implement dead code elimination. 568 | - This will probably subsume the trivial dead code elimination you did in the previous task. 569 | - But you should test and see! 570 | - As before, include some measurement of the effectiveness and correctness of your optimization/analysis. 571 | - Run your optimizations on not only the tests in the `examples/` directory, 572 | but also on the benchmarks in the `benchmarks/` directory. 573 | - Optionally: 574 | - Implement another dataflow analysis like strong liveness or reaching definitions. 575 | - What properties does that analysis have? Distributive? May/must? Forward/backward? Optimistic/pessimistic? 576 | - Implement a generic dataflow solver that can be used for any dataflow problem. 577 | 578 | As before, the repo includes some [example analyses and test cases](https://github.com/mwillsey/bril/tree/main/examples/test/df) you may use. 579 | Do not copy the provided analysis; please attempt your own first based on the course material. 580 | You may use the provided analysis as a reference after you have attempted your own. 581 | You may always use the provided test cases, but note that your results may differ and still be correct, 582 | the provided tests are checking the *exact* output the provided analysis produces. 583 | Note how that folder includes some `.out` files which are not programs, 584 | but just lists of facts. 585 | This is not a standard format, 586 | it's just a text dump to stdout from the analysis for `turnt` to check. 587 | -------------------------------------------------------------------------------- /lessons/03-ssa.md: -------------------------------------------------------------------------------- 1 | # Static Single Assignment (SSA) Form 2 | 3 | As you've undoubtedly noticed, 4 | re-assignment of variables 5 | has been a bit painful. 6 | 7 | We also saw in last week's paper 8 | a peek at SSA 9 | and how it can enable and even power-up optimizations 10 | by allowing a "sparse" computation of dataflow. 11 | 12 | In this lesson, we'll dive into SSA 13 | and see how it works 14 | and how to convert a program into SSA form. 15 | 16 | Resources: 17 | - Lots has been written about SSA, the [Wikipedia page](https://en.wikipedia.org/wiki/Static_single_assignment_form) is a good start. 18 | - The [_SSA Book_](https://pfalcon.github.io/ssabook/latest/book-full.pdf) (free online) is an extensive resource. 19 | - Jeremy Singer's [SSA Bibliography](https://www.dcs.gla.ac.uk/~jsinger/ssa.html) is a great resource for papers on SSA, 20 | and Kenneth Zadeck's [slides](https://compilers.cs.uni-saarland.de/ssasem/talks/Kenneth.Zadeck.pdf) on the history of SSA are a good read as well. 21 | - This presentation of SSA follows the seminal work by Cytron et al, [Efficiently computing static single assignment form and the control dependence graph](https://dl.acm.org/doi/pdf/10.1145/115372.115320). 22 | - The Cytron algorithm is still used by many compilers today. 23 | - A more recent paper is "Simple and Efficient Construction of Static Single Assignment Form", by Braun et al., which is [this week's reading](../reading/braun-ssa.md). 24 | 25 | ## SSA 26 | 27 | SSA stands for "Static Single Assignment". 28 | The "static" part means that each variable is assigned to at precisely one place in the program text. 29 | Of course dynamically, in the execution of the program, a variable can be assigned to many times (in a loop). 30 | The "precisely one" part is a big deal! 31 | This means that at any use of a variable, 32 | you can easily look at its definition, 33 | no analysis necessary. 34 | 35 | This creates a nice correspondence between the program text and the dataflow in the program. 36 | - definition = variable 37 | - instruction = value 38 | - argument = data flow edges 39 | 40 | This is a step towards a graph-based IR! 41 | In fact, LLVM and other compilers use pointers to represent arguments in the IR. 42 | So instead of a variable being a string, it's a pointer to the instruction that defines it (because it's unique!). 43 | 44 | ### Straight-Line Code 45 | 46 | Let's look at SSA conversion starting with simple, straight-line code. 47 | 48 | ```c 49 | x = 1; 50 | x = x + 1; 51 | y = x + 2; 52 | z = x + y; 53 | x = z + 1; 54 | ``` 55 | 56 | For straight-line code like this, 57 | SSA is easy. 58 | You can essential walk through the code 59 | and rename each definition to a new variable 60 | (typically like `x_1`, `x_2`, etc). 61 | When you see a use, 62 | you update that use to the most recent definition. 63 | 64 | ```c 65 | x_1 = 1; 66 | x_2 = x_1 + 1; 67 | y_1 = x_2 + 2; 68 | z_1 = x_2 + y_1; 69 | x_3 = z_1 + 1; 70 | ``` 71 | 72 | ### Branches 73 | 74 | Easy peasy! But what about control flow? 75 | Here we encounter the $\phi$ ("phi") function, 76 | which we saw defined in last week's paper. 77 | The $\phi$ function arises 78 | from situations where control flow _merges_, 79 | and a variables may have been assigned to in different ways. 80 | An `if` statement is a classic example. 81 | 82 | ```c 83 | int x; 84 | if (cond) { 85 | x = 1; 86 | } else { 87 | x = 2; 88 | } 89 | use(x); 90 | ``` 91 | 92 | And in the IR: 93 | 94 | ``` 95 | .... 96 | br cond .then .else 97 | .then: 98 | x = 1; 99 | jump .end 100 | .else: 101 | x = 2; 102 | jump .end 103 | .end: 104 | use x 105 | ``` 106 | 107 | SSA is "static" single assignment, 108 | so even though only one of the branches will be taken, 109 | we still need to rename `x` differently in each branch. 110 | Say `.then` defines `x_1` and `.else` defines `x_2`. 111 | What do we do at `.end`, which `x` do we use? 112 | 113 | We need a $\phi$ function! 114 | 115 | ``` 116 | .... 117 | br cond .then .else 118 | .then: 119 | x_1 = 1; 120 | jump .end 121 | .else: 122 | x_2 = 2; 123 | jump .end 124 | .end: 125 | x_3 = phi .then x_1 .end x_2 126 | use x_3 127 | ``` 128 | 129 | In Bril, 130 | the `phi` instruction takes 2 variables and 2 labels 131 | (they can be mixed up, due to the JSON representation storing registers and labels separately). 132 | In some literature, they write it without the labels: $\phi(x_1, x_2)$. 133 | That's just the same, 134 | since in SSA a variable is defined in exactly one place. 135 | In Bril it's just a bit more explicit. 136 | 137 | The `phi` instruction gives us a way to merge dataflow from different branches. 138 | Intuitively, it's like saying "if we came from `.then`, use `x_1`, if we came from `.else` use `x_2`". 139 | It's called "phi" because it's a "phony" instruction, 140 | not a real computation, 141 | it only exists to enable use to use SSA form. 142 | The Bril interpreter is actually capable of executing `phi` instructions, 143 | but in a real setting, they are eliminated (see below). 144 | 145 | ### Loops 146 | 147 | Loops is SSA form are similar to branches. 148 | The interesting points are basic blocks with multiple predecessors. 149 | 150 | Here's a simple do-while loop: 151 | ```c 152 | int x = 0; 153 | do { 154 | x = x + 1; 155 | } while (cond); 156 | use(x); 157 | ``` 158 | 159 | In the IR: 160 | 161 | ``` 162 | ... 163 | x = 0; 164 | jump .loop 165 | .loop: 166 | x = x + 1; 167 | cond = ...; 168 | br cond .loop .end 169 | .end: 170 | use x 171 | ``` 172 | 173 | Here, `.loop` is the point where control flow merges, much like the block after an `if`. 174 | And in SSA form: 175 | 176 | ``` 177 | ... 178 | x_1 = 0; 179 | jump .loop 180 | .loop: 181 | x_3 = phi .loop x_1 .loop x_2 182 | x_2 = x_3 + 1; 183 | cond = ...; 184 | br cond .loop .end 185 | .end 186 | use x_2 187 | ``` 188 | 189 | Note the ordering of the `x` vars. 190 | It's not necessary to have them in any order, 191 | but it suggests the order in which we "discovered" that we need a $\phi$ there. 192 | First we did the renaming, 193 | and then we went around the loop and saw that we needed to merge the 194 | definition of `x` from the header and that of the loop body. 195 | 196 | ### Where to Put $\phi$ Functions 197 | 198 | Where do we need to put $\phi$ functions? 199 | We have approached this informally for now. 200 | Our current algorithm is to do a forward-ish pass, 201 | renaming and introducing $\phi$ functions "where they are necessary". 202 | 203 | A simple way to think about it is in terms of live variables and reaching definitions 204 | (recall these from the dataflow lesson). 205 | Certainly where phis are necessary is related to control flow merges. 206 | A form of SSA called "trivial SSA" 207 | can be constructed by inserting $\phi$ functions at every join point for all live variables. 208 | 209 | But we can do better than that. 210 | Even at a join point, we don't need a $\phi$ function for every variable. 211 | Consider a CFG with a diamond shape, it should be the case at uses after the branches merge 212 | can refer to definition from before the branches diverged without needing a $\phi$ function. 213 | This leads us to "minimal SSA", 214 | where we only insert $\phi$ at join points 215 | for variables that that multiple reaching definitions. 216 | 217 | An alternative way 218 | (and one used more frequently since it avoid having to compute liveness) 219 | to place $\phi$ functions is to consider _dominators_. 220 | 221 | ## Dominators 222 | 223 | The key concept we need is that of dominators. 224 | Node $A$ in a CFG _dominates_ node $B$ if every path from the entry to $B$ goes through $A$. 225 | 226 | Recall an extended basic block is a tree of basic blocks. 227 | In an EBB, the entry node dominates all the other nodes. 228 | 229 | What about for slightly more complex CFGs? 230 | 231 | ```mermaid 232 | graph TD 233 | A --> B & C 234 | B & C --> D 235 | ``` 236 | 237 | Above is a simple CFG with a diamond shape, perhaps from an `if` statement. 238 | Let's figure out which nodes dominate which. 239 | Hopefully it's intuitive that `A` dominates `B` and `C`, since it has to run directly before them. 240 | `A` also dominates `D`, since every path to `D` goes through `A`. 241 | You can also view this inductively: `D`'s predecessors are `B` and `C`, and `A` dominates both of them, and so `A` dominates `D` as well. 242 | Domination is a reflexive relation, so `A` dominates itself. 243 | Therefore, `A` dominates all the nodes in this CFG. 244 | 245 | What about `B` and `C`? Do they dominate `D`? 246 | No! 247 | For example, the path `A -> C -> D` does not go through `B`, so `B` does not dominate `D`. 248 | Similarly, `C` does not dominate `D`. 249 | And finally, `D` does not dominate `B` or `C`. 250 | 251 | So the complete domination relation is: 252 | - `A` dominates `A`, `B`, `C`, and `D` 253 | - `B` dominates `B` 254 | - `C` dominates `C` 255 | - `D` dominates `D` 256 | 257 | How does domination help us with $\phi$ functions? 258 | So far, it tells us where we _don't_ need $\phi$ functions! 259 | For example, any definition in `A` does not need a $\phi$ function for any use in `A`, `B`, `C`, or `D`, since `A` dominates all of them. 260 | Intuitively, if a definition is guaranteed to run before a use, we don't need a $\phi$ function. 261 | 262 | ### Computing Dominators 263 | 264 | How do we compute dominators? 265 | The naive algorithm is to compute the set of dominators for each node. 266 | The formula for the dominators of a node $b$ is: 267 | $$ \text{dom}(b) = \{b\} \cup \left(\bigcap_{p \to b} \text{dom}(p)\right) $$ 268 | 269 | Note that the above is the things that dominate $b$, not the things that $b$ dominates! 270 | 271 | This formula captures exactly the intuition above! 272 | A block $b$ dominates itself, and it dominates a block $c$ iff every predecessor of $c$ is dominated by $b$. 273 | 274 | This looks a bit like a dataflow equation, and in fact you can compute dominators using a fixed-point algorithm 275 | in the same fashion! 276 | Just iterate the above equation until you reach a fixed point. 277 | 278 | Computing dominators in this way is $O(n^2)$ in the worst case, 279 | but if you visit the nodes in reverse postorder, it's $O(n)$. 280 | 281 | ### Dominator Tree 282 | 283 | A convenient and compact way to represent the dominator relation is with a dominator tree. 284 | Some algorithms in compilers will want to traverse the dominator tree directly. 285 | To build one, 286 | we need a couple more definitions: 287 | - $A$ _strictly dominates_ $B$ if $A$ dominates $B$ and $A \neq B$. 288 | - $A$ _immediately dominates_ $B$ if $A$ strictly dominates $B$ _and_ $A$ does not strictly dominate any other node that strictly dominates $B$. 289 | 290 | The _dominator tree_ is a tree where each node is a basic block, 291 | and the parent of a node is the immediate dominator of that node. 292 | This also means that for any subtree of the dominator tree, 293 | the root of the subtree is dominates of all the nodes in the subtree. 294 | 295 | Example CFG: 296 | 297 | ```mermaid 298 | graph TD 299 | A --> B & C 300 | B & C --> D 301 | D --> E 302 | ``` 303 | 304 | And the dominator tree: 305 | 306 | ```mermaid 307 | graph TD 308 | A --> B & C & D 309 | D --> E 310 | ``` 311 | 312 | ### Dominance Frontier 313 | 314 | The final concept we need is the _dominance frontier_. 315 | Essentially, the dominance frontier of a node $A$ is the set of nodes "just outside" of the ones that $A$ dominates. 316 | 317 | More formally, $B$ is in the dominance frontier of a node $A$ if both of the following hold: 318 | - $A$ does _not_ strictly dominate $B$ 319 | - but $A$ _does_ dominate a predecessor of $B$ 320 | 321 | We need the _strictly_ because a node can be in its own dominance frontier in the presence of a loop. 322 | 323 | For example, in this CFG, $B$ dominates $\{B, C\}$, and the dominance frontier of $B$ is $\{B, D\}$. 324 | 325 | ```mermaid 326 | graph TD 327 | A --> B 328 | B --> C --> B 329 | A --> D 330 | B --> D 331 | ``` 332 | 333 | ## Converting Into SSA 334 | 335 | To convert a program into SSA form, 336 | we are going to use the dominance frontier to guide where to place $\phi$ functions. 337 | First, we place these "trivial" $\phi$ functions, without doing any renaming. 338 | Then, we rename all the variables. 339 | 340 | ### Place the $\phi$ Functions 341 | 342 | The dominance relation tells us where we _don't_ need $\phi$ functions for a definition: 343 | if a definition dominates a use, we don't need a $\phi$ function. 344 | But just outside of that region, we do need $\phi$ functions. 345 | 346 | (convince yourself of this! if $A$'s dominance frontier contains $B$, then $B$ is a join point for the control flow from $A$) 347 | 348 | We begin by placing trivial $\phi$ functions that look like `x = phi x .source1 x .source2` at these points. 349 | In general, a phi function will take a number of arguments equal to the number of predecessors of the block it's in. 350 | So make sure your initial, trivial $\phi$ functions reflect that. 351 | 352 | 353 | 354 | ```py 355 | DF[b] = dominance frontier of block b 356 | defs[v] = set of blocks that define variable v 357 | for var in variables: 358 | for defining_block in defs[var]: 359 | for block in DF[defining_block]: 360 | insert_phi(var, block) if not already there 361 | defs[var].add(block) 362 | ``` 363 | 364 | Note that this is "minimal SSA", but the some of these $\phi$ instructions may still be dead. 365 | Minimal SSA refers to having no "unneccessary" $\phi$ functions, 366 | (i.e., no $\phi$ functions that are dominated by a single definition). 367 | but it's still possible to have some that are dead. 368 | There are other variants of SSA like pruned SSA that try to eliminate these dead $\phi$ functions. 369 | Your dead code elimination pass should be able to remove these just fine. 370 | 371 | ### Rename Variables 372 | 373 | Now that we have $\phi$ functions at the right places, 374 | we can rename variables to ensure that each definition is unique, 375 | and that each use refers to the correct definition. 376 | 377 | ```py 378 | stack[var] = [] # stack of names for each variable 379 | dom_tree[b] = list of children of block b in the dominator tree 380 | i.e., blocks that are *immediately* dominated by b 381 | def rename(block): 382 | remember the stack 383 | 384 | for inst in block: 385 | inst.args = [stack[arg].top for arg in inst.args] 386 | fresh = fresh_name(inst.dest) 387 | stack[inst.dest].push(fresh) 388 | inst.dest = fresh 389 | for succ in block.successors: 390 | for phi in succ.phis: 391 | v = phi.dest 392 | update the arg in this phi corresponding to block to stack[v].top 393 | for child in dom_tree[block]: 394 | rename(child) 395 | 396 | restore the stack by popping what we pushed 397 | ``` 398 | 399 | If you're in a functional language, you can use a more functional style and pass the stack around as an argument. 400 | 401 | Note that when you update the phi argument, if you don't have a name on the stack, then you should just rename it to something undefined to indicate that indeed the variable is not defined when coming from that predecessor. 402 | 403 | ## Converting Out of SSA 404 | 405 | Converting out of SSA is a much simpler. 406 | An easy way to remove all the $\phi$ functions 407 | is to simply insert a copy instruction at each of the arguments of the $\phi$ function. 408 | 409 | ``` 410 | .a: 411 | x_1 = 1; 412 | jump .c 413 | .b: 414 | x_2 = 2; 415 | jump .c 416 | .c: 417 | x_3 = phi .a x_1 .b x_2 418 | use x_3 419 | ``` 420 | 421 | Becomes: 422 | 423 | ``` 424 | .a: 425 | x_1 = 1; 426 | x_3 = x_1; 427 | jump .c 428 | .b: 429 | x_2 = 2; 430 | x_3 = x_2; 431 | jump .c 432 | .c: 433 | # phi can just be removed now 434 | use x_3 435 | ``` 436 | 437 | This is quite simple, but you can see that it introduces some silly-looking copies. 438 | 439 | There are many techniques for removing these copies (some of your passes might do this automatically). 440 | You can also use a more sophisticated algorithm to come out of SSA form to prevent introducing these copies in the first place. 441 | Chapter 3.2 of the [SSA Book](https://pfalcon.github.io/ssabook/latest/book-full.pdf) has a good overview of these techniques. 442 | 443 | 444 | 445 | # Task 446 | 447 | There is no task for this lesson. 448 | Later lectures and tasks will sometimes assume that you can convert to SSA form. 449 | 450 | If you implement SSA conversion in your compiler 451 | (I highly recommend it!), 452 | tell me about it in whichever task you first use it. 453 | 454 | 455 | 456 | 457 | -------------------------------------------------------------------------------- /lessons/04-loops.md: -------------------------------------------------------------------------------- 1 | # Loop Optimization 2 | 3 | Loop optimizations are a key part of optimizing compilers. 4 | Most of a program's execution time will be spent on loops, 5 | so optimizing them can have a big impact on performance! 6 | 7 | Resources: 8 | - [LICM](http://www.cs.cmu.edu/afs/cs/academic/class/15745-s19/www/lectures/L9-LICM.pdf) 9 | and [induction variable](https://www.cs.cmu.edu/afs/cs/academic/class/15745-s19/www/lectures/L8-Induction-Variables.pdf) 10 | slides from CMU's 15-745 11 | - [Compiler transformations for high-performance computing](https://dl.acm.org/doi/10.1145/197405.197406), 1994 12 | - Sections 6.1-6.4 deal with loop optimizations 13 | - Course notes from Cornell's CS 4120 on [induction variables](https://www.cs.cornell.edu/courses/cs4120/2019sp/lectures/27indvars/lec27-sp19.pdf) 14 | 15 | ## Natural Loops 16 | 17 | Before we can optimize loops, we need to find them in the program! 18 | 19 | Many optimizations focus on so-called _natural loops_, loops 20 | with a single entry point. 21 | 22 | In this CFG, there is a natural loop formed by nodes A-E. 23 | 24 | ```mermaid 25 | graph TD 26 | A[A: preheader] --> B[B: loop header] 27 | B --> C & D --> E --> B 28 | D & B --> F[F: exit] 29 | ``` 30 | 31 | Some things to observe: 32 | - A natural loop has a _single entry point_, which we call the _loop header_ 33 | - the loop header dominates all nodes in the loop! 34 | - this makes the loop header essential for loop optimizations 35 | - Frequently, a loop will have a _preheader_ node 36 | - this is a node that is the _only_ predecessor of the loop header 37 | - useful for loop initialization code 38 | - if there is no preheader, we can create one by inserting a new node 39 | - The loop may have many exits 40 | 41 | The following is not a natural loop, because there is no single entry point. 42 | 43 | ```mermaid 44 | graph TD 45 | A --> B & C 46 | B --> C 47 | C --> B 48 | ``` 49 | 50 | Natural loops also allow us to classify certain edges in the CFG as _back edges_. 51 | A back edge is an edge in CFG from node A to node B, where B dominates A. 52 | Every back edge corresponds to a natural loop in the CFG! 53 | In the natural loop example above, the edge from E to B is a back edge. 54 | Note how the non-natural loop example has no back edges! 55 | 56 | ### Reducibility 57 | 58 | An alternative and more general definition of a 59 | loop in a CFG would be to look for strongly-connected components. 60 | However, this is typically less useful for optimization. 61 | In this class, we will focus on natural loops. 62 | 63 | A CFG with only natural loops is called _reducible_. 64 | If you're building a compiler from a language with structured control flow, 65 | (if statements, while loops, etc), 66 | the CFG will be reducible. 67 | If you have arbitrary control flow (like goto), you may have irreducible CFGs. 68 | 69 | There are techniques to reduce an irreducible CFG to a reducible one, 70 | but we won't cover them in this class. 71 | The vast majority of programs you will encounter will have reducible CFGs. 72 | We'll only talk about optimizing natural loops, and you may just ignore the non-natural loops for now. 73 | 74 | ### Nested and Shared Loops 75 | 76 | Natural loops may share a loop header: 77 | 78 | ```mermaid 79 | graph TD 80 | A[A: header] --> B --> A 81 | A --> C --> A 82 | ``` 83 | 84 | And they may be nested: 85 | 86 | ```mermaid 87 | graph TD 88 | A[A: header 1] --> B[B: header 2] --> C --> B 89 | B --> D --> A 90 | ``` 91 | 92 | In the nested loop example, 93 | node C is in the inner loop defined by the back edge from C to B, 94 | but is it in the outer loop? 95 | The outer loop is defined by the back edge from D to A. 96 | 97 | We define a natural loop in terms of the back edge that defines it. 98 | It's the smallest set of nodes that: 99 | - contains the back edge that points to the loop header 100 | - has no predecessors outside the set 101 | - except for the predecessors of the loop header 102 | 103 | By this definition, node C _is_ in the outer loop! 104 | It is a predecessor of B, 105 | which is not the loop header of the outer loop. 106 | 107 | ### Finding Natural Loops 108 | 109 | Armed with that definition, we can find natural loops in a CFG. 110 | We will go about this by first finding back edges, then finding the loops they define. 111 | 112 | 1. Compute the dominance relation 113 | 2. Find back edges (A -> B, where B dominates A) 114 | 3. Find the natural loops defined by the back edges 115 | - if backedge does from N -> H (header) 116 | - many ways to find to loop nodes 117 | - find those nodes that can reach N without going through H 118 | - those nodes plus H form the loop 119 | 120 | ### Loop Normalization 121 | 122 | It may be convenient to normalize the structure of loops 123 | to make them easier to optimize. 124 | For example, LLVM normalizes loops to have the following structure: 125 | - Pre-header: The sole predecessor of the loop header 126 | - Header: The entry point of the loop 127 | - Latch: A single node executed for looping; the source of the back edge 128 | - Exit: An exit node that is guaranteed to be dominated by the header 129 | 130 | You might find it useful to do some normalization. 131 | In particular, you may want to add a preheader if it doesn't exist, 132 | as it's a convenient place to put loop initialization code. 133 | You may also want loop headers (and pre-headers) to be unique to a loop, 134 | that way a loop is easily identified by its header. 135 | 136 | Consider the following CFG where two natural loops share a header: 137 | 138 | ```mermaid 139 | graph TD 140 | A[A: header] --> B --> A 141 | A --> C --> A 142 | ``` 143 | 144 | You could normalize by combining the two loops into one: 145 | ```mermaid 146 | graph TD 147 | A[A: header] --> B --> D[latch] 148 | A --> C --> D 149 | D --> A 150 | ``` 151 | 152 | ## Loop Invariant Code Motion (LICM) 153 | 154 | A classic loop optimization is _loop invariant code motion_ (LICM), 155 | which is a great motivation for finding and normalizing loops with a pre-header. 156 | 157 | The goal is to move code out of the loop that doesn't depend on the loop iteration. 158 | Consider the following loop: 159 | 160 | ```c 161 | i = 0; 162 | do { 163 | i++; 164 | c = a + b; 165 | use(c); 166 | } while (cond); 167 | ``` 168 | 169 | The expression `a + b` is loop invariant, as it doesn't change in the loop. 170 | So we could get some savings by moving it out of the loop: 171 | 172 | ```c 173 | i = 0; 174 | c = a + b; 175 | do { 176 | i++; 177 | use(c); 178 | } while (cond); 179 | ``` 180 | 181 | ### Requirements for LICM 182 | 183 | Why does the above example work? And why does it use a do-while loop instead of a while loop? 184 | 1. Moving the code doesn't change the semantics of the program 185 | - watch out for redefinitions of the same variable in a loop! 186 | - if you're SSA, it's a bit easier 187 | 2. The moved instruction dominated all loop exits 188 | - in other words, it's guaranteed to execute! 189 | 190 | Requirement 2 is why we use a do-while loop. 191 | In a while loop, none of the code in the body is guaranteed to execute, 192 | so none of it dominates all loop exits. 193 | Many loops won't look like this, 194 | so what can you do? 195 | 196 | The first approach is to ignore the second requirement. 197 | On one hand, this will allow you to directly implement LICM on while loops. 198 | On the other hand, 199 | you now have some new performance _and_ correctness issues to worry about. 200 | Suppose the example above was a while loop. 201 | Even though `c = a + b` is loop invariant, 202 | it's not guaranteed to execute. 203 | So if `cond` is false on the first iteration, 204 | we will have make our program worse by moving the code out of the loop! 205 | 206 | Even worse, what if the operation was something more complex than addition? 207 | If it has side effects, we have changed the meaning of the program! 208 | Still, this can be a useful optimization in practice 209 | if you're careful to limit it to pure, side-effect-free code. 210 | 211 | ### Loop Splitting/Peeling 212 | 213 | Another approach to enabling LICM (and some other loop optimizations) 214 | is to perform _loop splitting_, sometimes called _loop peeling_. 215 | The effect is to convert a while loop into a do-while loop: 216 | 217 | ```c 218 | while (cond) { 219 | body; 220 | } 221 | ``` 222 | 223 | becomes 224 | 225 | ```c 226 | if (cond) { 227 | do { 228 | body; 229 | } while (cond) 230 | } 231 | ``` 232 | 233 | In CFG form, the while loop: 234 | 235 | ```mermaid 236 | graph TD 237 | P[preheader] --> A 238 | A[header] --> B[body] 239 | B --> A 240 | A --> C[exit] 241 | ``` 242 | 243 | becomes the do-while loop: 244 | 245 | ```mermaid 246 | graph TD 247 | X[header copy] --> B1 & C 248 | B1[preheader] --> B[body] 249 | A[header] --> B[body] 250 | B --> A 251 | A --> C[exit] 252 | ``` 253 | 254 | Note that in the converted do-while loop, the block labeled body is actually the natural loop header for the loop. 255 | Once in this form, code from the block labeled `body` (actually the header) dominates all loop exits, 256 | so you can apply LICM to it. 257 | 258 | In loops like do-while loops, 259 | you can even perform LICM on some code that is side-effecting, 260 | as long as those side effects are idempotent. 261 | A good example would be division. 262 | In general, 263 | it's not safe to move division out of a loop 264 | if you consider crashing the program to be a side effect 265 | (you could also make it undefined behavior). 266 | But in a loop of this structure, 267 | you can move loop invariant divisions into the preheader. 268 | 269 | Another way to phrase this transformation is that 270 | code in the loop header dominates all loop exits automatically. 271 | So can be easier to move loop invariant code out of header of a loop than out of other nodes. 272 | Loop splitting can be seen as a way to make more loop code dominate loop exits; 273 | in simple cases it essentially turns the loop body into the loop header. 274 | 275 | ### Finding Loop Invariant Code 276 | 277 | Now it just remains to identify which code is loop invariant. 278 | This too is a fixed point, but specific to the code in a loop. 279 | 280 | We will limit ourselves to discussing the SSA form of the code. 281 | If you're not in SSA form, you'll have to do some extra work 282 | to reason about reaching definitions and avoiding redefinitions. 283 | 284 | A value is loop invariant 285 | if it will always have the same value through out the execution of the loop. 286 | Loop invariance is a property of a value/instruction with respect to a particular loop. 287 | In nested loops, some code may be loop invariant with respect to one loop, 288 | but not another. 289 | 290 | A value (we are in SSA!) is loop invariant if either: 291 | - It is defined outside the loop 292 | - It is defined inside the loop, and: 293 | - All arguments to the instruction are loop invariant 294 | - The instruction is deterministic 295 | 296 | The last point is important. 297 | Consider the instruction `load 5` which loads from memory location 5. 298 | Is that loop invariant? 299 | It depends on if the loop changes memory location 5! 300 | We can maybe figure that out with some kind of analysis, 301 | but for now, we will consider that loads and stores cannot be loop invariant. 302 | 303 | This definition should give you enough to iterate to a fixed point! 304 | 305 | ## Induction Variables 306 | 307 | Another quintessential elements of a loop in an _induction variable_, 308 | typically defined as a variable that is incremented or decremented by some constant amount each iteration. 309 | 310 | Consider the following C code: 311 | ```c 312 | // a is an array of 32-bit ints 313 | for (int i = 0; i < N; i++) { 314 | a[i] = 42; 315 | } 316 | ``` 317 | 318 | And in a (non-bril) IR 319 | ``` 320 | i = 0 321 | .loop.header: 322 | c = i < N 323 | branch c .loop.body .loop.exit 324 | .loop.body: 325 | offset = i * 4 326 | addr = a + offset 327 | store 42 addr 328 | i = i + 1 329 | jump .loop.header 330 | .loop.exit: 331 | ... 332 | ``` 333 | 334 | In the above code, 335 | `i` is the loop counter, 336 | and in this case it is also an so-called _basic induction variable_ 337 | since it is incremented by a constant amount each iteration. 338 | The variables `offset` and `addr` are _derived induction variables_, 339 | since they are a function of the basic induction variable. 340 | Typically we restrict these deriving functions to be linear with respect to the basic induction variables: 341 | so of some form `j = c * i + d` where: 342 | - `j` is the derived induction variable 343 | - `i` is the basic induction variable 344 | - `c` and `d` are loop invariant with respect to `i`'s loop (typically constants) 345 | 346 | Loop induction variables are part of a couple classic loop optimizations, 347 | namely induction variable elimination and strength reduction[^1] 348 | which we will discuss in the next section. 349 | But they are also important for unrolling, loop parallelization and interchange, and many other loop optimizations. 350 | 351 | [^1]: Sometimes people use "strength reduction" to refer to the more general optimization of replacing expensive operations with cheaper ones, even outside of loops. Sometimes it's used specifically to refer to this optimization with respect to induction variables. 352 | 353 | 354 | In the above example, 355 | the key observation is that `addr = a + i * 4`, and `a` is loop invariant. 356 | This fits one of a set of commons patterns that allows us to perform a "strength reduction", 357 | replacing the relatively expensive multiplication with a cheaper addition. 358 | Instead of multiplying by 4 each iteration, we can just add 4 each iteration, and initialize `addr` to `a`. 359 | 360 | ``` 361 | i = 0 362 | addr = a 363 | .loop.header: 364 | c = i < N 365 | branch c .loop.body .loop.exit 366 | .loop.body: 367 | store 42 addr 368 | addr = addr + 4 369 | i = i + 1 370 | jump .loop.header 371 | ``` 372 | 373 | Above I've also removed some dead code for the old calculation of `addr`. 374 | Now we can observe that `addr` is now a basic induction variable 375 | instead of a derived induction variable. 376 | 377 | Great, we've optimized the loop by removing a multiplication! 378 | But we can go further by observing that `i` is now only used to compute the loop bound. 379 | We can instead compute the loop bound in terms of `addr`, which allows us to eliminate `i` entirely. 380 | 381 | ``` 382 | i = 0 383 | addr = a 384 | bound = a + N * 4 385 | .loop.header: 386 | c = addr < bound 387 | branch c .loop.body .loop.exit 388 | .loop.body: 389 | store 42 addr 390 | addr = addr + 4 391 | jump .loop.header 392 | ``` 393 | 394 | Loop optimized! 395 | 396 | 397 | ### Finding Basic Induction Variables 398 | 399 | Of course to do any optimization with induction variables, 400 | you need to be able to find them. 401 | There are many approaches, 402 | and we will read about one 403 | in the paper "[Beyond Induction Variables](../reading/beyond-induction-variables.md)". 404 | 405 | I will discuss an approach based on SSA, you can also take a dataflow approach, 406 | which the Dragon Book and these [notes from Cornell](https://www.cs.cornell.edu/courses/cs4120/2019sp/lectures/27indvars/lec27-sp19.pdf) cover quite well. 407 | The dataflow approach is quite elegant and worth looking at! 408 | It operates with a lattice based on maps from variables to triples: `var -> (var2, mult, add)`. 409 | These dataflow approaches are totally compatible with SSA form 410 | (and SSA makes them easier to implement since you don't have to worry about redefinitions). 411 | I will instead discuss a simple approach that directly analyzes the SSA graph, 412 | as SSA makes basic induction variable finding quite easy. 413 | 414 | We begin by looking for basic induction variables of the form `i = i + e` where `e` is some loop invariant expression. 415 | If you're just getting started, you can limit yourself to constant `e`s. 416 | 417 | The essence of finding induction variables in SSA form is to look for cycles in the SSA graph. 418 | For a basic induction variable, you're looking for a cycle of the form `i = phi(base, i + incr)`. 419 | Graphically: 420 | 421 | ```mermaid 422 | graph TD 423 | A["i: phi(base, i + incr)"] 424 | B[base] 425 | C[i + incr] 426 | D[incr] 427 | A --> B & C 428 | C --> A & D 429 | ``` 430 | 431 | ### Finding Derived Induction Variables 432 | 433 | For derived induction variables `j`, you're looking for a pattern of the form `j = c * i + d` where `c` and `d` are loop invariant and `i` is a basic induction variable. 434 | Induction variables derived from the same basic induction variable are said to be in the same _family_ as the basic induction variable. 435 | Again, you can limit yourself to constant `c` and `d` to start. 436 | We can do this with a dataflow analysis as well, but we will stick to directly inspecting the SSA graph. 437 | 438 | Note that you will need to consider separate patterns for the commutative cases, and cases where one of `c` or `d` is 0. 439 | 440 | ```mermaid 441 | graph TD 442 | A["j = c * i + d"] --> B & C 443 | B["c * i"] --> B1["c"] & B2["i"] 444 | C["d"] 445 | ``` 446 | 447 | ### Replacing Induction Variables 448 | 449 | The purpose of limiting ourselves to simple linear functions is that we 450 | can easily replace derived induction variables with basic induction variables. 451 | 452 | For a derived induction variable `j = c * i + d`: 453 | - `i` is a basic induction variable, with base `base` and increment `incr` 454 | - Initialize `j0` to `c * base + d` in the preheader. 455 | - In the loop, redefine `j` to `j = phi(j0, j + c * incr)` 456 | - `c * incr` is definitely loop invariant, probably constant! 457 | 458 | ### Replacing Comparisons 459 | 460 | In many cases, once you replace the derived induction variables, 461 | the initial basic induction variable is only needed to compute the loop bounds. 462 | If that's the case, 463 | you can replace the comparison to operate on another induction variable from the same family. 464 | 465 | Consider `i < N` as a computation of a loop bound. 466 | And say we have a derived induction variable `j = c * i + d`. 467 | Some simple algebra gives us the transformation: 468 | ``` 469 | i < N 470 | c * i + d < c * N + d 471 | j < c * N + d 472 | ``` 473 | 474 | So we can replace the comparison `i < N` with `j < c * N + d`. 475 | With that, a dead code analysis will be able to eliminate `i` entirely (if it's not used elsewhere). 476 | 477 | ### Induction Variable Families 478 | 479 | In general, induction variables in the same family can be expressed in terms of each other. 480 | As we saw above, a derived induction variable can be "lowered" into it's own basic induction variable as well 481 | via strength reduction, 482 | so basic induction variables can be expressed in terms of each other sometimes as well. 483 | This opens up a wider question/opportunity for optimization: 484 | for a family of induction variables, 485 | what do we want to do? 486 | We could try to eliminate all but one of the basic induction variables, 487 | or we could try to replace all derived induction variables with basic induction variables. 488 | 489 | These have different trade-offs: 490 | more basic induction variables may mean better strength reduction, 491 | but it can also increase register pressure, as you have more variables to keep live across the loop. 492 | On the other hand, you can imagine trying to "de-optimize" code in the reverse process, 493 | trying to express as many derived induction variables as possible in terms of a single basic induction variable. 494 | LLVM [does such a thing](https://llvm.org/doxygen/classllvm_1_1Loop.html#ae731e6e33c2f2a9a6ebd1d51886ce534) 495 | in fact it tries to massage the loop to have a single basic induction variable that starts at 0 and increments by 1. 496 | 497 | The classic strength reduction optimization as we discussed above is a case where the trade-off is very favorable. 498 | We begin with 1 basic (`i`) and 2 derived induction variables (`offset` and `addr`). 499 | and we end up with 1 basic induction variable (`addr`) only. 500 | 501 | # Task 502 | 503 | This task will be to implement _some kind_ of loop optimization. 504 | It's up to you! 505 | If you aren't sure, 506 | consider doing a basic LICM implementation, 507 | as it has a good payoff for the effort. 508 | 509 | As always, run your optimizer on the entire benchmark suite in `benchmarks/`. 510 | Make sure to include some kind of summary statistics (average, min/max... or a plot!) in your writeup. 511 | -------------------------------------------------------------------------------- /lessons/05-memory.md: -------------------------------------------------------------------------------- 1 | # Memory 2 | 3 | You have probably encountered memory instructions like `load` and `store` in Bril programs, 4 | and so far we have mostly ignored them or worse, said that they have prevented other optimizations. 5 | 6 | Resources: 7 | - [15-745 slides](https://www.cs.cmu.edu/afs/cs/academic/class/15745-s19/www/lectures/L13-Pointer-Analysis.pdf) 8 | 9 | 10 | ## Simple Memory Optimizations 11 | 12 | Our motivating goal for this lesson is to be able to perform 13 | some similar optimizations 14 | that we did on variables on memory accesses. 15 | In the first couple lessons, we performed the following optimizations: 16 | - **Dead code elimination** removed instructions that wrote to variables that were never read from (before being written to again). 17 | - **Copy propagation** replaced uses of variables with their definitions. 18 | - **Common subexpression elimination** prevented recomputing the same expression multiple times. 19 | 20 | We can imagine similar optimizations for memory accesses. 21 | - **Dead store elimination** removes stores to memory that are never read from. 22 | Like our initial trivial dead code elimination, this is simplest to recognize 23 | when you have two stores close to each other. 24 | - Just like `x = 5; x = 6;` can be replaced with `x = 6;`... 25 | - We can replace `store x 5; store x 6;` with `store x 6;`. 26 | - **Store-to-load forwarding** replaces a load with the most recent store to the same location. 27 | This is similar to copy propagation, but for memory. 28 | - Just like `x = a; y = x;` can be replaced with `x = a; y = a`... 29 | - We can replace `store x 5; y = load x;` with `store x 5; y = 5;`. 30 | - **Redundant load elimination** works on a similar redundancy-elimination principle to common subexpression elimination. 31 | - Just like `x = a + b; y = a + b;` can be replaced with `x = a + b; y = x;`... 32 | - We can replace `x = load p; y = load p;` with `x = load p; y = x;`. 33 | 34 | 35 | As written, 36 | these optimizations are not very useful 37 | since they only apply when the memory accesses are right next to each other. 38 | How can we extend them? 39 | 40 | To extend them, 41 | we first need to see 42 | how we could go wrong. 43 | In each of these cases, 44 | an intervening memory instruction would make the optimization invalid. 45 | For example, say we are trying to do dead store elimination on the following code: 46 | ``` 47 | store x 5; 48 | ... 49 | store x 6; 50 | ``` 51 | 52 | Whether or not we can eliminate the first store depends on what happens between the two stores. 53 | In particular, 54 | a problematic instruction would be a load from `x`, 55 | since it would observe the value `5` that we are trying to eliminate. 56 | 57 | So a sound, very conservative approach would be to say that we can only perform these optimizations 58 | if we can prove that there are **no** intervening memory instructions at all. 59 | 60 | What about a load from some other pointer `y`? 61 | That might break the optimization, but it might not! 62 | It could be the case that `y` is actually an *alias* for `x`, 63 | and so the load from `y` would observe the value `5` and break the optimization. 64 | Or, `y` could be completely unrelated to `x`, 65 | in which case we could still eliminate the first store! 66 | 67 | ## Alias Analysis 68 | 69 | This is the essence of a huge field of compilers/program analysis research called *alias analysis*. 70 | The purpose of such an analysis is to answer one question: 71 | can these two pointers (`x` and `y` in the above) in the program refer to the same memory location? 72 | 73 | If we can prove they cannot (they don't alias), 74 | then we can perform the above optimization and many others, 75 | since we don't have to worry about interactions between the two pointers. 76 | If we can't prove they don't alias, 77 | then we have to be conservative and assume they do alias, 78 | which will prevent us from performing many optimizations. 79 | 80 | There are many, many algorithms for alias analysis 81 | that vary in their precision, efficiency, and assumptions about the memory model. 82 | Exploring these would make for a good project! 83 | 84 | We will present a simple alias analysis powered by, what else, dataflow analysis! 85 | The idea is to track the memory locations that each pointer can point to, 86 | and then use that information to determine if two pointers can point to the same location. 87 | 88 | ### Memory Locations 89 | 90 | What's a memory location? 91 | In some languages, like C with its address-of operator `&`, 92 | you can get a pointer to a memory location in many ways. 93 | But in Bril, 94 | there is only one way to create a memory location, 95 | calling the `alloc` instruction. 96 | The `alloc` instruction _dynamically_ returns a fresh pointer 97 | to a memory location that is not aliased with any other memory location. 98 | 99 | How can we model that in a static analysis? 100 | We can't! 101 | So we will be conservative 102 | and "name" the memory locations according to the (static) program location where they were allocated. 103 | This is a standard approach in alias analysis, 104 | since it allows you to work with a finite set of memory locations. 105 | 106 | So for example, if we have the following program: 107 | ```c 108 | while (...) { 109 | x = alloc 10; // line 1 110 | y = alloc 10; // line 2 111 | } 112 | ``` 113 | 114 | We now can statically say what `x` points to: the memory location allocated at line 1. 115 | This is a necessary approximation to avoid having to name the potentially infinite number of memory locations that could be allocated in a loop. 116 | Note how we can still reason about the non-aliasing of `x` and `y` in this case. 117 | 118 | ### A Simple Dataflow Analysis 119 | 120 | While `alloc` is the only way to create a memory location from nothing, 121 | there are other ways to create memory locations, and these are the sources of aliasing. 122 | 123 | - `p1 = id p2`: the move or copy instruction is the most obvious way to create an alias. 124 | - `p1 = ptradd p2 offset`: pointer arithmetic is an interesting and challenging source of aliasing. 125 | To figure out if these two pointers can alias, we'd need to figure out if `offset` can be zero. 126 | To simplify things, we will assume that `offset` could always be zero, 127 | and so we will conservatively say that `p1` and `p2` can alias. 128 | This also means we do not need to include indexing informations into our represetation of memory locations. 129 | - `p1 = load p2`: you can have pointers to other pointers, and so loading a pointer effectively copy a pointer, creating an alias. 130 | 131 | Our dataflow analysis 132 | will center around building the _points-to_ graph, 133 | a structure that maps each variable to the set of memory locations it can point to. 134 | 135 | Here is a first, very conservative stab at it: 136 | 137 | - **Direction**: forward 138 | - **State**: map of `var -> set[memory location]` 139 | - If two vars have a non-empty intersection, they might alias! 140 | - **Meet**: Union for each variable's set of memory locations 141 | - **Transfer function**: 142 | - `x = alloc n`: `x` points to this allocations 143 | - `x = id y`: `x` points to the same locations as `y` did 144 | - `x = ptradd p offset`: same as `id` (conservative) 145 | - `x = load p`: we aren't tracking anything about p, so `x` points to all memory locations 146 | - `store p x`: no change 147 | 148 | This is a very conservative but still useful analysis! 149 | If your program doesn't have any pointers-to-pointers, 150 | then this big approximation of not modeling what `p` might point to isn't so bad. 151 | 152 | This is, however, still an _intra_-procedural analysis. 153 | So we don't know anything about the incoming function arguments. 154 | Ensure that you initialize your analysis something conservative, 155 | like all function arguments pointing to all memory locations. 156 | 157 | # Task 158 | 159 | Implement an alias analysis for Bril programs 160 | and use it to perform at least one of the memory optimizations we discussed above. 161 | 162 | You may implement any alias analysis you like, 163 | including the simple one described above. 164 | If you are interested in a more advanced analysis, 165 | you could look into the _Andersen_ analysis, 166 | or the more efficient but less precise _Steensgaard_ analysis. 167 | 168 | As always, run your optimizations on the provided test cases 169 | and make sure they pass. 170 | Show some data analysis of the benchmarking results. 171 | 172 | In addition, 173 | create at least one contrived example that shows your optimization in action. 174 | It can be a simple code snippet, 175 | but show the before and after of the optimization. 176 | -------------------------------------------------------------------------------- /lessons/06-interprocedural.md: -------------------------------------------------------------------------------- 1 | # Interprodural Optimization and Analysis 2 | 3 | Most of the techniques we have discussed so far have been limited to a single function. 4 | But we have, however, made the jump from reasoning about a single basic block to reasoning about a whole function. 5 | Let's recap how we did that. 6 | 7 | ## Intraprocedural: Local to Global 8 | 9 | A local analysis or optimization doesn't consider any kind of control flow. 10 | To extend beyond a single basic block, 11 | we needed to figure out how to carry information from one block to another. 12 | 13 | Consider the following CFG: 14 | 15 | ```mermaid 16 | graph TD 17 | A --> B & C --> D 18 | ``` 19 | 20 | As we saw in the first lesson about [extended basic blocks](./01-local-opt.md#extended-basic-blocks), 21 | you can "for free" reuse the information from a block the dominates you (for a forward analysis). 22 | In the above example, blocks B and C can reuse information from block A since they are both dominated by A. 23 | In fact, similar for block D, but you'd have to be a little careful about variable redefinitions. 24 | But how do we get information from blocks B and C into D? 25 | 26 | ### The Problem with Paths 27 | 28 | First of all, what's even the problem here? Why is hard to reason about block D? 29 | The issue that is that our analysis is attempted to summarize all possible executions of a program up to a point. 30 | And when there are joins in the control flow, 31 | there are multiple paths that could have led to that point. 32 | When there are cycles, there are infinitely many paths! 33 | So the issue with global (and as we'll see later, interprocedural) 34 | analysis is that we need to handle multiple or potentially infinite paths of execution in the state of our analysis. 35 | 36 | ### Summarize 37 | 38 | The main approach that we took in this class was to summarize the information from multiple paths 39 | using the monotone framework of dataflow analysis. 40 | When control converges in a block (i.e. there are multiple ways to get to a block), 41 | we combine the analysis results from each path using a "meet" operation. 42 | This meet operation has nice algebraic properties 43 | that ensure we can always do this without spinning out of control in, for example, a loop. 44 | However, it loses information! 45 | Recall that some analyses, like constant propagation, 46 | are not distributive over the meet operation. 47 | 48 | In our dataflow analyses, 49 | we have both optimistic and pessimistic analyses. 50 | An optimistic analysis might result in more precision, 51 | but you have to wait until it's converged (until all paths have been considered) 52 | before you can trust the results. 53 | A pessimistic analysis might be less precise, 54 | but you can trust the results at any point in the analysis. 55 | We will see this tradeoff again in interprocedural analysis and the open-world assumption. 56 | 57 | ### Duplicate 58 | 59 | The other approach (that we briefly discussed) is to copy! 60 | If you simply duplicate block D: 61 | ```mermaid 62 | graph TD 63 | A --> B --> D1 64 | A --> C --> D2 65 | ``` 66 | Then you can apply the analysis to D2, and then merge the results back into D. 67 | In this case, you could actually merge blocks D1 and D2 back into their predecessor blocks. 68 | In an acyclic CFG, you can always do this, eliminating any convergence at potentially exponential cost. 69 | Here's the pessimal CFG to duplicate that witnesses the exponential explosion: 70 | ```mermaid 71 | graph TD 72 | A1 & A2 --> B1 & B2 --> C1 & C2 --> D1 & D2 73 | ``` 74 | 75 | An of course, CFGs with cycles cannot be fully duplicated in this way, since there are infinitely many paths. 76 | 77 | But! You can always partially apply this technique and combine it with summarization. 78 | Loop unrolling is a classic example of this technique. 79 | Practical application of basic block duplication (in acyclic settings) will also 80 | only do this sometimes, leaving the rest to the summarization technique. 81 | 82 | ### Contextualize 83 | 84 | This is very similar to duplication, 85 | but instead of exploding the graph, 86 | you keep track of some context 87 | (in the case of a CFG, the path that led to the block) 88 | in the analysis state. 89 | Of course, there are infinitely many paths in a cycle, 90 | so you have to be careful about how you do this. 91 | A common approach is to finitize the context, 92 | for example by only keeping track of the last few blocks that led to the current block. 93 | For a global dataflow analysis, this approach is called _path-sensitive analysis_. 94 | We saw call-site sensitivity in the context of interprocedural analysis 95 | in last week's reading on the [Doop framework](../reading/doop.md). 96 | 97 | ## Interprocedural Analysis 98 | 99 | Of course, most programs are not just a single function. 100 | The interaction between functions is modeled by the _call graph_, 101 | very similar to the control flow graph can basic blocks. 102 | 103 | Let's saw I have a library that defines some public functions `lib_N` with private helpers `helper_N`: 104 | 105 | ```mermaid 106 | graph TD 107 | lib_1 --> helper_1 --> helper_2 --> helper_3 108 | lib_2 --> helper_1 109 | lib_2 --> lib_1 110 | lib_3 --> helper_5 --> helper_4 111 | 112 | helper_2 --> helper_4 113 | ``` 114 | 115 | In a CFG, one of the basic optimization is dead code elimination (on the block level). 116 | If a block is not reachable from the entry block, then it is dead code. 117 | But if you're compiling a library, you don't know what the entry block is, since you don't know what the user's program will look like. 118 | 119 | ### Open World 120 | 121 | This is the main challenge of interprocedural analysis: **the open-world assumption**. 122 | In most compilation settings, 123 | you don't know what the whole program looks like. 124 | There are a couple main reasons for this, which roughly fall into two categories: 125 | - separate compilation 126 | - where parts of the program are compiled separately and then linked together 127 | - this enables you to compile and distribute libraries 128 | - also useful for faster, incremental compilation 129 | - dynamic code 130 | - even worse, you might not know what the whole program looks like at runtime 131 | - in many programming languages, you can load code at runtime 132 | - so you might not know what the whole program looks even once the program is running! 133 | 134 | Thankfully, 135 | many of the techniques we've discussed so far can be extended to interprocedural analysis, 136 | even in the open world setting using similar techniques over the call graph. 137 | But summarization must be pessimistic, since you don't know what the whole program looks like. 138 | 139 | ### Closed World 140 | 141 | Some, like dead code analysis, don't really work in the open world setting since they need to be optimistic. 142 | There are some settings where you can in fact do whole-program analysis 143 | where you can make the **closed-world assumption**. 144 | The big ones are: 145 | - [LTO](https://en.wikipedia.org/wiki/Interprocedural_optimization#WPO_and_LTO) or link-time optimization. Even when the program is separately compiled, 146 | there is typically a final linking step where you can see the whole program. Modern compilers like GCC and LLVM will do some optimizations at this stage. 147 | - JIT (just-in-time) compilation. In this case, the compiler is present at runtime! 148 | This gives the compiler the superpower of *making assumptions*. The compiler can basically assume anything it wants about the program as long as it can check it at runtime. If the assumption is true, great, you can use the code optimized for that assumption. If not, you can fall back to the unoptimized code. 149 | 150 | ### Inlining 151 | 152 | Summarization and contextuality are weakened in the open-world setting, as you can't do anything optimistically. 153 | However, duplication still works! 154 | 155 | In the interprocedural setting, duplication manifests as **inlining**. 156 | Intuitively, inlining is the process of replacing a function call with the body of the function. 157 | 158 | Inlining is frequently referred to the most important optimization in a compiler 159 | for the following reasons: 160 | - Primarily, inlining can enable many other optimizations 161 | - It also removes the overhead of the function call itself 162 | - the call/return instruction 163 | - stack manipulation, argument passing, etc. 164 | 165 | The downsides to inlining are essentially the same of basic-block duplication: 166 | - code size increase, which can decrease instruction cache locality 167 | - increased compile time 168 | - impossible to inline recursive functions 169 | 170 | To illustrate the power of inlining, 171 | here are two (contrived) examples that work a little differently. 172 | 173 | The first shows how inlining can let precise information flow "into" a particular call site. 174 | 175 | ```c 176 | int add_one(int x) { 177 | return x + 1; 178 | } 179 | 180 | int caller() { 181 | return add_one(1); 182 | } 183 | 184 | int caller_inlined() { 185 | return 1 + 1; 186 | } 187 | ``` 188 | 189 | Here we can see that the constant argument can easily be propagated (and then folded) into the inlined call site. 190 | Inliners in a compiler will may also consider the callsite itself in addition the called function. 191 | If the callee is given constant arguments, then the inliner may be more likely to inline the function. 192 | 193 | The second example shows information flowing in the other direction. 194 | 195 | ```c 196 | int square(int x) { 197 | return x * x; 198 | } 199 | 200 | int caller(int x) { 201 | while (1) { 202 | int y = square(x); 203 | } 204 | } 205 | 206 | int caller_inlined(int x) { 207 | while (1) { 208 | int y = x * x; 209 | } 210 | } 211 | ``` 212 | 213 | Yes, many properties like loop invariance or purity can also be derived for functions. 214 | But inlining can save you from having to do so! 215 | 216 | 217 | ### When to Inline 218 | 219 | Whether or not to inline a function is very difficult to determine in general. 220 | It depends on what optimizations the inlining would enable, 221 | which might depend on other inlining decisions, 222 | and so on. 223 | Our [reading this week](../reading/optimal-inlining.md) discusses how to find an 224 | optimial point in this tradeoff space by an extremely expensive search. 225 | In practice, it's very difficult to know for sure if inlining a particular function is a good idea, 226 | so you need some heuristics to guide you. 227 | In many cases, the heuristics are simple and based on the size of the function. 228 | 229 | 230 | 231 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /lessons/README.md: -------------------------------------------------------------------------------- 1 | # Lessons 2 | 3 | These are the lecture notes for the course. 4 | 5 | See the parent [README](../README.md) for more info. -------------------------------------------------------------------------------- /project.md: -------------------------------------------------------------------------------- 1 | # CS 265: Final Project 2 | 3 | ## Public Projects! 4 | 5 | Some students have opted to make their projects public! 6 | Check them out, and feel free to reach out to students directly! 7 | I know some are graduating soon and looking for jobs, 8 | so if you're looking for compilers folks, look here! 9 | 10 | 11 | - [Redesigning the Vortex GPU ISA: 64-Bit Instruction Words and Conflict-Aware Register Allocation](https://github.com/richardyrh/cyclotron-cs265) 12 | - Ruohan Richard Yan, Shashank Anand 13 | - [Efficient Register Allocation Algorithms](https://github.com/JacobBolano/cs265_final_project/blob/master/CS_265_Final_Project_Report.pdf) 14 | - Jacob Bolano, Shankar Kailas 15 | - [Compiler Front-end for Translating ChocoPy into Bril](https://github.com/gabe-raulet/chocopy2bril/blob/master/report.pdf) 16 | - Gabe Raulet 17 | - [Bril to RISC-V](https://github.com/ElShroomster/bril_to_riscv) 18 | - Sriram Srivatsan 19 | - [Going to the gym with MLIR: Writing a recompiler for DEX instructions](https://badumbatish.github.io/posts/going_to_mlir_gym_1) 20 | - Jasmine Tang 21 | - [TGO: Trace Guided Optimization](https://github.com/iansseijelly/ltrace_chipyard/tree/CS265-final-project-report) 22 | - Chengyi Lux Zhang 23 | - [Multi-backend support in the cartokit compiler](https://observablehq.com/@parkerziegler/multi-backend-support-in-the-cartokit-compiler) 24 | - Parker Ziegler 25 | 26 | # Project Info 27 | 28 | This course features a project component. 29 | 30 | You may do the project individually or in groups of 2-3 people. 31 | Unlike the reflection assignments, 32 | you should submit a single project report for the group. 33 | 34 | You cannot use late days on the project. 35 | If you think you will need an extension, please talk to me as soon as possible. 36 | 37 | See the [schedule](./README.md#schedule) for due dates. 38 | 39 | **If you are in a group**, 40 | have one person submit the proposal/check-in/report and list the group members in the proposal. 41 | The others should just submit a text entry saying that they are in a group with that person. 42 | 43 | ## Project Proposals 44 | 45 | The first part of the project is to write a proposal. 46 | 47 | The purpose of the proposal is to help you scope out project 48 | that is both interesting and feasible 49 | to complete in the time allotted. 50 | 51 | The proposal should be at least 2-3 pages long and should be submitted as a PDF on bCourses. 52 | 53 | The proposal should include the following sections: 54 | 55 | - Intro 56 | - What are you doing? 57 | - What is your goal? 58 | - Why? 59 | - Background 60 | - What do you already know or need to learn to do this project? 61 | - What pieces of infrastructure will you need? Bril? LLVM? Doop? 62 | - Are you already familiar with these? Or will you need to learn them? 63 | - What parts are already done? 64 | - Approach 65 | - How will you accomplish the things that need to be done? 66 | - What software will you need to build? Algorithms to implement? Papers to read? 67 | - Evaluation plan 68 | - How will you know if you've succeeded? 69 | - What will you measure? 70 | 71 | I will broadly accept projects that are related to any part of compilation, not just the middle-end that we focused on in class. 72 | 73 | Here are some (non-exhaustive) categories that I expect to see projects in: 74 | 1. Expanding your Bril compiler 75 | - Pick a new optimization or class of optimzations to implement and measure 76 | - I expect this will be the most common project, and that's great! 77 | - Expand the Bril infrastructure in some way 78 | - Generate Bril from a new source language 79 | - Add new IR features (parallelism, virtual functions, etc.) 80 | - Implement a backend to a real or virtual architecture 81 | 2. Any of the above in some other compiler infrastructure 82 | - probably only do this if you already have experience with the infrastructure 83 | 3. Connecting up with your current research project / hobby project 84 | - If you're already working on a project that involves compilation 85 | - Still follow the project guidelines above re: goal setting and evaluation 86 | 4. Survey paper 87 | - If you're not interested in implementing something, you can write a survey paper on a topic in compilation 88 | - Still follow the project guidelines above re: goal setting 89 | - "Evaluation" will be a more nuanced reflection on how your report compares with the state of the art. What doesn't it cover? 90 | 91 | Looking for ideas, come chat with me! 92 | For more inspiration, 93 | see what students in the similar [CS 6120](https://www.cs.cornell.edu/courses/cs6120/2023fa/blog/) course at Cornell did in that instance or others. 94 | 95 | ## Project Check-ins 96 | 97 | This is a ~1 page report submitted to bCourses to update me on your progress. It should answer the following questions: 98 | 1. What have you done so far to make progress towards your stated project goals? 99 | 2. Do you need to modify your project goals? If so, how? 100 | 3. What do you plan to do next? 101 | - If you're feeling on track, then let me know what's still to be done. 102 | - If you need to change your project goals, write how you plan to accomplish the new goals. 103 | 104 | ## Project Report 105 | 106 | The project report should be 4-6(ish) pages in length, and should be submitted on bCourses. 107 | Ultimately, the content of the report is flexible, but it should be self-contained 108 | (not relying on the reader to have read your proposal or check-in). 109 | The report should focus on evaluation, to the extent that it makes sense for your project. 110 | Include graphs, tables, and other visualizations as needed. 111 | If you plan to continue work on the project (not required, but some projects are part of a larger research agenda), 112 | include a section on future work. 113 | 114 | ### Making Projects Public 115 | 116 | You may **optionally** choose to make your project public. 117 | If you do this, I will link to your project from the course website, and post to social media saying "look at these cool projects!". 118 | To do this: 119 | 1. Submit only a URL to your project report on bCourses. The URL should point to a public website (github, personal website, etc.) where your project report is hosted. 120 | 2. Include a comment in the submission that says "I would like my project to be public." -------------------------------------------------------------------------------- /reading/beyond-induction-variables.md: -------------------------------------------------------------------------------- 1 | # Beyond Induction Variables 2 | 3 | - Michael Wolfe 4 | - PLDI 1992 5 | - ACM DL [PDF](https://dl.acm.org/doi/pdf/10.1145/143095.143131) 6 | 7 | This paper should be mostly accessible given the lecture material. 8 | 9 | Section 4 is the payoff, 10 | showing off the cool things you can find beyond 11 | standard linear induction variables. 12 | Don't get too bogged down in the details here. 13 | 14 | Section 6 will discuss dependence analysis, 15 | which we have not covered in class. 16 | You should still skim this part, 17 | as it's a good primer for when we do cover it. -------------------------------------------------------------------------------- /reading/braun-ssa.md: -------------------------------------------------------------------------------- 1 | # Simple and Efficient Construction of Static Single Assignment Form 2 | 3 | - Matthias Braun, Sebastian Buchwald, Sebastian Hack, Roland Leißa, Christoph Mallon, and Andreas Zwinkau 4 | - Compiler Construction (CC), 2013 5 | - [DOI link](https://dl.acm.org/doi/10.1007/978-3-642-37051-9_6), and [free PDF](https://c9x.me/compile/bib/braun13cc.pdf) 6 | 7 | Read Sections 1, 2, 3.1, (skip the rest of 3, 4, 5), and read 6-8. 8 | 9 | The skipped sections discuss reducible vs irreducible control flow graphs, 10 | which we will cover in more later in the course. 11 | 12 | One thing to look out for in this paper is the focus on efficiency (of compilation time). 13 | In fact, that was also the original motivation for SSA form 14 | as we saw in last week's reading. 15 | Another thing (also present in last week's reading) is the combination of optimizations, 16 | running them together in a single pass. 17 | 18 | Submit your reading reflection on bCourses before noon on the day of discussion. -------------------------------------------------------------------------------- /reading/buildit.md: -------------------------------------------------------------------------------- 1 | # BuildIt: A Type-Based Multi-stage Programming Framework for Code Generation in C++ 2 | 3 | - Ajay Brahmakshatriya and Saman Amarasinghe 4 | - CGO 2021 5 | - [Project Page](https://build-it.intimeand.space/) 6 | - [PDF](https://intimeand.space/docs/buildit.pdf) 7 | 8 | See also: 9 | - This [great intro to multi-stage programming](https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=1bec1c6c01075b518469434815c11b3f23c0093a) (cited in the above paper) 10 | - [Lightweight Modular Staging](https://scala-lms.github.io/) (LMS), a similar framework for Scala -------------------------------------------------------------------------------- /reading/copy-and-patch.md: -------------------------------------------------------------------------------- 1 | # Copy-and-patch compilation 2 | 3 | - Haoran Xu, Fredrik Kjolstad 4 | - OOPLSA 2021 5 | - [ACM DL](https://dl.acm.org/doi/10.1145/3485513) -------------------------------------------------------------------------------- /reading/doop.md: -------------------------------------------------------------------------------- 1 | # Doop: Strictly Declarative Specification of Sophisticated Points-to Analyses 2 | 3 | - [ACM DL PDF](https://dl.acm.org/doi/pdf/10.1145/1639949.1640108) 4 | - [Youtube talk](https://www.youtube.com/watch?v=FQRLB2xJC50) 5 | 6 | This paper introduces the Doop framework for specifying points-to analyses in Datalog programs. 7 | Datalog is a declarative logic programming language (and also a database query language) 8 | that is well-suited for expressing pointer analyses. 9 | 10 | The paper provides a pretty good introduction to Datalog if you're unfamiliar with it. 11 | For more of a Datalog primer, you can see [these lecture notes from my previous course](https://inst.eecs.berkeley.edu/~cs294-260/sp24/2024-02-05-datalog). 12 | 13 | Skip section 4 if you aren't already familiar with Datalog; 14 | it described the optimizations needed to make Datalog efficient enough for this purpose. 15 | It's quite interesting, but not necessary to understand from a points-to analysis perspective. 16 | 17 | -------------------------------------------------------------------------------- /reading/ir-survey.md: -------------------------------------------------------------------------------- 1 | # Intermediate Representations in Imperative Compilers: A Survey 2 | 3 | - James Stanier and Des Watson 4 | - 2013 5 | - [ACM DL](https://dl.acm.org/doi/10.1145/2480741.2480743) 6 | - [alternate link](https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=74db5beee87ef7abdb77eb4dd23e13fdaf12c77a) 7 | 8 | This paper provides a survey of intermediate representations (IRs) used in imperative compilers. 9 | It's a longer paper, but with little formalism, so it should be a quicker read. 10 | Don't get stuck on the details of any one IR; 11 | the goal is to understand the commonalities and differences between them. 12 | -------------------------------------------------------------------------------- /reading/lazy-code-motion.md: -------------------------------------------------------------------------------- 1 | # Lazy Code Motion 2 | 3 | - [ACM DL PDF](https://dl.acm.org/doi/pdf/10.1145/143103.143136) 4 | 5 | This work introduces lazy code motion, 6 | a technique that sits in a larger family of optimization called _partial redundancy elimination_. 7 | We've seen common subexpression elimination, 8 | which is a form of redundancy elimination that looks for expressions that are computed more than once. 9 | In fact, 10 | loop invariant code motion is another form of (temporal) redundancy elimination. 11 | Partial redundancy elimination is a more general form of this optimization, 12 | and it can be seen to subsume most forms of LICM. 13 | 14 | This work introduces a new form of partial redundancy elimination called _lazy code motion_, 15 | which is typically seen as simpler than the original PRE work by Morel and Renvoise. 16 | 17 | Note that many practical LICM implementations are not technically 18 | safe; they may actually hoist code out of a while-loop that doesn't have an effect. 19 | This is making the assumption that the loop will actually run, 20 | which is not always the case. 21 | In practice, since you cannot always prove statically that a loop will run, 22 | you have to choose between being conservative and not hoisting anything, 23 | or being more aggressive and hoisting non-effectful loop-invariant code. 24 | Partial redundancy elimination 25 | techniques are typically more conservative than the sometime aggressive LICM 26 | implementations, which is why LICM isn't totally subsumed by PRE. 27 | 28 | This paper is quite short but a little dense. 29 | If you are feeling stuck, 30 | focus your efforts on the transformation and the examples, and gloss over the proofs. 31 | There are also many presentations of this work online that may help you understand the material; 32 | for example here are some [slides from CMU's 15-745 course](http://www.cs.cmu.edu/afs/cs/academic/class/15745-s19/www/lectures/L10-Lazy-Code-Motion.pdf). 33 | 34 | There is also a pretty good [Youtube video](https://www.youtube.com/watch?v=zPLbAOdIqRw) 35 | that provides some examples. 36 | 37 | Some terminology: 38 | - down-safety: a.k.a. anticipated, needed. An expression is anticipated at point p if it's guaranteed to be computed in all outgoing paths. 39 | - up-safety: available. An expression is available at point p if it's guaranteed to be computed in all incoming paths. 40 | -------------------------------------------------------------------------------- /reading/linear-scan-ssa.md: -------------------------------------------------------------------------------- 1 | # Linear Scan Register Allocation on SSA Form 2 | - Christian Wimmer, Michael Franz 3 | - CGO 2010 4 | - [PDF](https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=3523f1d9f14a31a4fd2abc8f4a3ab06e26f84f52), [alternate](http://www.christianwimmer.at/Publications/Wimmer10a/Wimmer10a.pdf) -------------------------------------------------------------------------------- /reading/optimal-inlining.md: -------------------------------------------------------------------------------- 1 | # Understanding and exploiting optimal function inlining 2 | 3 | - Theodoros Theodoridis, Tobias Grosser, Zhendong Su 4 | - 2022 5 | - [ACM DL](https://dl.acm.org/doi/10.1145/3503222.3507744), [PDF](https://ethz.ch/content/dam/ethz/special-interest/infk/ast-dam/documents/Theodoridis-ASPLOS22-Inlining-Paper.pdf) -------------------------------------------------------------------------------- /reading/sparse-conditional-constant-prop.md: -------------------------------------------------------------------------------- 1 | # Constant Propagation with Conditional Branches 2 | 3 | - Mark N. Wegman and F. Kenneth Zadeck 4 | - Transactions on Programming Languages and Systems (TOPLAS), 1991 5 | - [PDF](https://dl.acm.org/doi/pdf/10.1145/103135.103136) from ACM Digital Library 6 | 7 | Read Sections 1-5. 8 | 9 | A quote from the paper sums up why this is an important topic: 10 | 11 | > Many optimizing compilers repeatedly execute constant propagation and 12 | unreachable code elimination since each provides information that improves 13 | the other. CC solves this problem in an elegant way by combining the two 14 | optimization. Additionally, the algorithm gets better results than are possible 15 | by repeated applications of the separate algorithms, as described in 16 | Section 5.1. 17 | 18 | In other words, this paper is a classic example of how 19 | two dataflow analyses can be combined to get better results 20 | that simply running them one after the other (even in a loop)! 21 | 22 | ## SSA Primer 23 | 24 | This paper mentions the use of Static Single Assignment (SSA) form, 25 | which we will cover in more detail later in the course. 26 | The paper actually provides a pretty accessible introduction to SSA form, 27 | but you may find it helpful to read more about it 28 | on the [Wikipedia page](https://en.wikipedia.org/wiki/Static_single-assignment_form) 29 | or any of the many online resources on this topic. 30 | 31 | In short, 32 | you've probably notices how annoying it is to deal with redefinitions in your value numbering implementation. 33 | SSA form is a way to make this easier by ensuring that each variable is only assigned once. 34 | In simple code, 35 | this can be done with a simple renaming: 36 | ``` 37 | x = y; 38 | x = x + 1; 39 | use(x) 40 | ``` 41 | becomes: 42 | ``` 43 | x1 = y; 44 | x2 = x1 + 1; 45 | use(x2) 46 | ``` 47 | 48 | What happens when a block has multiple predecessors? 49 | `if`s are a common example of this: 50 | ```c 51 | x = 1; 52 | if (cond) x = x + 1; 53 | use(x); 54 | ``` 55 | 56 | SSA's solution is to use the φ (phi) function 57 | to allow a special definition at the join point of the control flow 58 | that magically selects the correct value from the predecessors. 59 | So here's the before IR: 60 | ``` 61 | entry: 62 | x = 1 63 | br cond .then .end 64 | .then: 65 | x = x + 1 66 | .end 67 | use x 68 | ``` 69 | 70 | And here it is in SSA form: 71 | ``` 72 | entry: 73 | x1 = 1 74 | br cond .then .end 75 | .then: 76 | x2 = x1 + 1 77 | .end 78 | x3 = φ(x1, x2) 79 | use x3 80 | ``` 81 | 82 | Sometimes (and in Bril, as we'll see later), 83 | the φ function also mentions the labels that define the values: 84 | ``` 85 | x3 = φ(entry: x1, .then: x2) 86 | ``` 87 | 88 | This very brief introduction to SSA form should be enough to get you through the paper! 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /syllabus.md: -------------------------------------------------------------------------------- 1 | # Syllabus 2 | 3 | See the [README](README.md) for general course information, including the schedule. 4 | 5 | If you need support outside of context of the course, 6 | please feel free to contact me 7 | or use the [UC Berkeley Supportal](https://supportal.berkeley.edu/home) 8 | to access resources for mental health, basic needs, and more. 9 | 10 | **Caveat lector**: 11 | This course is under construction. 12 | That means that these documents are subject to change! 13 | That also means you are welcome to provide feedback to me about the course. 14 | 15 | ## Communication 16 | 17 | Course announcements, assignments, grading, and discussion will be on bCourses. 18 | 19 | ## Course materials 20 | 21 | The course material will be made available here in the form of lecture notes. 22 | As these materials are open-source, 23 | you may provide direct edits to course materials (either here or in other course repos) 24 | via pull requests if you feel so inclined, but it is not at all required. 25 | Issues and PRs to this repo are for typos, broken links, and other technical issues. 26 | Questions and course discussion should be directed to the bCourses forum. 27 | 28 | ## Class 29 | 30 | Some classes will be a lecture, 31 | some will be a discussion of a paper. 32 | Some classes (or portions of classes) 33 | will be dedicated to working on or discussing the implementation tasks. 34 | These portions are not required (you may leave class early if you wish), 35 | but I will be available to help you with the tasks. 36 | 37 | The lecture notes are not designed to be a replacement for attending class. 38 | There will not be recordings of class. 39 | 40 | ## Assignments 41 | 42 | Assignments mostly take the form of written reflections, 43 | which will be turned in on bCourses. 44 | I will read them! 45 | 46 | **NOTE**: The point of the reflections is to tell me **your thoughts** and **your decisions**. 47 | Yes, they are also there to ensure you actually did the assignment. 48 | But keep in mind that **I know the course material already**, and **I have read the paper**. 49 | You **do not** need to re-explain the course material or the paper to me; 50 | you may use terms and assume I know what they mean. 51 | You **do** need to tell me what you're thinking: 52 | things you found interesting, 53 | things you found challenging or got stuck on, 54 | decisions you made and how you made them, 55 | insights you had beyond what's in the course material, 56 | and so on. 57 | 58 | The course will feature the following assignments: 59 | - **Reading reflections**: 60 | - These are short reflections on the papers that we read in the course. 61 | - These should be done individually. 62 | - A reflection should roughly a paragraph at minimum. 63 | - Do not summarize the paper. 64 | - Assume that I have read the paper. 65 | - Include questions you may have about the paper, 66 | whether or not you liked it, 67 | and what you learned from it. 68 | - These are due **at noon** the day of the class where the paper is discussed. 69 | - Discussion participation will be holistically included in this grade. 70 | - **Implementation tasks**: 71 | - May be done individually or groups of 2-3 people. 72 | - Your work will not be turned in our automatically graded, 73 | but you will have to discuss your work in a reflection. 74 | - I reserve the right to ask for a demonstration of your work. 75 | - Reflection should still be written and submitted _individually_. 76 | - Include your group members in the reflection. 77 | - Include a brief description of what you personally contributed. 78 | - Include a link to your work. 79 | - I encourage you to use a public git repository, 80 | but you may use a private one if you wish. 81 | Just invite me to it. 82 | - Include discussion of the major design decisions and challenges in the implementation. 83 | - Show off! Includes some code snippets, figures, or other artifacts that you think are particularly interesting. 84 | - You may share some text/figures/code between group members' reflection. 85 | - Do the work! 86 | - Pretty much everything in this class has been done before. 87 | - You can easily find solutions online. I will even provide some in the course infrastructure. 88 | - You may look at any of these resources, 89 | but you must acknowledge them (not include resources from this course) in your reflection. 90 | I encourage you to do the work without looking at these resources at first! 91 | - **Final project**: see [project page](project.md) for more details. 92 | 93 | ## Grading 94 | 95 | The assignments from this course will be graded according to 96 | a "[Michelin star](https://en.wikipedia.org/wiki/Michelin_Guide#Stars)" system 97 | (borrowed from [Adrian Sampson's](https://www.cs.cornell.edu/courses/cs6120/2023fa/syllabus/#grading) policy): 98 | - **One star**: The work is high-quality. 99 | - **Two star**: The work is excellent. 100 | - **Three star**: The work is exceptional. 101 | 102 | On bCourses, this will appear as an assignment score out of 1 point. 103 | Work below the "high-quality" threshold will receive zero stars (points). 104 | Earning one star is the goal for all assignments, and consistently doing so will earn an A in the course. 105 | Consistently earning multiple stars will earn an A+. 106 | 107 | ## Late Policy 108 | 109 | The assignment deadlines are designed to help you pace yourself through the course. 110 | That said, you do have late days to use throughout the semester. 111 | 112 | 1. You have 15 "late days" to use throughout the semester. 113 | 2. Late days are counted in 24-hour increments, 1 minute late is 1 late day. 114 | 3. Reading reflections **cannot be turned in late**. --------------------------------------------------------------------------------