Lab 5 — Performance: -O0 vs -O2 on Your CPU
Goal
Measure the real performance difference between unoptimised (-O0) and optimised (-O2) C code running on the RV32I CPU you built in Project 1. By the end, you will have read assembly output, measured cycle counts with the hardware cycle counter, and computed a concrete speedup.
Prerequisites
- Project 1 submitted and running: your CPU boots C code over UART and executes it on the Tang Nano 9K.
- Basic familiarity with reading RV32I assembly (covered in Module 2 classes).
Why this matters
The KWS kernel has ~457K MACs. If the compiler generates even one unnecessary instruction per MAC, that is 457K extra cycles — at 27 MHz, ~17 ms of wasted time. Compiler optimisation is not magic; this lab shows you exactly what it does and does not do.
Step 1 — The benchmark function
Activity 1
Create bench.c with a simple loop that the compiler can optimise in obvious ways:
This function has a multiply (which uses the Zmmul extension you added in Project 2), an add, a compare, and a branch — one of each main operation type per iteration.
Step 2 — Measure cycles with rdcycle
Activity 2
Add a timing harness in main.c. Your CPU's Zicntr extension exposes the cycle counter via the mcycle CSR:
volatile on result prevents the compiler from eliminating the call entirely when it realises the result is unused.
rdcycleh then rdcycle ordering
Reading the high word first, then the low word, can give a wrong result if the low word wraps around between the two reads. For cycle counts under ~4 billion (150 seconds at 27 MHz), the low word alone is sufficient — just use rdcycle. The 64-bit version is shown for completeness.
Step 3 — Compile at -O0 and inspect
Activity 3
Compile bench.c twice and disassemble:
Compare the two .s files. Count the instructions inside the loop body.
Hint — what to look for
In the -O0 version, look for:
- Loads and stores to the stack frame for every variable access (the compiler does not keep i or acc in registers across iterations)
- A mul instruction that reads from memory, not from a register
In the -O2 version, look for:
- i and acc kept in registers throughout the loop
- Possibly a strength-reduction: i*i computed as prev + 2*i - 1 (incrementally) instead of a full multiply each iteration
- Fewer total instructions in the loop body
Activity 4
Fill in this table from your disassembly:
| -O0 | -O2 | |
|---|---|---|
| Instructions in loop body | ? | ? |
| Stack loads per iteration | ? | ? |
| Stack stores per iteration | ? | ? |
| Multiply instruction present | yes/no | yes/no |
Step 4 — Measure on the board
Activity 5
Compile and link the full program (with your Project 2 UART driver) at both optimisation levels and run on the board:
Load each, record the cycle count printed via UART, and compute the speedup:
$$\text{speedup} = \frac{\text{cycles at -O0}}{\text{cycles at -O2}}$$
Hint — typical results
For sum_of_squares(1000), expect roughly 8,000–12,000 cycles at -O0 and 3,000–5,000 cycles at -O2 — a speedup of 2–3×. The exact numbers depend on your CPU's microarchitecture (how many cycles each instruction takes in your multicycle design).
Step 5 — Connect to KWS
Activity 6
The KWS kernel has ~457K MACs. If the compiler saves even 2 instructions per MAC iteration:
- Savings: 457,000 × 2 = 914,000 cycles
- At 27 MHz: ~34 ms saved per inference
Using your measured per-iteration savings from Activities 3–5, estimate how many cycles the -O2 compiler saves for the full KWS kernel.
This is why Lab 6 (KWS kernel preview) asks you to compile the kernel at -O2 — and why Project 3 asks for a roofline analysis on top of compiler optimisation, not instead of it.
Checklist
-
bench.ccompiled at both-O0and-O2; assembly compared and table filled in. - Cycle counts measured on the board at both optimisation levels.
- Speedup computed.
- KWS cycle savings estimated.
Summary
You measured a real, hardware-confirmed speedup from compiler optimisation on your own CPU. The key insight: -O2 eliminates memory traffic by keeping loop variables in registers. For a kernel with 457K iterations, this compounds into tens of milliseconds of saved time — and that is before any hardware acceleration. In Module 5 you will see that the roofline model sets a theoretical ceiling on what software optimisation alone can achieve.