In C, a single line of code can hide a surprising amount of work. In this lab, you will investigate what happens when C programs are translated into RISC-V.
You will begin by acting as the compiler yourself: translating a small C expression into assembly. Then you will use a real compiler to generate RV32I assembly, inspect the instructions it produces, and compare different implementations.
Throughout the lab, we will return to one question:
Can we tell which C program is faster just by looking at the C code?
You will use generated assembly and instruction counts as evidence.
Record all written answers in
answers.md.
By the end of this lab, you should be able to:
First you must accept the assignment through classroom50
Follow this link. You must be in the GitHub organization first. You have all already received an invite here. If you receive an error or cannot accept it, email Kaki with your GitHub username. Clone the repo and you are ready to get going!
Start by following the setup instructions in
README.md.
Open the repository in VS Code, choose:
Dev Containers: Reopen in Container
and then run:
311-checkYou should see:
Success: generated RV32I assembly.
For Windows users who are in the container but the 311-check get an error regarding bash try the following:
sed -i 's/\r$//' scripts/311-* starter/*
Then try running the script again. If that doesn’t work, open the scripts folder, and go into each file starting with the “311-check.” In the bottom right corner if it says CRLF, click it, switch it to LF, and SAVE the file! Then try running the command again. You may have to do that for all the files in the script folder.
If neither solution works, we can definitely help you in OH!
Your starting repository should contain:
README.md
LAB.md
DEVCONTAINER.md
answers.md
starter/
scripts/
build/
If any of those are missing before you begin, check that you cloned/opened the correct repository.
Be careful when reading the compiler flags:
-O0 = capital O + zero → optimization off
-O2 = capital O + two → optimization on
Generated files will appear in build/.
Before asking a compiler to generate assembly, you will translate a short C statement yourself.
Open:
starter/part1.s
Assume:
t0 = base address of values
t1 = available temporary register
t2 = available temporary register
t3 = result
Each element of values is a 32-bit integer.
Translate:
result = values[1] + values[3] + 5;You may use the instructions you have learned in class, including
lw, add, and addi.
After editing starter/part1.s, run:
311-assemble starter/part1.sanswers.md1. Write your RV32I translation.
2. Which instructions access memory?
3. Why are the offsets for values[1] and
values[3] measured in bytes rather than array
indices?
4. What is the minimum number of RISC-V instructions needed to implement this one line of C using the assumptions above?
Compare:
starter/scale_add.c
starter/scale_shift.c
Both compute 8 * x.
Before compiling either version, answer the following in
answers.md.
answers.md1. Which version do you predict will require less machine work?
2. Why?
3. What do you expect the machine-level instructions for the two versions to do differently?
Do not justify your answer by saying that one version has fewer lines of C. Think about the operations the machine may need to perform.
Compile both functions with optimization off:
311-compile -O0 starter/scale_add.c
311-compile -O0 starter/scale_shift.cThen compile both with optimization on:
311-compile -O2 starter/scale_add.c
311-compile -O2 starter/scale_shift.cReminder:
-O0 = capital O + zero
-O2 = capital O + two
Open:
build/scale_add_O0.s
build/scale_shift_O0.s
build/scale_add_O2.s
build/scale_shift_O2.s
You do not need to understand every assembler directive.
For the table below, Total static instructions in function means:
Count every RISC-V instruction from the function’s first instruction through its return instruction.
Count function setup/teardown instructions if the compiler generated them.
Do not count:
scale_add:.text,
.globl, .align, or .sizeFor the category columns, count the instructions that fit that category. Some instructions may not fit one of the three listed categories, so the category columns do not necessarily need to sum to the total.
For each function, identify the major instructions responsible for:
Paste short, relevant assembly excerpts into
answers.md and add comments explaining what the major
instructions do. You do not need to submit the full generated
.s files.
answers.mdComplete this table:
| Version | Optimization | Total static instructions in function | Loads / Stores | Arithmetic / Logic | Branches / Jumps |
|---|---|---|---|---|---|
scale_add |
-O0 |
||||
scale_shift |
-O0 |
||||
scale_add |
-O2 |
||||
scale_shift |
-O2 |
Then answer:
1. What major differences do you observe between
-O0 and -O2?
2. What happened to the repeated-addition loop under optimization?
3. Did the optimized versions become more similar?
4. Was your original prediction still meaningful after compiler optimization? Why or why not?
A static instruction count tells us how many instructions appear in a piece of code.
A dynamic instruction count tells us how many instructions are actually executed during one run.
These are not necessarily the same.
For example, suppose a loop body contains four instructions:
add ...
addi ...
addi ...
blt ...The static count is four instructions.
If the loop executes eight times, those instructions account for approximately:
4 instructions × 8 iterations = 32 instruction executions
before accounting for setup and exit instructions outside the loop.
Use the scale_add assembly generated at
-O0.
answers.md1. Identify the loop body. Paste the relevant assembly excerpt.
2. How many static instructions are in the loop body?
3. How many times does the loop execute?
4. How many dynamic instruction executions are caused by the loop body? Show your reasoning.
5. Why is this dynamic count different from the static instruction count?
Now compare two implementations of the same array computation:
starter/array_indexed.c
starter/array_pointer.c
Before compiling, answer the prediction questions in
answers.md.
answers.md1. Which version do you predict will require less machine work?
2. What do you expect the assembly for each version to do differently?
Now compile both versions:
311-compile -O0 starter/array_indexed.c
311-compile -O0 starter/array_pointer.c
311-compile -O2 starter/array_indexed.c
311-compile -O2 starter/array_pointer.cInspect:
build/array_indexed_O0.s
build/array_pointer_O0.s
build/array_indexed_O2.s
build/array_pointer_O2.s
answers.md3. In sum_indexed, where can you see the
computation of an array element address? Paste a short
excerpt.
4. In sum_pointer, how does the pointer change
from one iteration to the next? Paste a short excerpt.
5. At -O0, which version appears to require more
machine work? What evidence are you using?
6. At -O2, how similar are the two generated
implementations?
7. Based on the generated assembly, is pointer-based C automatically faster than indexed C? Explain.
8. What does this tell you about making performance claims from source code alone?
Create a new source file for your modified implementation so you can compare the original and modified assembly side-by-side.
Start by copying the original:
cp starter/array_indexed.c starter/array_indexed_two_at_a_time.cEdit:
starter/array_indexed_two_at_a_time.c
so each loop iteration processes two elements. You may assume
n is even.
For example, the loop structure could be:
for (int i = 0; i < n; i += 2) {
sum += a[i];
sum += a[i + 1];
}Before compiling, answer the prediction questions in
answers.md.
answers.md1. Do you predict the static instruction count will increase or decrease? Why?
2. Do you predict the number of loop iterations will increase or decrease?
3. Do you predict the dynamic instruction count will increase or decrease? Why?
Compile the modified file:
311-compile -O0 starter/array_indexed_two_at_a_time.c
311-compile -O2 starter/array_indexed_two_at_a_time.cNow your original and modified generated assembly remain separate:
build/array_indexed_O0.s
build/array_indexed_O2.s
build/array_indexed_two_at_a_time_O0.s
build/array_indexed_two_at_a_time_O2.s
Compare them directly.
answers.md4. Did the static instruction count increase or decrease?
5. Did the number of loop iterations increase or decrease?
6. Did your estimated dynamic instruction count increase or decrease? Show your reasoning.
7. Why can static and dynamic instruction counts move in different directions?
8. Did the optimizer preserve your source-level transformation, or did it generate something different?
You will submit starter/array_indexed_two_at_a_time.c,
but you do not need to submit the generated files in
build/. Paste any assembly excerpts you use as evidence
into answers.md.
Compare:
starter/mul8.c
starter/shift8.c
Suppose someone makes the following claim:
“The shift version is faster because bit shifting is always cheaper than multiplication.”
answers.md1. Before compiling, predict what assembly each version will produce.
Then compile both:
311-compile -O0 starter/mul8.c
311-compile -O0 starter/shift8.c
311-compile -O2 starter/mul8.c
311-compile -O2 starter/shift8.canswers.md2. Are the generated instructions different at
-O0? What evidence do you see?
3. Are they different at -O2? What evidence do
you see?
4. Does the evidence support the claim that manually replacing multiplication by 8 with a shift necessarily improves performance? Explain.
5. What role does the compiler play in evaluating source-level optimization advice?
Answer each question in 2–4 sentences in answers.md.
1. What was the most surprising difference between the C code and the generated RISC-V?
2. Why is dynamic instruction count often more informative than static instruction count?
3. Why might fewer instructions still not guarantee a proportionally faster program?
4. Looking across Parts 2–7, what did compiler optimization change most dramatically? Support your answer with examples from at least two earlier parts.
5. If someone claims that one C implementation is “faster,” what evidence would you now want before accepting that claim?
Before submitting, run:
311-submit-checkCommit and push exactly these student-created/modified deliverables:
answers.md
starter/part1.s
starter/array_indexed_two_at_a_time.c
Your completed answers.md should contain:
You do not need to submit generated .s,
.o, or .dump files from
build/.
| Component | Percentage |
|---|---|
| Part 1: Hand-written C → RISC-V | 15% |
| Part 2–3: Prediction and compiler investigation | 20% |
| Part 4: Static vs. dynamic instruction count | 15% |
| Part 5: Indexed vs. pointer comparison | 20% |
| Part 6: Modification experiment | 15% |
| Part 7: Performance claim investigation | 5% |
| Part 8: Final reflection | 10% |
| Total | 100% |
The goal of this lab is not to memorize compiler behavior.
The goal is to practice this workflow:
Predict → Compile → Inspect → Count → Compare → Explain
For now, we are using executed instruction count as a simple measure of machine work. Later in the course, we will see why two programs executing the same number of instructions can still take different amounts of time.