# Assignment 1: RISC-V Assembly and Instruction Pipeline contributed by < [phyrexxxxx](https://github.com/phyrexxxxx/ca2025-quizzes) > ## Prerequisite ### Installing RISC-V Toolchain ```bash $ sudo apt update $ sudo apt install gcc-riscv64-unknown-elf ``` Install RISC-V GCC compiler and related tools ### Verifying Installation ```bash # Check RISC-V toolchain riscv64-unknown-elf-gcc --version riscv64-unknown-elf-as --version riscv64-unknown-elf-ld --version # Check tools which riscv64-unknown-elf-size which riscv64-unknown-elf-objdump which riscv64-unknown-elf-nm ``` Output: ``` $ riscv64-unknown-elf-gcc --version riscv64-unknown-elf-gcc (13.2.0-11ubuntu1+12) 13.2.0 Copyright (C) 2023 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ riscv64-unknown-elf-as --version GNU assembler (2.42-1ubuntu1+6) 2.42 Copyright (C) 2024 Free Software Foundation, Inc. This program is free software; you may redistribute it under the terms of the GNU General Public License version 3 or later. This program has absolutely no warranty. This assembler was configured for a target of `riscv64-unknown-elf'. $ riscv64-unknown-elf-ld --version GNU ld (2.42-1ubuntu1+6) 2.42 Copyright (C) 2024 Free Software Foundation, Inc. This program is free software; you may redistribute it under the terms of the GNU General Public License version 3 or (at your option) a later version. This program has absolutely no warranty. $ which riscv64-unknown-elf-size /usr/bin/riscv64-unknown-elf-size $ which riscv64-unknown-elf-objdump /usr/bin/riscv64-unknown-elf-objdump $ which riscv64-unknown-elf-nm /usr/bin/riscv64-unknown-elf-nm ``` The Ubuntu package installs `riscv64-unknown-elf-gcc`, but it can compile RV32I programs using `-march=rv32i`: - `riscv64-unknown-elf-gcc` + `-march=rv32i` + `-mabi=ilp32` = compile RV32I programs - Though the toolchain is named `riscv64`, it can compile both 32-bit and 64-bit programs - This is the standard practice for RISC-V official GNU toolchain ## Preparing Ripes Simulator Visit: https://github.com/mortbopet/Ripes/releases > ⚠️ Note: **WSL2 cannot directly open GUI windows**, you must either: > > - Install **X Server for Windows** (e.g., [VcXsrv](https://sourceforge.net/projects/vcxsrv/) or [X410](https://x410.dev/)) > - Or download **Windows version of Ripes** directly on the Windows host Click: `Ripes-v2.2.6-70-gc8e0412-win-x86_64.zip` After extraction, directly click `Ripes.exe` to launch Ripes GUI # Problem B: CLZ (v1-basic) > [commit 4ef4eb1](https://github.com/phyrexxxxx/ca2025-quizzes/blob/6082c8d70486b21cfe842e6cd79ec32aa92e3e37/q1b-uf8/clz/v1-basic/clz_v1.s) ```asm .text .globl clz_v1 .type clz_v1, @function clz_v1: li t0, 32 # n = 32 li t1, 16 # c = 16 # Iteration 1: c = 16 srl t2, a0, t1 # y = x >> 16 beqz t2, .L1_skip # if (y == 0) sub t0, t0, t1 # n -= 16 mv a0, t2 # x = y .L1_skip: # Iteration 2: c = 8 srli t1, t1, 1 # c >>= 1 → c = 8 srl t2, a0, t1 # y = x >> 8 beqz t2, .L2_skip # if (y == 0) sub t0, t0, t1 # n -= 8 mv a0, t2 # x = y .L2_skip: # Iteration 3: c = 4 srli t1, t1, 1 # c >>= 1 → c = 4 srl t2, a0, t1 # y = x >> 4 beqz t2, .L3_skip # if (y == 0) sub t0, t0, t1 # n -= 4 mv a0, t2 # x = y .L3_skip: # Iteration 4: c = 2 srli t1, t1, 1 # c >>= 1 → c = 2 srl t2, a0, t1 # y = x >> 2 beqz t2, .L4_skip # if (y == 0) sub t0, t0, t1 # n -= 2 mv a0, t2 # x = y .L4_skip: # Iteration 5: c = 1 srli t1, t1, 1 # c >>= 1 → c = 1 srl t2, a0, t1 # y = x >> 1 beqz t2, .L5_skip # if (y == 0) sub t0, t0, t1 # n -= 1 mv a0, t2 # x = y .L5_skip: sub a0, t0, a0 # return n - x ret .size clz_v1, .-clz_v1 ``` ```bash cd q1b-uf8/clz/v1-basic ``` ## Step 1: Compile CLZ Versions ```bash # Compile v1-hand (hand-written assembly) riscv64-unknown-elf-as \ -march=rv32i \ -mabi=ilp32 \ -o clz_v1.o clz_v1.s # Compile GCC -O0 (no optimization) riscv64-unknown-elf-gcc \ -march=rv32i \ -mabi=ilp32 \ -ffunction-sections \ -O0 -c -o clz_gcc_O0.o clz.c # Compile GCC -O2 (standard optimization) riscv64-unknown-elf-gcc \ -march=rv32i \ -mabi=ilp32 \ -ffunction-sections \ -O2 -c -o clz_gcc_O2.o clz.c # Compile GCC -O3 (aggressive optimization) riscv64-unknown-elf-gcc \ -march=rv32i \ -mabi=ilp32 \ -ffunction-sections \ -O3 -c -o clz_gcc_O3.o clz.c # Compile test framework riscv64-unknown-elf-as \ -march=rv32i \ -mabi=ilp32 \ -o ../test_framework/test_framework.o \ ../test_framework/test_framework.s ``` ## Step 2: Link Test Programs Errors encountered during the process: ``` $ riscv64-unknown-elf-ld \ -nostdlib \ -o test_v1_hand.elf \ ../test_framework/test_framework.o \ clz_v1.o riscv64-unknown-elf-ld: ../test_framework/test_framework.o: ABI is incompatible with that of the selected emulation: target emulation `elf32-littleriscv' does not match `elf64-littleriscv' riscv64-unknown-elf-ld: failed to merge target specific data of file ../test_framework/test_framework.o riscv64-unknown-elf-ld: clz_v1.o: ABI is incompatible with that of the selected emulation: target emulation `elf32-littleriscv' does not match `elf64-littleriscv' riscv64-unknown-elf-ld: failed to merge target specific data of file clz_v1.o riscv64-unknown-elf-ld: ../test_framework/test_framework.o: in function `test_loop': (.text+0x30): undefined reference to `clz' riscv64-unknown-elf-ld: test_v1_hand.elf(.text): relocation ".L11+0x0 (type R_RISCV_JAL)" goes out of range riscv64-unknown-elf-ld: ../test_framework/test_framework.o: file class ELFCLASS32 incompatible with ELFCLASS64 riscv64-unknown-elf-ld: final link failed: file in wrong format ``` Error reason: ``` target emulation `elf32-littleriscv' does not match `elf64-littleriscv' ``` i.e., `elf32-littleriscv` and `elf64-littleriscv` don't match: - ABI mismatch: Object file `clz_v1.o` is 32-bit, but the linker is using 64-bit default mode - Symbol name mismatch: Assembly defines `clz_v1`, but test framework looks for `clz` ```asm # clz_v1.s clz_v1: li t0, 32 # n = 32 li t1, 16 # c = 16 ``` Fixed using the following commands to re-link: ```bash # Link v1-hand version riscv64-unknown-elf-ld \ -melf32lriscv \ -nostdlib \ --defsym clz=clz_v1 \ -o test_v1_hand.elf \ ../test_framework/test_framework.o \ clz_v1.o # Link GCC -O0 version riscv64-unknown-elf-ld \ -melf32lriscv \ -nostdlib \ --defsym clz=clz \ -o test_gcc_O0.elf \ ../test_framework/test_framework.o \ clz_gcc_O0.o # Link GCC -O2 version riscv64-unknown-elf-ld \ -melf32lriscv \ -nostdlib \ --defsym clz=clz \ -o test_gcc_O2.elf \ ../test_framework/test_framework.o \ clz_gcc_O2.o # Link GCC -O3 version riscv64-unknown-elf-ld \ -melf32lriscv \ -nostdlib \ --defsym clz=clz \ -o test_gcc_O3.elf \ ../test_framework/test_framework.o \ clz_gcc_O3.o ``` Key parameter explanation: | Parameter | Purpose | |---------------------|---------------------------------------| | `-melf32lriscv` | Tell 64-bit linker to use 32-bit RISC-V emulation mode | | `--defsym clz=clz_v1` | Create symbol alias, making `clz` point to `clz_v1` | Generated ELF executable: - ELF 32-bit LSB executable - RISC-V soft-float ABI - Contains `_start`, `clz`, and `clz_v1` symbols For the "Link GCC -O0 version", "Link GCC -O2 version", and "Link GCC -O3 version" above, `--defsym clz=clz` may not be necessary (I have verified this) — GCC-compiled object files already have the correct `clz` symbol, but adding it doesn't hurt. The most critical parameter is `-melf32lriscv`. ### Check Generated Test Programs ``` $ ls -lh test_*.elf -rwxr-xr-x 1 phyrexxxxx phyrexxxxx 1.7K Oct 10 14:32 test_gcc_O0.elf -rwxr-xr-x 1 phyrexxxxx phyrexxxxx 1.7K Oct 10 14:34 test_gcc_O2.elf -rwxr-xr-x 1 phyrexxxxx phyrexxxxx 1.7K Oct 10 14:35 test_gcc_O3.elf -rwxr-xr-x 1 phyrexxxxx phyrexxxxx 1.7K Oct 10 14:31 test_v1_hand.elf ``` ## Step 3: Verify Correctness **Important**: Verify program correctness first before measuring code size! The test framework will automatically execute 12 test cases and "expect" to output correct or wrong in the Ripes Console. ### Launch Ripes 1. Start Ripes 2. Select Processor: **5-stage pipeline (RV32I)** ### Test v1-hand Version Load program: File → Load Program → Select `test_v1_hand.elf` #### Check Results **Problem:** After execution in Ripes, the console doesn't show `correct` or `wrong`, only displays: ``` Program exited with code: 0 ``` **Reason:** When loading ELF, Ripes doesn't process its "educational syscalls (a7=1/4/10)"; ecall 10 is just treated as "exit", so we only see Program exited with code: 0. In Ripes' "Assembly Program" mode (directly pasting `.s`), it supports simplified syscalls like a7=4 print string. **Fix:** `q1b-uf8/clz/test_framework/test_framework.s`: ```diff all_correct: - # Output "correct\n" - la a0, correct_str - li a7, 4 # syscall: print_string - ecall - j exit + li a0, 0 # exit code 0 = PASS + li a7, 10 # ecall: exit + ecall test_failed: - # Output "wrong\n" - la a0, wrong_str - li a7, 4 # syscall: print_string - ecall + li a0, 1 # exit code 1 = FAIL + li a7, 10 + ecall ``` Then recompile and link. But later I directly integrated `test_framework.s` and `clz_v1.s` into: `clz_v1_standalone.s` Then load `clz_v1_standalone.s` in Ripes and run it directly. This is the fastest way to verify RISC-V program correctness. ![image](https://hackmd.io/_uploads/SJxkAIL6gl.png) Only versions that pass correctness testing are worth continuing to measure code size and performance. ## Step 4: Measure Code Size The prerequisite for measuring code size and runtime performance is: **passing the program logic correctness test** in the previous step. ### Method 1: Measure Using size Tool ``` $ riscv64-unknown-elf-size clz_v1.o clz_gcc_O0.o clz_gcc_O2.o clz_gcc_O3.o text data bss dec hex filename 112 0 0 112 70 clz_v1.o 128 0 0 128 80 clz_gcc_O0.o 48 0 0 48 30 clz_gcc_O2.o 92 0 0 92 5c clz_gcc_O3.o ``` ### Method 2: Using objdump > https://dokk.org/manpages/debian/12/binutils-riscv64-unknown-elf/riscv64-unknown-elf-objdump.1.en > > objdump - display information from object files > - [-h|--section-headers|--headers] > - [-d|--disassemble[=symbol]] ``` $ riscv64-unknown-elf-objdump -h clz_v1.o clz_v1.o: file format elf32-littleriscv Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000070 00000000 00000000 00000034 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .data 00000000 00000000 00000000 000000a4 2**0 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 00000000 00000000 000000a4 2**0 ALLOC 3 .riscv.attributes 0000001a 00000000 00000000 000000a4 2**0 CONTENTS, READONLY ``` To measure code size, extract the `Size` field of the `.text` section from `riscv64-unknown-elf-objdump -h` output ### Code Size Analysis Sorted (smallest to largest): 1. **GCC -O2**: 48 bytes 2. **GCC -O3**: 92 bytes (+91%) 3. **v1-hand**: 112 bytes (+133%) 4. **GCC -O0**: 128 bytes (+166%) **Observations:** - GCC -O2 is smallest, compiler optimization is very effective - Hand-written assembly is **2.3 times** larger than GCC -O2 ## Step 5: Generate Disassembly Files ### Disassemble All Versions ```bash riscv64-unknown-elf-objdump -d clz_v1.o > clz_v1.dis riscv64-unknown-elf-objdump -d clz_gcc_O0.o > clz_gcc_O0.dis riscv64-unknown-elf-objdump -d clz_gcc_O2.o > clz_gcc_O2.dis riscv64-unknown-elf-objdump -d clz_gcc_O3.o > clz_gcc_O3.dis ``` ### View Disassembly Content ``` $ cat clz_v1.dis clz_v1.o: file format elf32-littleriscv Disassembly of section .text: 00000000 <clz_v1>: 0: 02000293 li t0,32 4: 01000313 li t1,16 8: 006553b3 srl t2,a0,t1 c: 00038663 beqz t2,18 <.L1_skip> 10: 406282b3 sub t0,t0,t1 14: 00038513 mv a0,t2 00000018 <.L1_skip>: 18: 00135313 srli t1,t1,0x1 1c: 006553b3 srl t2,a0,t1 20: 00038663 beqz t2,2c <.L2_skip> 24: 406282b3 sub t0,t0,t1 28: 00038513 mv a0,t2 0000002c <.L2_skip>: 2c: 00135313 srli t1,t1,0x1 30: 006553b3 srl t2,a0,t1 34: 00038663 beqz t2,40 <.L3_skip> 38: 406282b3 sub t0,t0,t1 3c: 00038513 mv a0,t2 00000040 <.L3_skip>: 40: 00135313 srli t1,t1,0x1 44: 006553b3 srl t2,a0,t1 48: 00038663 beqz t2,54 <.L4_skip> 4c: 406282b3 sub t0,t0,t1 50: 00038513 mv a0,t2 00000054 <.L4_skip>: 54: 00135313 srli t1,t1,0x1 58: 006553b3 srl t2,a0,t1 5c: 00038663 beqz t2,68 <.L5_skip> 60: 406282b3 sub t0,t0,t1 64: 00038513 mv a0,t2 00000068 <.L5_skip>: 68: 40a28533 sub a0,t0,a0 6c: 00008067 ret ``` ### Count Static Instructions ``` $ grep -E '^\s+[0-9a-f]+:' clz_v1.dis | wc -l 28 $ grep -E '^\s+[0-9a-f]+:' clz_gcc_O0.dis | wc -l 32 $ grep -E '^\s+[0-9a-f]+:' clz_gcc_O2.dis | wc -l 12 $ grep -E '^\s+[0-9a-f]+:' clz_gcc_O3.dis | wc -l 23 ``` Record results: | Version | Static Instructions | |---------|---------------------| | v1-hand | 28 | | GCC -O0 | 32 | | GCC -O2 | 12 | | GCC -O3 | 23 | ## Step 6: Measure Runtime Performance (Using Ripes) **Prerequisite**: Passed correctness test + measured code size Link the hand-written RISC-V `clz_v1.s` into `clz_v1.o` and link `clz_gcc_O0.o`, `clz_gcc_O2.o`, `clz_gcc_O3.o` compiled by GCC with `test_framework.o` Generate respectively: - `test_v1_hand.elf` - `test_gcc_O0.elf` - `test_gcc_O2.elf` - `test_gcc_O3.elf` ```bash # Link v1-hand version riscv64-unknown-elf-ld \ -melf32lriscv \ -nostdlib \ --defsym clz=clz_v1 \ -o test_v1_hand.elf \ ../test_framework/test_framework.o \ clz_v1.o # Link GCC -O0 version riscv64-unknown-elf-ld \ -melf32lriscv \ -nostdlib \ --defsym clz=clz \ -o test_gcc_O0.elf \ ../test_framework/test_framework.o \ clz_gcc_O0.o # Link GCC -O2 version riscv64-unknown-elf-ld \ -melf32lriscv \ -nostdlib \ --defsym clz=clz \ -o test_gcc_O2.elf \ ../test_framework/test_framework.o \ clz_gcc_O2.o # Link GCC -O3 version riscv64-unknown-elf-ld \ -melf32lriscv \ -nostdlib \ --defsym clz=clz \ -o test_gcc_O3.elf \ ../test_framework/test_framework.o \ clz_gcc_O3.o ``` Execute the following steps for each version: 1. Start Ripes 2. Select Processor: **5-stage pipeline (RV32I)** 3. File → Load Program, test sequentially: - `test_v1_hand.elf` - `test_gcc_O0.elf` - `test_gcc_O2.elf` - `test_gcc_O3.elf` Run performance test. After completion, check the **Execution info** panel in the lower right: `test_v1_hand.elf`: ![image](https://hackmd.io/_uploads/rJuihvUpgx.png) `test_gcc_O0.elf`: ![image](https://hackmd.io/_uploads/HyNREFL6xg.png) `test_gcc_O2.elf`: ![image](https://hackmd.io/_uploads/B12zStI6ex.png) `test_gcc_O3.elf`: ![image](https://hackmd.io/_uploads/ByJ4HK8ale.png) | ELF Filename | Cycles | Instrs. retired | CPI | IPC | Clock rate | | -------------------- | ------ | --------------- | ---- | ----- | ---------- | | `test_v1_hand.elf` | 644 | 415 | 1.55 | 0.644 | 0 Hz | | `test_gcc_O0.elf` | 1520 | 967 | 1.57 | 0.636 | 0 Hz | | `test_gcc_O2.elf` | 788 | 487 | 1.62 | 0.618 | 0 Hz | | `test_gcc_O3.elf` | 488 | 283 | 1.72 | 0.58 | 0 Hz | # Problem B: CLZ (v2-branchless) > [commit e28af9e](https://github.com/sysprog21/ca2025-quizzes/commit/e28af9e1427b27d60ab63e34e4ede40da3424f50) > [commit eb6fd76](https://github.com/sysprog21/ca2025-quizzes/commit/eb6fd7620def011f171f6f2a7693f3a5a71b9243) ```asm .text .globl clz_v2 .type clz_v2, @function clz_v2: li t0, 32 # n = 32 li t1, 16 # c = 16 # Iteration 1: c = 16 srl t2, a0, t1 # y = x >> 16 # Create conditional mask sltu t3, x0, t2 # t3 = (y != 0) ? 1 : 0 neg t4, t3 # t4 = (y != 0) ? -1 : 0 = 0xFFFFFFFF : 0x00000000 # Conditional update n: n -= (y != 0) ? c : 0 and t5, t1, t4 # t5 = (y != 0) ? c : 0 sub t0, t0, t5 # n -= t5 # Conditional update x: x = (y != 0) ? y : x and t5, t2, t4 # t5 = (y != 0) ? y : 0 not t6, t4 # t6 = (y != 0) ? 0 : 0xFFFFFFFF and t6, a0, t6 # t6 = (y != 0) ? 0 : x or a0, t5, t6 # x = t5 | t6 = (y != 0) ? y : x # Iteration 2: c = 8 srli t1, t1, 1 # c = 8 srl t2, a0, t1 # y = x >> 8 sltu t3, x0, t2 # condition neg t4, t3 # mask and t5, t1, t4 # conditional c sub t0, t0, t5 # n -= (y != 0) ? c : 0 and t5, t2, t4 # conditional y not t6, t4 # inverse mask and t6, a0, t6 # conditional x or a0, t5, t6 # x = (y != 0) ? y : x # Iteration 3: c = 4 srli t1, t1, 1 # c = 4 srl t2, a0, t1 # y = x >> 4 sltu t3, x0, t2 neg t4, t3 and t5, t1, t4 sub t0, t0, t5 and t5, t2, t4 not t6, t4 and t6, a0, t6 or a0, t5, t6 # Iteration 4: c = 2 srli t1, t1, 1 # c = 2 srl t2, a0, t1 # y = x >> 2 sltu t3, x0, t2 neg t4, t3 and t5, t1, t4 sub t0, t0, t5 and t5, t2, t4 not t6, t4 and t6, a0, t6 or a0, t5, t6 # Iteration 5: c = 1 srli t1, t1, 1 # c = 1 srl t2, a0, t1 # y = x >> 1 sltu t3, x0, t2 neg t4, t3 and t5, t1, t4 sub t0, t0, t5 and t5, t2, t4 not t6, t4 and t6, a0, t6 or a0, t5, t6 # return n - x sub a0, t0, a0 ret .size clz_v2, .-clz_v2 ``` The testing procedure is identical to v1-basic. ```bash cd q1b-uf8/clz/v2-branchless ``` Code Size: ``` $ riscv64-unknown-elf-size clz_v2.o clz_gcc_O0.o clz_gcc_O2.o clz_gcc_O3.o text data bss dec hex filename 212 0 0 212 d4 clz_v2.o 344 0 0 344 158 clz_gcc_O0.o 108 0 0 108 6c clz_gcc_O2.o 108 0 0 108 6c clz_gcc_O3.o ``` Static instructions: | Version | Static Instructions | |---------|---------------------| | v2-hand | 53 | | GCC -O0 | 86 | | GCC -O2 | 27 | | GCC -O3 | 27 | ### ⭐ Observation: `clz_gcc_O2.dis` vs `clz_gcc_O3.dis` ```diff diff -u clz_gcc_O2.dis clz_gcc_O3.dis --- clz_gcc_O2.dis 2025-10-10 22:50:57.562537064 +0800 +++ clz_gcc_O3.dis 2025-10-10 22:50:57.566537061 +0800 @@ -1,5 +1,5 @@ -clz_gcc_O2.o: file format elf32-littleriscv +clz_gcc_O3.o: file format elf32-littleriscv Disassembly of section .text.clz: ``` From the diff results above, we can see that the two disassembly results (`.dis`) are almost identical, with the only difference being: the filename changes from `clz_gcc_O2.o` to `clz_gcc_O3.o`. This indicates that the v2 version of `clz.c` has reached its optimization limit at `-O2`, and `-O3` has nothing more to do. Runtime Performance (Using Ripes): `test_v2_hand.elf`: ![image](https://hackmd.io/_uploads/ryHR1jUTlx.png) `test_gcc_O0.elf`: ![image](https://hackmd.io/_uploads/r1_eljI6xl.png) `test_gcc_O2.elf`: ![image](https://hackmd.io/_uploads/B15blj8pee.png) `test_gcc_O3.elf`: ![image](https://hackmd.io/_uploads/rkRzloIale.png) | ELF Filename | Cycles | Instrs. retired | CPI | IPC | Clock rate | | -------------------- | ------ | --------------- | ---- | ----- | ---------- | | `test_v2_hand.elf` | 944 | 835 | 1.13 | 0.885 | 0 Hz | | `test_gcc_O0.elf` | 1532 | 1159 | 1.32 | 0.757 | 0 Hz | | `test_gcc_O2.elf` | 536 | 451 | 1.19 | 0.841 | 0 Hz | | `test_gcc_O3.elf` | 536 | 451 | 1.19 | 0.841 | 0 Hz | # Problem B: CLZ (v3-table) > [Commit 958167e](https://github.com/sysprog21/ca2025-quizzes/commit/958167e51b3246d988e8f6393e473a7cca8dda8d) > [Commit 2828c7c](https://github.com/sysprog21/ca2025-quizzes/commit/2828c7cdb464b7a5000521a994f49cae68a407be) ```asm .data .align 2 # 8-bit lookup table: stores leading zeros for each number from 0-255 clz_table_8: .byte 8, 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4 .byte 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 .byte 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 .byte 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 .byte 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 .byte 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 .byte 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 .byte 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .text .globl clz_v3 .type clz_v3, @function # a0 = input value & return value # a1 = extracted byte value (0–255) used for lookup table # a2 = base address of clz_table_8 # a3 = offset value (0, 8, 16, or 24) clz_v3: # Special case: x = 0 beqz a0, .L_return_32 # Load the base address of the lookup table into a2 la a2, clz_table_8 # Check [31:24] srli a1, a0, 24 # a1 = x >> 24 bnez a1, .L_byte3 # if (byte), offset=0 # Check [23:16] srli a1, a0, 16 # a1 = x >> 16 andi a1, a1, 0xFF # a1 = (x >> 16) & 0xFF bnez a1, .L_byte2 # if (byte), offset=8 # Check [15:8] srli a1, a0, 8 # a1 = x >> 8 andi a1, a1, 0xFF # a1 = (x >> 8) & 0xFF bnez a1, .L_byte1 # if (byte), offset=16 # Check [7:0] andi a1, a0, 0xFF # a1 = x & 0xFF li a3, 24 # offset = 24 j .L_lookup .L_byte3: # [31:24], offset = 0 li a3, 0 # offset = 0 j .L_lookup .L_byte2: # [23:16], offset = 8 li a3, 8 # offset = 8 j .L_lookup .L_byte1: # [15:8], offset = 16 li a3, 16 # offset = 16 .L_lookup: # result = offset + clz_table_8[byte] add a2, a2, a1 # a2 = &clz_table_8[byte] lbu a0, 0(a2) # a0 = clz_table_8[byte] add a0, a0, a3 # a0 = clz_table_8[byte] + offset ret .L_return_32: # Special case: x = 0 li a0, 32 ret ``` The testing procedure is identical to v1-basic and v2-branchless. ```bash cd q1b-uf8/clz/v3-table ``` Code Size: ``` $ riscv64-unknown-elf-size clz_v3.o clz_gcc_O0.o clz_gcc_O2.o clz_gcc_O3.o text data bss dec hex filename 100 256 0 356 164 clz_v3.o 488 0 0 488 1e8 clz_gcc_O0.o 384 0 0 384 180 clz_gcc_O2.o 384 0 0 384 180 clz_gcc_O3.o ``` Static Instructions: | Version | Static Instructions | |---------|---------------------| | v3-hand | 25 | | GCC -O0 | 58 | | GCC -O2 | 32 | | GCC -O3 | 32 | ### ⭐ Observation: `clz_gcc_O2.dis` vs `clz_gcc_O3.dis` ```diff diff -u clz_gcc_O2.dis clz_gcc_O3.dis --- clz_gcc_O2.dis 2025-10-11 13:32:58.711310893 +0800 +++ clz_gcc_O3.dis 2025-10-11 13:32:58.715258710 +0800 @@ -1,5 +1,5 @@ -clz_gcc_O2.o: file format elf32-littleriscv +clz_gcc_O3.o: file format elf32-littleriscv Disassembly of section .text.clz: ``` Same situation as the v2 version — the v3 version of `clz.c` has reached its optimization limit at `-O2`, and `-O3` has nothing more to do. Runtime Performance (Using Ripes): `test_v3_hand.elf`: ![image](https://hackmd.io/_uploads/rkxHCPDTgx.png) `test_gcc_O0.elf`: ![image](https://hackmd.io/_uploads/BytU0vDaeg.png) `test_gcc_O2.elf`: ![image](https://hackmd.io/_uploads/HJkOCvvplx.png) `test_gcc_O3.elf`: ![image](https://hackmd.io/_uploads/SyfKAvPpel.png) | ELF Filename | Cycles | Instrs. retired | CPI | IPC | Clock rate | | -------------------- | ------ | --------------- | ---- | ----- | ---------- | | `test_v3_hand.elf` | 272 | 163 | 1.67 | 0.599 | 0 Hz | | `test_gcc_O0.elf` | 392 | 271 | 1.45 | 0.691 | 0 Hz | | `test_gcc_O2.elf` | 272 | 163 | 1.67 | 0.599 | 0 Hz | | `test_gcc_O3.elf` | 272 | 163 | 1.67 | 0.599 | 0 Hz | # Analysis `q1b-uf8/clz/v1-basic/clz_v1.s`: ```asm clz_v1: li t0, 32 # n = 32 li t1, 16 # c = 16 # Iteration 1: c = 16 srl t2, a0, t1 # y = x >> 16 beqz t2, .L1_skip # if (y == 0) sub t0, t0, t1 # n -= 16 mv a0, t2 # x = y ``` Test input: `0x80000000` (`a0 = 0x80000000`) ``` .word 0x80000000 # 10: highest bit -> 0 ``` ## IF Stage (Instruction Fetch) The main goal of the IF stage is: "Fetch the next instruction to execute from memory" — that is, let the PC (Program Counter) provide the address → read the instruction → prepare to pass it to the next stage (ID). ![image](https://hackmd.io/_uploads/SyV9I2vplg.png) ![image](https://hackmd.io/_uploads/S16c8nvTgl.png) ![image](https://hackmd.io/_uploads/r1Zi8hDpgg.png) ![image](https://hackmd.io/_uploads/S1DujnD6gg.png) At PC = `0x000001e8`, the instruction memory fetches `0x006553b3`, which decodes to: ``` srl x7, x10, x6 ``` The PC adder outputs `0x000001ec` (next instruction). The IF/ID register has `enable = green = 1`, `clear = red = 0`, indicating normal pipeline flow. This stage doesn't update memory because it's only fetching instructions. Reference: https://www.rose-hulman.edu/class/csse/csse232/pdf/RISCV_Green_Card.pdf ![image](https://hackmd.io/_uploads/rkMCb1d6lx.png) ![image](https://hackmd.io/_uploads/SynJzk_Tgg.png) ``` srl x7, x10, x6 ``` According to the above explanation, `srl` is an R-type instruction. The 32-bit instruction format for R-type is: ``` funct7 | rs2 | rs1 | funct3 | rd | opcode | 31 25 24 20 19 15 14 12 11 7 6 0 ``` - `opcode = 0110011` - `funct3 = 101` - `funct7 = 0000000` - `rd = x7 = 7` - `rs1 = x10 = 10` - `rs2 = x6 = 6` | Field | Bit Range | Value (Binary) | Decimal/Note | | -------- | --------- | -------------- | --------------- | | `funct7` | 31..25 | `0000000` | SRL | | `rs2` | 24..20 | `00110` | x6 (=6) | | `rs1` | 19..15 | `01010` | x10 (=10) | | `funct3` | 14..12 | `101` | SRL/SRA group | | `rd` | 11..7 | `00111` | x7 (=7) | | `opcode` | 6..0 | `0110011` | OP (R-type) | Combine the above into 32-bit: ``` 0000000 00110 01010 101 00111 0110011 = 0000 0000 0110 0101 0101 0011 1011 0011 = 0x006553b3 ``` Leading 0 can be omitted as `0x6553b3`, but tools often display full 8 digits: - In Little Endian memory, the 4 bytes would be sequentially: `b3 53 65 00` - Ripes' "instr" column will display the combined 32-bit `0x006553b3` ### Multiplexer: PC Selector ![image](https://hackmd.io/_uploads/Hk0Ghl_ale.png) - Bottom input: `0x000001e8` (current PC) - Top input: Branch or jump target (currently unused) - Output: `0x000001e8` (still current PC) This multiplexer determines "the source of the next PC": - If normal execution: select `PC + 4` - If encountering branch or jump instruction: select branch target During this cycle (`srl` instruction), there's no branch/jump, so the multiplexer selects "current PC", allowing the pipeline to proceed sequentially. ### Adder ![image](https://hackmd.io/_uploads/H1oS3x_Tel.png) The figure shows: * Bottom input: `PC = 0x000001e8` * Top input: `0x00000004` * Output: `0x000001ec` RISC-V instructions are 4 bytes each, so the adder calculates "the address of the next instruction": ``` PC = PC + 4 = 0x000001e8 + 0x4 = 0x000001ec ``` So the next instruction will be fetched from `0x000001ec`. ### Instruction Memory ![image](https://hackmd.io/_uploads/BJID2eu6lx.png) The figure shows: - Input address: `addr = 0x000001e8` - Output instruction: `instr = 0x006553b3` Instruction Memory's function is to read the corresponding 32-bit instruction content based on the address provided by PC. Here it fetches the machine code of `srl x7, x10, x6`. ### Compressed Decoder Below Instruction Memory ![image](https://hackmd.io/_uploads/H13u3gd6lg.png) The figure shows: - Input: `0x006553b3` - Output: `0x006553b3` RISC-V supports 16-bit "compressed instructions (RVC)" and 32-bit normal instructions. Therefore, this module will: - Check if the instruction is in compressed format - If it's a compressed instruction (16 bit), expand it to a 32-bit instruction here - If not (like this example is standard 32-bit), pass it through unchanged Since `0x006553b3` in the figure is already a 32-bit instruction, the decoder doesn't change the content, and input equals output. ### IF/ID Pipeline Register ![image](https://hackmd.io/_uploads/Hk7shgd6ll.png) - enable = green (1) → allow data to proceed to the next stage - clear = red (0) → no flush (meaning no branch or exception) - IF/ID Register buffers: - Instruction content (`0x006553b3`) - PC value (`0x000001e8`) ## ID Stage (Instruction Decode) ![image](https://hackmd.io/_uploads/rJGSI6Pagg.png) ![image](https://hackmd.io/_uploads/Syyk0gdpel.png) ID Stage main tasks: - Decode instruction (Control Unit generates control signals) - Read source register values (Register File) - Generate immediate value (Immediate Generator) - Pass results to the next stage IDEX pipeline register ### Decode Module (Control Unit) ![image](https://hackmd.io/_uploads/rJC4Bg_age.png) - Decode Module identifies this instruction as SRL (Shift Right Logical) - `opcode = 0x0a` (R-type) - `rs1 = 0x0a` (`x10 = 10`) - `rs2 = 0x06` (`x6 = 6`) - `rd = 0x07` (`x7 = 7`) - In ID stage, Control Unit generates control signals based on opcode / funct3 / funct7: - `RegWrite = 1` (because result will be written back to x7) - `ALUSrc = 0` (because it's a register-to-register operation, not immediate value) - `MemRead = 0` - `MemWrite = 0` - `Branch = 0` - `ALUOp = "SRL"` ### Register File ![image](https://hackmd.io/_uploads/HkgJKedpee.png) Instruction `srl x7, x10, x6`: - R1 idx: `rs1 = 0x0a` (`x10 = 10`) - R2 idx: `rs2 = 0x06` (`x6 = 6`) Register File automatically reads the two corresponding source registers `rs1` and `rs2` based on decode results: ![image](https://hackmd.io/_uploads/SJPJoe_axx.png) - Reg 1: 0x80000000 - Reg 2: 0x1000007d These values will be sent to the ALU input multiplexer. ### Immediate Generator ![image](https://hackmd.io/_uploads/rJa9jg_Tll.png) Value in the figure: `0xdeadbeef` - This is Ripes' default display value (because SRL is R-type and doesn't use immediate value) - Actually, this module won't be used for R-type instructions - For I-type (e.g., `addi`), this would generate a sign-extended immediate value ### ID/EX Pipeline Register ![image](https://hackmd.io/_uploads/Sy51ngdpee.png) The figure shows: - `rs1 = 0x80000000` - `rs2 = 0x1000007d` ID/EX Pipeline Register passes decoded results to the next stage (EX): - `enable = green = 1` → pipeline proceeds normally - `clear = red = 0` → no flush ## EX Stage (Execute) ![image](https://hackmd.io/_uploads/Sy3FaluTeg.png) ![image](https://hackmd.io/_uploads/Skwnpxdplx.png) EX Stage function: - Goal: ALU (Arithmetic Logic Unit) performs actual operations (shifts, addition/subtraction, AND/OR, etc.) and determines whether to branch - Input source: From ID/EX Pipeline Register (includes Register values and Control Signals) - Output result: Sent to EX/MEM Pipeline Register (for subsequent MEM or WB use) ### ID/EX Pipeline Register ![image](https://hackmd.io/_uploads/BkCHrW_agx.png) The figure shows: - `rs1 = 0x80000000` - `rs2 = 0x1000007d` - `PC = 0x000001e8` - Control signals: - `enable = green = 1` - `clear = red = 0` ID/EX Pipeline Register is the data handoff point from ID → EX, it buffers: - Register input values (`rs1`, `rs2`) - Control signals corresponding to the instruction (such as `RegWrite`, `ALUOp`, `Branch`, etc.) - Current PC (for branch calculation) - `enable = 1` indicates pipeline proceeds normally - `clear = 0` indicates no flush (non-jump instruction stage) ### ALU ![image](https://hackmd.io/_uploads/S1G9Obdpll.png) ![image](https://hackmd.io/_uploads/rkecjb_Tel.png) ``` srl x7, x10, x6 ``` Its function is: ``` x7 = x10 >> (x6[4:0]) = x10 >> (x6 & 0x1F) ``` In the official RV32I specification: > For SRL, SRA, and SLL instructions, only the lower 5 bits of the shift amount (rs2[4:0]) are used. > [RISC-V User-Level ISA Spec, Vol. I: Unprivileged Architecture, §2.5 Shift Instructions] Meaning: Even if rs2 contains a large value, only the lower 5 bits can be used, because in RV32I, the valid shift range for a 32-bit integer is 0~31, which has 32 possibilities ⇒ 5 bits are sufficient to represent. Values in the figure: - `Op1 = 0x80000000` - `rs1` → data to be shifted - `Op2 = 0x00000010` - Only the lower 5 bits are used as shift amount - `x6[4:0] = 0x1000007d[4:0] = x6 & 0x1F = 0x1d` - `Res = 0x00008000` - `x7 = x10 >> (x6 & 0x1F) = 0x80000000 >> 0x1d = 29` - However, `Res` here shows not 29, but `0x00008000`, because a **data hazard** occurred ``` clz_v1: li t0, 32 # n = 32 li t1, 16 # c = 16 # Iteration 1: c = 16 srl t2, a0, t1 # y = x >> 16 beqz t2, .L1_skip # if (y == 0) sub t0, t0, t1 # n -= 16 mv a0, t2 # x = y ``` | Cycle | 1 | 2 | 3 | 4 | 5 | 6 | |:---------------- |:---:|:---:|:---:|:---:|:---:|:---:| | `li t1, 16` | IF | ID | EX | MEM | WB | | | `srl t2, a0, t1` | | IF | ID | EX | MEM | WB | - Hazard type occurring here: **RAW (Read After Write)** - The `t1` that `srl` needs to read is precisely the destination register that the previous `li` (i.e., `addi t1, x0, 16`) is writing back to - **Forwarding path:** From `li` EX/MEM → `srl` ALU - When the pipeline detects that `srl` needs to use the result of `t1`, it directly forwards the execution result of `li` from the output of the EX/MEM Pipeline Register back to the ALU input, without waiting for `li` to complete writeback (WB) - This way, `srl` can get the correct `t1 = 16 = 0x00000010` in the EX stage of cycle 4, avoiding pipeline stall and maintaining correct execution - That is: `x7 = x10 >> (x6 & 0x1F) = 0x80000000 >> (0x00000010 & 0x1F) = 0x80000000 >> 16 = 0x00008000` ### ALU Multiplexers ![image](https://hackmd.io/_uploads/r1hXXf_axx.png) MUX with three green dots in the figure: - Left MUX: Select Op1 = rs1 (non-immediate value) - Right MUX: Select Op2 = rs2 (non-immediate value) **Reason:** - Control Signal `ALUSrc = 0` (because this `srl` instruction is R-type, doesn't use immediate value) - Therefore, both inputs come from Register File: - `Op1` ← `rs1 (x10)` - `Op2` ← `rs2 (x6)` ### Branch Module ![image](https://hackmd.io/_uploads/BJxMEfOaex.png) The figure shows: `Branch taken = red = 0` This instruction is `srl`, not a branch instruction, therefore: - `Branch = 0` - `Branch taken` signal is red (false) - For instructions like `beq` or `bne`, this would use ALU's zero flag to determine if the branch should be taken ### EX/MEM Pipeline Register ![image](https://hackmd.io/_uploads/ByBO4G_Tge.png) EX/MEM Pipeline Register will: - Buffer ALU result (`0x00008000`) - Buffer Control Signal (such as whether to write back to register) - Buffer destination register number (`rd = x7`) ## MEM Stage (Memory Access) ![image](https://hackmd.io/_uploads/HywM2MuTxg.png) ![image](https://hackmd.io/_uploads/H1KGBfd6el.png) ### EX/MEM Pipeline Register - This stage's input comes from the previous cycle's EX stage - `ALU Res = 0x00008000`: This is the ALU operation result from the previous stage (EX) - For a branch instruction (e.g., `beq`) in the MEM stage, this represents the branch target address (`PC + imm`) - For a general arithmetic instruction (e.g., `srl` in the figure) in the MEM stage, it's just passed through to the next stage (WB), doesn't touch data memory - ALU operation result is buffered in the EX/MEM Pipeline Register, waiting for WB stage to write to x7 ### Data Memory Although the `srl` instruction is in the MEM stage, `srl` is not a Load/Store instruction, therefore: - Data memory is not activated - `Wr en = 0` (red) - `MemRead = 0` - `MemWrite = 0` Values displayed in memory: - `Addr. = 0x00008000` - `Data in = 0x00000010` - `Read out = 0x00000000` These are all don't care values (just signals flowing in the pipeline, not actual access operations). ## WB Stage (Write Back) ![image](https://hackmd.io/_uploads/SJ4SizOpel.png) ![image](https://hackmd.io/_uploads/rJ3riM_aeg.png) In the WB (Write Back) stage, the execution result of instruction `srl x7, x10, x6` is ready to be written back to the register file. ### MEM/WB Pipeline Register - This stage's input is passed from the MEM stage - `ALU result = 0x00008000` (obtained from `srl x7, x10, x6`) - Buffered control signals (such as `RegWrite`) are also passed from the MEM stage - The main function of this register is: - Buffer the value to be written back - Buffer the target register number (`rd`) to write back to - Control signal, determines whether to actually perform writeback ### Write Back Multiplexer ![image](https://hackmd.io/_uploads/r1P8pGd6xe.png) - WB stage must determine the source of data to write back: - If Load instruction → write back the value read from Data Memory (`MemRead=1`) - If general ALU instruction (e.g., the `srl` instruction currently in WB) → write back from ALU result (`MemRead=0`) - In the figure: - Bottom input (from Data memory Read out) = `0x00000000` - Top input (from ALU result) = `0x00008000` - Since this cycle is an R-type instruction (SRL), the writeback source is ALU result → `0x00008000` ### Write Back Register File ![image](https://hackmd.io/_uploads/SynZ0Gd6ll.png) - According to the information in MEM/WB Pipeline Register: - `rd = x7` - `Write data = 0x00008000` - `RegWrite = 1` (allow write) Therefore, the next cycle will write `0x00008000` to Register x7, as shown in the figure below: ![image](https://hackmd.io/_uploads/rkFtAfOpxg.png)