Due: before class on Tuesday 09 December
Part 1: Type Systems & Polymorphism (Unit 5)
-
In modern programming language terminology, which of the following scenarios best demonstrates “Strong Typing”?
- A language where you must explicitly write
int x = 10;rather than justx = 10;. - A language that throws a runtime error if you try to multiply a string by a boolean, rather than implicitly converting the boolean to 0 or 1.
- A language where the type of a variable is determined at compile-time and cannot change.
- A language that allows you to cast a pointer to a specific memory address into an integer and perform arithmetic on it.
- A language where you must explicitly write
-
Consider the implementation of Dynamic Typing using a “tagged union” approach (as seen in Python or Scheme). Why does this implementation inherently introduce runtime overhead compared to static typing?
- The interpreter must perform a “tag check” before every operation (like addition) to ensure the types are compatible.
- Tagged unions require all variables to be stored on the heap, which causes frequent garbage collection pauses.
- The language must generate multiple versions of every function to handle different types.
- It prevents the use of recursion, forcing the programmer to write slower iterative loops.
-
Why is Ad-hoc Polymorphism (Function Overloading) generally impossible to implement in a dynamically typed language like Python?
- Dynamic languages strictly forbid defining two functions with the same name in the same scope.
- The interpreter cannot determine which version of the function to call because the arguments do not have declared types to match against a signature.
- Dynamic languages use vtables for all function calls, which only support single inheritance.
- It would require “Type Erasure,” which is only available in compiled languages like Java.
-
In Java when we write a method call on an object like
midn.march(), what happens at compile-time and at run-time? (Select all that apply)- The compiler selects the correct version of the function based only on the declared type (static dispatch)
- The compiler ensures the declared type has a method with that name (static typing)
- The JVM runtime looks up which version of the function to call based on the actual type of the object using a vtable (dynamic dispatch)
- The JVM runtime will throw an error if the actual object type doesn’t have any method with that name (duck typing)
-
Which of the following statements accurately describes the trade-offs between C++ Templates and Java Generics?
- C++ Templates use “Type Erasure,” resulting in smaller executable files but slower runtime performance due to casting.
- Java Generics use “Monomorphization,” creating specialized copies of the class for every data type used.
- C++ Templates allow for better runtime performance because the compiler generates specialized machine code for each type (e.g.,
vector<int>vsvector<float>). - Java Generics allow you to instantiate new objects of type
T(e.g.,new T()) because the type information is preserved at runtime.
Part 2: LLVM Optimizations and Backend (Unit 6)
-
In the context of the LLVM optimization pipeline, what is the primary role of the Middle-End?
- To parse the source code and generate the Abstract Syntax Tree (AST).
- To map virtual registers to physical hardware registers (e.g., x86 or ARM).
- To analyze and transform the Intermediate Representation (IR) to be more efficient without changing its behavior.
- To verifying type safety and syntax correctness before lowering to IR.
-
You are optimizing a function to remove stack memory usage (
alloca/load/store). If a variable is assigned different values in two different branches of anif/elsestatement, how must you handle the merge point in SSA form?- You must assign the variable to a memory address in the merge block to reset its state.
- You must duplicate the code of the merge block into both the
ifandelsebranches to avoid merging. - You can simply define the variable twice in the merge block, once for each path.
- You must insert a Phi node (\(\phi\)) at the start of the merge block to select the correct value based on which predecessor block was executed.
- You insert copy commands in each predecessor block, immediately before the branch instruction
-
Which of the following are strict rules regarding the placement and structure of Phi nodes in LLVM IR? (Select all that apply)
- Phi nodes must be the very first instructions in a Basic Block.
- A Phi node must list a value/label pair for every successor block in the control flow graph.
- A Phi node must list a value/label pair for every predecessor block in the control flow graph.
- Phi nodes can only be used in loops, not in standard if/else conditional merges.
-
When performing the mem2reg optimization (promoting memory to registers), what distinguishes an “Easy Case” from a “Hard Case”?
- Easy Case: The variable is an integer; Hard Case: The variable is a pointer or struct.
- Easy Case: The
loadhas only one correspondingstore(e.g., within the same block); Hard Case: Thestorecould come from multiple predecessor blocks. - Easy Case: The code contains no loops; Hard Case: The code contains nested loops.
- Easy Case: The variable is defined in the
preheader; Hard Case: The variable is defined in thebody.
-
Loop Invariant Code Motion (LICM) allows the compiler to move an instruction from the loop body to the preheader. Which of the following instructions would be illegal to move?
%x = add i32 %a, 5(where%ais defined before the loop starts).%x = mul i32 10, 20(constant arithmetic).%x = call i32 @random()(a function that returns a random number).%x = getelementptr ...(address calculation using only invariant operands).
-
You are writing a pass to unroll a loop that iterates from 0 to
N. SinceNis unknown at compile-time, you cannot guarantee it is divisible by your unrolling factor. What structural component handles the “leftover” iterations?- The Epilogue loop.
- The Preheader block.
- The Vtable.
- The Interference Graph.
-
Which of the following best summarizes the primary goal of Loop Unrolling?
- To reduce the total number of instructions in the binary executable.
- To reduce the “administrative overhead” (incrementing counters, checking conditions, branching) relative to the actual work done in the loop body.
- To ensure the loop executes exactly the same number of times regardless of input.
- To eliminate all
phinodes from the loop header.
-
In Register Allocation, we build an Interference Graph. What does an edge between two nodes (variables) signify in this graph?
- The two variables are used in the same mathematical operation.
- The two variables have overlapping Live Ranges (they are both “alive” at the same time).
- The two variables are assigned the same value.
- One variable is a pointer to the other.
-
During the graph coloring phase of register allocation, if the graph cannot be colored with \(K\) registers, the compiler must “Spill” a variable. What does “spilling” entail?
- Moving the variable to a different physical register (e.g., from
raxtorbx). - Deleting the variable entirely and re-calculating it later.
- Moving the variable from a register into stack memory, inserting
loadandstoreinstructions. - Expanding the number of physical registers available on the CPU.
- Moving the variable to a different physical register (e.g., from
Part 3: Optimization challenge
-
Given the following LLVM IR code, run through all the optimization steps we have learned about:
- constant propagation
- memory elimination
- loop-invariant code motion
- loop unrolling
- phi elimination
- register allocation
The final result should be “pseudo-LLVM” IR code, which has exactly the same structure but doesn’t have phi instructions and can re-assign registers and perform copy commands if necessary.
Show all of your work!
define i32 @hw61(i32 %reg0, i32 %reg1) { A: %reg3 = alloca i32 %reg4 = alloca i32 %reg5 = alloca i32 %reg6 = alloca i32 store i32 %reg0, ptr %reg3 store i32 %reg1, ptr %reg4 %reg7 = load i32, ptr %reg3 %reg8 = load i32, ptr %reg4 %reg9 = add i32 %reg7, %reg8 store i32 %reg9, ptr %reg5 store i32 1, ptr %reg6 br label %B B: %reg11 = load i32, ptr %reg6 %reg12 = icmp sle i32 %reg11, 3 br i1 %reg12, label %C, label %E C: %reg14 = load i32, ptr %reg3 %reg15 = add i32 %reg14, 10 %reg16 = load i32, ptr %reg5 %reg17 = mul i32 %reg16, %reg15 store i32 %reg17, ptr %reg5 br label %D D: %reg19 = load i32, ptr %reg6 %reg20 = add i32 %reg19, 1 store i32 %reg20, ptr %reg6 br label %B E: %reg22 = load i32, ptr %reg5 %reg23 = load i32, ptr %reg4 %reg24 = sub i32 %reg22, %reg23 ret i32 %reg24 }