# CA2025 Quiz6 > 張家瑋 ## Q4: What ISA feature most directly impacts compiler optimization freedom? ### Ans: The number of registers is determined by the ISA and can not be changed once the CPU is built. While a smaller number of registers allows for faster access times, on the other hand, a smaller register file reduces compiler optimization freedom.This is because the compiler needs to use registers be more efficiently in that case. ### Reference: - Lecture 7: Intro to RISC-V `Each ISA a predetermined number of registers` - slide 15 `RISC-V code must be very carefully put together to efficiently use registers` - slide 15 `Why 32? Smaller is faster, but too small is bad` - slide 15 - Lecture 8: RISC-V Data Transfer `Each kernel has a register footprint and note that register use can be optimized better than I did before` - slide 3 ### How to validate my answer with experiments Constraining the compiler to use fewer registers on the same RISC-V pipeline #### Experiment Design 1 : compare number of lw/sw instructions - Write a high register-pressure program Write a high register-pressure workload and implement it in C so that the compiler performs register allocation. For example, we can reuse `bf16_sqrt` algorithm from [Quiz 1 Problem C](https://hackmd.io/@sysprog/arch2025-quiz1-sol#Problem-C). This program needs to maintain many live temporaries simultaneously. In addition, under the ISA RV32I subset multiplication must be implemented in other methods (no `mul`), which introduces even more live temporaries. Overall, these conditions make this program becomes a high register pressure program. - Compile two versions We then compile the same C program with and without register restrictions ``(via -ffixed-xN)``. - command ```bash= # Normal build riscv64-unknown-elf-gcc -O2 -S -march=rv32i -mabi=ilp32 bf16_sqrt.c -o normal.s # Restricted build (example: reserve x5~x12) riscv64-unknown-elf-gcc -O2 -S -march=rv32i -mabi=ilp32 -ffixed-x5 -ffixed-x6 -ffixed-x7 -ffixed-x28 -ffixed-x29 -ffixed-x30 bf16_sqrt.c -o limited.s ``` - Measure strategy Now we check whether `sw/lw` instructions significantly increase in the restricted version. That's because with fewer registers, the compiler can't keep all values in registers, so it must spill them to the stack and reload them later, which introduces extra sw/lw instructions. #### Experiment Design 2: Run on MyCPU (3-stage pipeline) and analyze waveform - Setup - Target: MyCPU(3-pipeline) - Analysis Tools: Verilator, GTKWave - Compile two versions ```bash= # Normal riscv64-unknown-elf-gcc -O2 -march=rv32i -mabi=ilp32 main.c bf16_sqrt.c -o normal.elf # Restricted riscv64-unknown-elf-gcc -O2 -march=rv32i -mabi=ilp32 \ -ffixed-x5 -ffixed-x6 -ffixed-x7 -ffixed-x28 -ffixed-x29 -ffixed-x30 \ main.c bf16_sqrt.c -o limited.elf ``` - Create .asmbin file ```bash= riscv64-unknown-elf-objcopy -O binary -j .text -j .data normal.elf normal.asmbin riscv64-unknown-elf-objcopy -O binary -j .text -j .data limited.elf limited.asmbin ``` Then we copy `.asmbin` file into `/src/main/resources/` - Run on Verilator and generate waveform ```bash= # Normal make sim SIM_ARGS="-instruction src/main/resources/normal.asmbin" cp trace.vcd trace_normal.vcd # Restricted make sim SIM_ARGS="-instruction src/main/resources/limited.asmbin" cp trace.vcd trace_limited.vcd ``` - observation ```bash= gtkwave trace_normal.vcd gtkwave trace_limited.vcd ``` - Verification Now we check whether the number of pulses on `mem_read_enable / mem_write_enable` is significantly higher in the restricted version. These pulses indicate more frequent data-memory accesses, largely caused by additional lw/sw instructions from stack spills and reloads. ### Implementation - Encounter problem First, I thought as compiler is no longer to use `mul` instruction, this would make it tend to use traditional multiplication, which will produce lots of live temporaries. However, GCC call to a software runtime helper `mulsi3` (from libgcc) to perform 32-bit integer multiplication. ```asm .text .globl __mulsi3 ... call __mulsi3 ... ``` Second, we no longer need to perform a traditional multiplication, so the number of live temporaries we have to keep track of drops significantly. Moreover, with `-O2` enabled, the compiler-generated code is actually quite good—certainly better than my line-by-line C-to-assembly translation. As a result, register pressure under the limited-register setting isn’t really an issue in this case. - Solution (Make register usage more restricted) command ```bash= riscv64-unknown-elf-gcc -O2 -S -march=rv32i -mabi=ilp32 -ffixed-x5 -ffixed-x6 -ffixed-x7 -ffixed-x9 -ffixed-x18 -ffixed-x19 -ffixed-x20 -ffixed-x21 -ffixed-x22 -ffixed-x23 -ffixed-x24 -ffixed-x25 -ffixed-x26 -ffixed-x27 -ffixed-x28 -ffixed-x29 -ffixed-x30 bf16_sqrt.c -o limited.s ``` Here we constrait compiler no allowed to use registers `t0-t5` and `s1-s11` in generating assembly `limited.s` - Observation ```BASH = blz@localhost:~/ca2025-mycpu/3-pipeline/csrc$ grep -E '^\s*(lw|sw)\b' normal.s | wc -l 18 blz@localhost:~/ca2025-mycpu/3-pipeline/csrc$ grep -E '^\s*(lw|sw)\b' limited.s | wc -l 26 ``` We observed that the number of `lw/sw` instructions truly increased. <details> <summary>normal.s </summary> ```asm= .file "bf16_sqrt.c" .option nopic .attribute arch, "rv32i2p1" .attribute unaligned_access, 0 .attribute stack_align, 16 .text .globl __mulsi3 .align 2 .globl bf16_sqrt .type bf16_sqrt, @function bf16_sqrt: slli a0,a0,16 srli a0,a0,16 addi sp,sp,-32 srli a5,a0,7 sw s5,4(sp) sw ra,28(sp) andi a5,a5,0xff li a3,255 srli a4,a0,15 andi s5,a0,127 beq a5,a3,.L28 bne a5,zero,.L4 li a0,0 beq s5,zero,.L3 li a0,32768 addi a0,a0,-64 neg a4,a4 and a0,a0,a4 .L3: lw ra,28(sp) lw s5,4(sp) addi sp,sp,32 jr ra .L4: li a0,32768 addi a0,a0,-64 bne a4,zero,.L3 sw s0,24(sp) addi s0,a5,-127 sw s1,20(sp) sw s2,16(sp) sw s3,12(sp) sw s4,8(sp) andi a4,s0,1 ori s5,s5,128 bne a4,zero,.L29 srai s0,s0,1 addi s0,s0,127 .L8: li s3,128 li s4,256 li s2,90 .L11: add s1,s2,s4 srli s1,s1,1 mv a1,s1 mv a0,s1 call __mulsi3 srli a0,a0,7 bltu s5,a0,.L9 addi s2,s1,1 mv s3,s1 .L10: bgeu s4,s2,.L11 li a5,255 bleu s3,a5,.L12 addi a0,s0,1 slli a0,a0,7 slli a0,a0,16 srli s3,s3,1 srli a0,a0,16 .L13: lw s0,24(sp) lw ra,28(sp) andi s3,s3,127 lw s1,20(sp) lw s2,16(sp) lw s4,8(sp) lw s5,4(sp) or a0,a0,s3 lw s3,12(sp) addi sp,sp,32 jr ra .L28: bne s5,zero,.L3 beq a4,zero,.L3 lw ra,28(sp) li a0,32768 lw s5,4(sp) addi a0,a0,-64 addi sp,sp,32 jr ra .L29: addi a5,a5,-128 srai a5,a5,1 slli s5,s5,1 addi s0,a5,127 j .L8 .L9: addi s4,s1,-1 j .L10 .L12: li a5,127 bgtu s3,a5,.L14 li a4,1 j .L16 .L15: beq s0,a4,.L30 .L16: slli s3,s3,1 addi s0,s0,-1 bleu s3,a5,.L15 .L14: slli a0,s0,7 slli a0,a0,16 srli a0,a0,16 j .L13 .L30: li a0,128 j .L13 .size bf16_sqrt, .-bf16_sqrt .ident "GCC: (13.2.0-11ubuntu1+12) 13.2.0" ``` </details> <details> <summary>limited.s </summary> ```asm= .file "bf16_sqrt.c" .option nopic .attribute arch, "rv32i2p1" .attribute unaligned_access, 0 .attribute stack_align, 16 .text .globl __mulsi3 .align 2 .globl bf16_sqrt .type bf16_sqrt, @function bf16_sqrt: slli a0,a0,16 srli a0,a0,16 srli a5,a0,7 andi a5,a5,0xff li a3,255 srli a4,a0,15 andi a2,a0,127 beq a5,a3,.L30 bne a5,zero,.L4 li a0,0 beq a2,zero,.L31 li a0,32768 neg a4,a4 addi a0,a0,-64 and a0,a0,a4 ret .L4: li a0,32768 addi a0,a0,-64 bne a4,zero,.L32 addi sp,sp,-48 addi a4,a5,-127 ori a2,a2,128 sw ra,44(sp) sw s0,40(sp) andi a3,a4,1 sw a2,24(sp) bne a3,zero,.L33 srai a4,a4,1 addi a5,a4,127 sw a5,28(sp) .L8: li a5,128 sw a5,16(sp) li a4,256 li s0,90 .L11: add a1,s0,a4 srli a1,a1,1 mv a0,a1 sw a4,20(sp) sw a1,12(sp) call __mulsi3 lw a1,12(sp) lw a5,24(sp) srli a0,a0,7 addi a4,a1,-1 bltu a5,a0,.L10 lw a4,20(sp) addi s0,a1,1 sw a1,16(sp) .L10: bgeu a4,s0,.L11 lw a4,16(sp) li a5,255 bleu a4,a5,.L12 lw a5,28(sp) addi a0,a5,1 slli a0,a0,7 srli a5,a4,1 slli a0,a0,16 sw a5,16(sp) srli a0,a0,16 .L13: lw a5,16(sp) lw ra,44(sp) andi s0,a5,127 or a0,a0,s0 lw s0,40(sp) addi sp,sp,48 jr ra .L30: bne a2,zero,.L27 beq a4,zero,.L34 li a0,32768 addi a0,a0,-64 ret .L27: ret .L33: addi a5,a5,-128 srai a5,a5,1 slli a4,a2,1 addi a5,a5,127 sw a4,24(sp) sw a5,28(sp) j .L8 .L34: ret .L12: lw a4,16(sp) li a5,127 bgtu a4,a5,.L14 li a4,1 j .L16 .L15: lw a3,28(sp) beq a3,a4,.L35 .L16: lw a3,16(sp) lw a2,28(sp) slli a3,a3,1 addi a2,a2,-1 sw a3,16(sp) sw a2,28(sp) bleu a3,a5,.L15 slli a0,a2,7 slli a0,a0,16 srli a0,a0,16 j .L13 .L31: ret .L32: ret .L14: lw a5,28(sp) slli a0,a5,7 slli a0,a0,16 srli a0,a0,16 j .L13 .L35: li a0,128 j .L13 .size bf16_sqrt, .-bf16_sqrt .ident "GCC: (13.2.0-11ubuntu1+12) 13.2.0" ``` </details> From the above code, we can see that the compiler keeps long-lived variables in `s0-s5` in `normal.s`; On the other hands, due to the restriction of register, the compiler has not enough registers in `limited.s` to save long-lived variable, which will end up with lots of spill/reload situation. We can observe above scenario in label `.L11` (binary search in original code) - `.L11` section in `normal.s` - `low/mid/high/result/m` all use s regs stored - there's no lw/sw inside loop ```asm add s1,s2,s4 srli s1,s1,1 # mid = (low+high)/2 mv a1,s1 mv a0,s1 call __mulsi3 srli a0,a0,7 # sq = (mid * mid) / 128; bltu s5,a0,.L9 addi s2,s1,1 mv s3,s1 ... bgeu s4,s2,.L11 ``` - `.L11` section in `limited.s` ```asm add a1,s0,a4 # s0=low,a4=high srli a1,a1,1 # a1=mid mv a0,a1 sw a4,20(sp) # spill high sw a1,12(sp) # spill mid call __mulsi3 lw a1,12(sp) # reload mid lw a5,24(sp) # reload m srli a0,a0,7 addi a4,a1,-1 bltu a5,a0,.L10 lw a4,20(sp) addi s0,a1,1 sw a1,16(sp) ... lw a4,20(sp) # reload high ... bgeu a4,s0,.L11 ``` Because we need `call __mulsi3` for multiplcation, compiler have to assume that all caller saved registers (a, t regs) will be coverd after the function call. Due to this, it should be remain value where it might be used later, however, there's no free register to use. The only way to save this values is spill them to memory(stack) and reload them later when we need them. In particular, variable `high` is repeatedly stored to and loaded from in memory address `stack pointer + 20`, while variable `mid` is the same respect to `stack pointer + 12`, and so on. - wave validation Next, we run the assembly program on the pipelined RISC-V CPU. he workflow is very similar to [our Assignment 3](https://hackmd.io/mNi8JittTgWWEjnLOy3ybw?view#Modified-Assembly), so i'll skip the steps here and just validate the result. >trace_normal.vcd ![image](https://hackmd.io/_uploads/SJPT0T3Ebe.png) First, let's look at the `trace_normal.vcd`. As mentioned above, we focus on the signal `mem_io_memory_read_enable` and `mem_io_memory_write_enable`, which correspond to the usage of lw/sw. As we can see, these signals are asserted only a few times over hundreds of executed instructions. >limited_normal.vcd ![image](https://hackmd.io/_uploads/BJ58g0hV-g.png) This time the number of the signal `mem_io_memory_read_enable` and `mem_io_memory_write_enable` are asserted has significantly increased , which indicates more frequent memory accesses (loads/stores) during execution. This matches our expectation: with fewer available registers, the compiler spills more long-lived values to the stack and later reloads them, leading to more lw/sw activity and higher memory traffic. ## Q5: Why is PC-relative addressing important? ### Ans: Rather than using absolute addressing, which typically requires building a 32-bits immediate, PC-relative addressing only requires few bit field to determine the jump target. Moreover, this feature enables Position-Independent Code(PIC). While absolute addressing is brittle to code movement, PC-relative addressing doesn't need editing even if text is relocated. This is because the offset between instructions remains constant when the whole text segment is relocated. ### Reference: - Lecture 12: Instruction Formats (2) `Use sparingly: Brittle to code movement/need to build 32-bit immediate` - silde 8 ``“Position-Independent Code”: If all of code moves, relative offsets don’t change!`` - silde 8 - Lecture 13: Compiling-Assembling-Linking-Loading `Never relocate Position-independent code (PIC)` - silde 22 `Again, conditional branches (B-Type) don’t need editing!` - silde 23 `PC-relative addressing preserved even if text is relocated` - silde 23 ### How to validate my answer with experiments To see whether absolute addressing instruction need relocation or not #### Experiment Design - test code First, we write down an absolute addressing code ```c .section .text .globl _start _start: la t0, global_var j . .section .data .globl global_var global_var: .word 0x12345678 ``` The assembler doesn't know the finla address of `global_var` when producing a relocatable object file, cause linker combine mutiple object file together. At the end, it will go through Relocation Table and fill in all absolute address. - Turn into a relocatable object file ``` bash= riscv64-unknown-elf-gcc -march=rv32i -mabi=ilp32 -c test.S -o test.o ``` - Disassemble and print relocation entries - `-d`: disassemble `.text` section in object file - `-r`: print relocation entries ```BASH= objdump -dr test.o ``` - validation Now we check whether the code generated for `la t0, global_var` contains relocation entries. ## Reference https://wiki.csie.ncku.edu.tw/arch/schedule