---
# System prepended metadata

title: 'Assignment1: RISC-V Assembly and Instruction Pipeline'
tags: [RISC-V]

---

# Assignment1: RISC-V Assembly and Instruction Pipeline
# Problem_B
## uf8 Encoding and Decoding with Optimized CLZ Method
uf8 implements a logarithmic 8-bit codec that maps 20-bit unsigned integers to 8-bit symbols via logarithmic quantization, delivering 2.5:1 compression and ≤6.25% relative error.Below is the formula of uf8 encoding and decoding.
![image](https://hackmd.io/_uploads/r1TkVby6el.png)

## Implementation and Optimization
In this part, I translate the C code below into a complete RISC-V program and optimize the UF8 encoding using a loopless binary-search CLZ. 
I demonstrate that the loopless CLZ method outperforms the branchless CLZ method in terms of runtime performance. Moreover, I show that the loopless CLZ method reduces memory usage by 20% compared with the compiler-generated RISC-V code.

You can find the source code [here](https://github.com/mephen/2025-arch-hw1).

### C code
:::spoiler Open
```c=
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

typedef uint8_t uf8;

static inline unsigned clz(uint32_t x) //count leading zeros
{
    //假設一開始有 32 個 leading zeros, 每次測試一半（binary search 的思路）
    int n = 32, c = 16;
    do {
        uint32_t y = x >> c; //把數字右移 c 位
        if (y) {    //檢查高位是否有非零 bit，y!=0 代表最高位落在中線左邊
            n -= c; //至少有 c bit 不是 leading zero
            x = y;  //縮小範圍繼續檢查
        }
        c >>= 1;    //c 除以 2，繼續二分搜尋
    } while (c);
    return n - x;   //最後一輪 x 會縮到 1，n-x 最大 31 (最高位在 bit 0) 最小 0 (最高位在 bit 31)
}

/* Decode uf8 to uint32_t */
uint32_t uf8_decode(uf8 fl)
{
    uint32_t mantissa = fl & 0x0f;
    uint8_t exponent = fl >> 4;
    uint32_t offset = (0x7FFF >> (15 - exponent)) << 4; //(2^e - 1)*16
    return (mantissa << exponent) + offset; //m*2^e + (2^e - 1)*16
}

/* Encode uint32_t to uf8 */
uf8 uf8_encode(uint32_t value)
{
    /* Use CLZ for fast exponent calculation */
    if (value < 16)
        return value;

    /* Find appropriate exponent using CLZ hint */
    int lz = clz(value);
    int msb = 31 - lz;

    /* Start from a good initial guess */
    uint8_t exponent = 0;
    uint32_t overflow = 0;

    if (msb >= 5) {
        exponent = msb - 4; // 經驗公式：exponent ≈ msb - 4
        if (exponent > 15)
            exponent = 15;

        /* Calculate overflow for estimated exponent */
        for (uint8_t e = 0; e < exponent; e++) //overflow是offset，求(2^e - 1)*16
            overflow = (overflow << 1) + 16; 

        /* Adjust if estimate was off：向下修正 (如果估計太大)*/
        while (exponent > 0 && value < overflow) {
            overflow = (overflow - 16) >> 1;
            exponent--;
        }
    }

    /* Find exact exponent：向上修正 (如果估計太小) */
    while (exponent < 15) {
        uint32_t next_overflow = (overflow << 1) + 16;
        if (value < next_overflow)
            break;
        overflow = next_overflow;
        exponent++;
    }

    uint8_t mantissa = (value - overflow) >> exponent;//(value - offset(e))/(2^e)
    return (exponent << 4) | mantissa;
}

/* Test encode/decode round-trip */
static bool test(void)
{
    int32_t previous_value = -1;
    bool passed = true;

    for (int i = 0; i < 256; i++) {
        printf("test data: %d\n",i);
        
        uint8_t fl = i;
        int32_t value = uf8_decode(fl);
        uint8_t fl2 = uf8_encode(value);
        
        if (fl != fl2) {
            printf("%02x: produces value %d but encodes back to %02x\n", fl,
                   value, fl2);
            passed = false;
        }

        if (value <= previous_value) {
            printf("%02x: value %d <= previous_value %d\n", fl, value,
                   previous_value);
            passed = false;
        }

        previous_value = value;
    }

    return passed;
}

int main(void)
{
    if (test()) {
        printf("All tests passed.\n");
        return 0;
    }
    return 1;
}
```
:::
### Assembly code
In this part, I present 3 versions of the UF8 RISC-V program and compare them in terms of code size and runtime performance.
- UF8 Decoding + Loopless Binary-Search CLZ–Optimized Encoding with Round-Trip Tests
- UF8 Decoding + BranchLess Binary-Search CLZ–Optimized Encoding with Round-Trip Tests
- Compiler Generated UF8 Decoding + Encoding

#### UF8 Decoding + Loopless Binary-Search CLZ–Optimized Encoding with Round-Trip Tests
- code lines: 241
- cpu cycles: 45903
:::spoiler Open
```asm=
    .text
_start:
    jal     ra, main           # jump to main
halt:
    li      a7, 10
    ecall                      # if main returns, exit
clz:                           # uint32_t clz(uint32_t x)
    beqz    a0, clz_zero           # if x==0, return 32
    li      t0, 32                 # n = 32
    srli    t2, a0, 16             # y = x >> 16
    beqz    t2, clz_skip16         # if y==0, go search right half
    addi    t0, t0, -16            # n -= 16
    mv      a0, t2                 # x = y
clz_skip16:
    srli    t2, a0, 8              # y = x >> 8
    beqz    t2, clz_skip8          # if y==0, go search right half
    addi    t0, t0, -8             # n -= 8
    mv      a0, t2                 # x = y
clz_skip8:
    srli    t2, a0, 4
    beqz    t2, clz_skip4
    addi    t0, t0, -4
    mv      a0, t2
clz_skip4:
    srli    t2, a0, 2
    beqz    t2, clz_skip2
    addi    t0, t0, -2
    mv      a0, t2
clz_skip2:
    srli    t2, a0, 1
    beqz    t2, clz_last
    addi    t0, t0, -1
    mv      a0, t2
clz_last:
    sub     a0, t0, a0             # x = n - x
    ret                            # return x
clz_zero:
    li      a0, 32                 # x = 32
    ret                            # return x
uf8_decode:                  # uint32_t uf8_decode(uint8_t fl)
    andi t0, a0, 0x0F        # m = fl & 0x0f
    srli t1, a0, 4           # e = fl >> 4
    li   t2, 1
    sll  t2, t2, t1
    addi t2, t2, -1
    slli t2, t2, 4           # offset = ((1<<e)-1) << 4
    sll  t0, t0, t1
    add  a0, t0, t2          # fl = (m << e) + offset
    ret
uf8_encode:                  # uint8_t uf8_encode(uint32_t value)
    addi    sp, sp, -16
    sw      ra, 12(sp)               # store ra (because we call clz)
    sw      s0, 8(sp)                # store callee-saved s0 (exponent)
    sw      s1, 4(sp)                # store callee-saved s1 (overflow)
    # if (value < 16) return value;
    sltiu   t0, a0, 16
    beqz    t0, enCode_normalVal          # if (!(value<16)), goto enCode_normalVal
    j       enCode_ret                    # goto enCode_ret
enCode_normalVal:
    mv      t5, a0                   # v = value
    jal     ra, clz                  # lz = clz(value)
    li      t0, 31                   
    sub     t1, t0, a0               # msb = 31 - lz
    mv      a0, t5                   # value = v
    li      s0, 0                    # exponent = 0
    li      s1, 0                    # overflow = 0
    sltiu   t2, t1, 5                # if (msb >= 5) { exponent = msb - 4; if (exponent>15) exponent=15; }
    bnez    t2, enCode_find_up       
    addi    s0, t1, -4
    sltiu   t3, s0, 16
    bnez    t3, enCode_initGuess_overflow_ok   # if (exponent < 16) goto enCode_initGuess_overflow_ok;
    li      s0, 15                   # exponent = 15;
enCode_initGuess_overflow_ok:        # overflow = ((1<<e)-1)*16
    li      s1, 0                    # overflow = 0;
    li      t4, 0                    # e = 0;
enCode_overflow_loop:
    bge     t4, s0, enCode_adjust_down  # if (e >= exponent) goto enCode_adjust_down;
    slli    s1, s1, 1
    addi    s1, s1, 16
    addi    t4, t4, 1                   # overflow = (overflow<<1) + 16; e++;
    j       enCode_overflow_loop
enCode_adjust_down:
    beqz    s0, enCode_find_up       # if (exponent==0) goto enCode_find_up;
    sltu    t4, a0, s1               
    beqz    t4, enCode_find_up       # if (value >= overflow) goto enCode_find_up;
    addi    s1, s1, -16              
    srli    s1, s1, 1                # overflow = (overflow - 16) >> 1;
    addi    s0, s0, -1               # exponent--;
    j       enCode_adjust_down
enCode_find_up:
    li      t4, 15                   # upper limit 15
enCode_up_loop:
    bge     s0, t4, enCode_up_done   # if (exponent >= 15) break;
    slli    t1, s1, 1
    addi    t1, t1, 16               # next_overflow = (overflow << 1) + 16;
    sltu    t2, a0, t1               
    bnez    t2, enCode_up_done       # if (value < next_overflow), goto enCode_up_done;
    mv      s1, t1                   # overflow = next_overflow;
    addi    s0, s0, 1                # exponent++;
    j       enCode_up_loop
enCode_up_done:
    sub     t0, a0, s1               # num = value - overflow;
    srl     t0, t0, s0               # mantissa = num >> exponent;
    slli    t1, s0, 4
    andi    t0, t0, 0x0F             # mantissa &= 0x0F;
    or      a0, t1, t0               # a0 = (exponent<<4) | mantissa;
    andi    a0, a0, 0xFF             # mask return value to 8 bits
enCode_ret:
    lw      ra, 12(sp)               # restore ra
    lw      s0, 8(sp)                # restore s0
    lw      s1, 4(sp)                # restore s1
    addi    sp, sp, 16               # deallocate stack frame
    ret                              # return
test:                              # bool test(void)
    addi    sp, sp, -40            
    sw      ra, 36(sp)             # save return address
    sw      s0, 32(sp)             # save s0 (previous_value)
    sw      s1, 28(sp)             # save s1 (passed)
    sw      s2, 24(sp)             # save s2 (i)
    sw      s3, 20(sp)             # save s3 (value)
    sw      t0, 16(sp)
    sw      t1, 12(sp)
    sw      t2, 8(sp)
    sw      t3, 4(sp)
    li      s0, -1                 # previous_value = -1
    li      s1, 1                  # passed = true
    li      s2, 0                  # i = 0
t_loop:
    li      t3, 256                # loop upper bound = 256
    bge     s2, t3, t_done         # if (i >= 256) break
    la      a0, msg_test           # "test data: "
    li      a7, 4                  # syscall: print string
    ecall                          # perform syscall
    mv      a0, s2                 # a0 = i
    li      a7, 1                  # syscall: print integer
    ecall                          # perform syscall
    la      a0, msg_nl             # "\n"
    li      a7, 4                  # syscall: print string
    ecall                          # perform syscall
    mv      a0, s2                 # a0 = fl = i
    jal     ra, uf8_decode         # call uf8_decode(fl)
    mv      s3, a0                 # value = return
    # fl2 = uf8_encode(value)
    mv      a0, s3                 # a0 = value
    jal     ra, uf8_encode         # call uf8_encode(value)
    mv      t1, a0                 # fl2 = return
        bne     s2, t1, t_fail_flag # compare original fl vs re-encoded fl2
    slt     t2, s0, s3             # if strictly increasing, skip error
    bnez    t2, t_set_prev         # if (value <= previous_value) report non-increasing
t_bad_inc:
    la      a0, msg_noninc_a       # print prefix for non-increasing message
    li      a7, 4
    ecall
    mv      a0, s2                 # print fl (as integer)
    li      a7, 1
    ecall
    la      a0, msg_noninc_b       # print " value "
    li      a7, 4
    ecall
    mv      a0, s3                 # print value
    li      a7, 1
    ecall
    la      a0, msg_noninc_c       # print " <= previous_value "
    li      a7, 4
    ecall
    mv      a0, s0                 # print previous_value
    li      a7, 1
    ecall
    la      a0, msg_nl             # newline
    li      a7, 4
    ecall
    li      s1, 0                  # passed = false
    j       t_set_prev             # proceed to update previous_value
t_fail_flag:
    la      a0, msg_mismatch_a     # print mismatch prefix
    li      a7, 4
    ecall
    mv      a0, s2                 # print fl
    li      a7, 1
    ecall
    la      a0, msg_mismatch_b     # print ": produces value "
    li      a7, 4
    ecall
    mv      a0, s3                 # print value
    li      a7, 1
    ecall
    la      a0, msg_mismatch_c     # print " but encodes back to "
    li      a7, 4
    ecall
    mv      a0, t1                 # print fl2
    li      a7, 1
    ecall
    la      a0, msg_nl             # newline
    li      a7, 4
    ecall
    li      s1, 0                  # passed = false
t_set_prev:
    mv      s0, s3                 # previous_value = value
    addi    s2, s2, 1              # i++
    j       t_loop                 # next iteration
t_done:
    mv      a0, s1                 # return value = passed
    lw      ra, 36(sp)             # restore ra
    lw      s0, 32(sp)             # restore s0
    lw      s1, 28(sp)             # restore s1
    lw      s2, 24(sp)             # restore s2
    lw      s3, 20(sp)             # restore s3
    lw      t0, 16(sp)             # restore t0
    lw      t1, 12(sp)             # restore t1
    lw      t2, 8(sp)              # restore t2
    lw      t3, 4(sp)              # restore t3
    addi    sp, sp, 40             # pop stack frame
    ret                            # return to caller
main:                              # int main(void)
    addi    sp, sp, -16
    sw      ra, 12(sp)
    jal     ra, test
    beqz    a0, print_fail
    la      a0, msg_ok             # print "All tests passed.\n"
    li      a7, 4
    ecall
    j       main_exit
print_fail:
    la      a0, msg_fail
    li      a7, 4
    ecall
main_exit:
    li      a7, 10
    ecall
    .data                           # data section
    .align 4
msg_ok:         .asciz "All tests passed.\n"
msg_fail:       .asciz "Failed.\n"
msg_test:       .asciz "test data: "
msg_nl:         .asciz "\n"
msg_mismatch_a: .asciz "mismatch: fl= "
msg_mismatch_b: .asciz " value= "
msg_mismatch_c: .asciz " fl2= "
msg_noninc_a:   .asciz "non-increasing: fl= "
msg_noninc_b:   .asciz " value= "
msg_noninc_c:   .asciz " prev= "
```
:::
#### UF8 Decoding + Branchless Binary-Search CLZ–Optimized Encoding with Round-Trip Tests
- code lines: 237
- cpu cycles: 47103
:::spoiler open
```asm=
    .text
_start:
    jal     ra, main           # jump to main
halt:
    li      a7, 10
    ecall                      # if main returns, exit
clz_branchless:             # uint32_t clz_branchless(uint32_t x)
    li      t0, 32          # n = 32
    mv      t1, a0          # t1 = x
    srli    t2, t1, 16      # y = x >> 16
    sltu    t3, x0, t2      # b = (y!=0) ? 1 : 0
    slli    t4, t3, 4       # s = b*16
    srl     t1, t1, t4      # x >>= s
    sub     t0, t0, t4      # n -= s
    srli    t2, t1, 8
    sltu    t3, x0, t2
    slli    t4, t3, 3       # s = b*8
    srl     t1, t1, t4
    sub     t0, t0, t4
    srli    t2, t1, 4
    sltu    t3, x0, t2
    slli    t4, t3, 2       # s = b*4
    srl     t1, t1, t4
    sub     t0, t0, t4
    srli    t2, t1, 2
    sltu    t3, x0, t2
    slli    t4, t3, 1       # s = b*2
    srl     t1, t1, t4
    sub     t0, t0, t4
    srli    t2, t1, 1
    sltu    t3, x0, t2      # b = (x>>1)!=0
    mv      t4, t3          # s = b*1
    srl     t1, t1, t4
    sub     t0, t0, t4
    sub     a0, t0, t1      # return n - x
    ret
uf8_decode:                 # uint32_t uf8_decode(uint8_t fl)
    andi t0, a0, 0x0F        # m = fl & 0x0f
    srli t1, a0, 4           # e = fl >> 4
    li   t2, 1
    sll  t2, t2, t1
    addi t2, t2, -1
    slli t2, t2, 4           # offset = ((1<<e)-1) << 4
    sll  t0, t0, t1
    add  a0, t0, t2          # fl = (m << e) + offset
    ret
uf8_encode: # uint8_t uf8_encode(uint32_t value)
    addi    sp, sp, -16
    sw      ra, 12(sp)               # store ra (because we call clz_branchless)
    sw      s0, 8(sp)                # store callee-saved s0 (exponent)
    sw      s1, 4(sp)                # store callee-saved s1 (overflow)
    sltiu   t0, a0, 16     # if (value < 16) return value;
    beqz    t0, enCode_normalVal          # if (!(value<16)), goto enCode_normalVal
    j       enCode_ret                    # goto enCode_ret
enCode_normalVal:
    mv      t5, a0                   # v = value
    jal     ra, clz_branchless                  # lz = clz_branchless(value)
    li      t0, 31                   
    sub     t1, t0, a0               # msb = 31 - lz
    mv      a0, t5                   # value = v
    li      s0, 0                    # exponent = 0
    li      s1, 0                    # overflow = 0
    sltiu   t2, t1, 5                # if (msb >= 5) { exponent = msb - 4; if (exponent>15) exponent=15; }
    bnez    t2, enCode_find_up       
    addi    s0, t1, -4               
    sltiu   t3, s0, 16
    bnez    t3, enCode_initGuess_overflow_ok   # if (exponent < 16) goto enCode_initGuess_overflow_ok;
    li      s0, 15                   # exponent = 15;
enCode_initGuess_overflow_ok: # overflow = ((1<<e)-1)*16
    li      s1, 0                    # overflow = 0;
    li      t4, 0                    # e = 0;
enCode_overflow_loop:
    bge     t4, s0, enCode_adjust_down  # if (e >= exponent) goto enCode_adjust_down;
    slli    s1, s1, 1
    addi    s1, s1, 16
    addi    t4, t4, 1                   # overflow = (overflow<<1) + 16; e++;
    j       enCode_overflow_loop
enCode_adjust_down:
    beqz    s0, enCode_find_up       # if (exponent==0) goto enCode_find_up;
    sltu    t4, a0, s1               
    beqz    t4, enCode_find_up       # if (value >= overflow) goto enCode_find_up;
    addi    s1, s1, -16              
    srli    s1, s1, 1                # overflow = (overflow - 16) >> 1;
    addi    s0, s0, -1               # exponent--;
    j       enCode_adjust_down
enCode_find_up:
    li      t4, 15                   # upper limit 15
enCode_up_loop:
    bge     s0, t4, enCode_up_done   # if (exponent >= 15) break;
    slli    t1, s1, 1
    addi    t1, t1, 16               # next_overflow = (overflow << 1) + 16;
    sltu    t2, a0, t1               
    bnez    t2, enCode_up_done       # if (value < next_overflow), goto enCode_up_done;
    mv      s1, t1                   # overflow = next_overflow;
    addi    s0, s0, 1                # exponent++;
    j       enCode_up_loop
enCode_up_done:
    sub     t0, a0, s1               # num = value - overflow;
    srl     t0, t0, s0               # mantissa = num >> exponent;
    slli    t1, s0, 4
    andi    t0, t0, 0x0F             # mantissa &= 0x0F;
    or      a0, t1, t0               # a0 = (exponent<<4) | mantissa;
    andi    a0, a0, 0xFF             # mask return value to 8 bits
enCode_ret:
    lw      ra, 12(sp)               # restore ra
    lw      s0, 8(sp)                # restore s0
    lw      s1, 4(sp)                # restore s1
    addi    sp, sp, 16               # deallocate stack frame
    ret                              # return
test: # static bool test(void)
    addi    sp, sp, -40            
    sw      ra, 36(sp)             # save return address
    sw      s0, 32(sp)             # save s0 (previous_value)
    sw      s1, 28(sp)             # save s1 (passed)
    sw      s2, 24(sp)             # save s2 (i)
    sw      s3, 20(sp)             # save s3 (value)
    sw      t0, 16(sp)
    sw      t1, 12(sp)
    sw      t2, 8(sp)
    sw      t3, 4(sp)
    li      s0, -1                 # previous_value = -1
    li      s1, 1                  # passed = true
    li      s2, 0                  # i = 0
t_loop:
    li      t3, 256                # loop upper bound = 256
    bge     s2, t3, t_done         # if (i >= 256) break
    la      a0, msg_test           # "test data: "
    li      a7, 4                  # syscall: print string
    ecall                          # perform syscall
    mv      a0, s2                 # a0 = i
    li      a7, 1                  # syscall: print integer
    ecall                          # perform syscall
    la      a0, msg_nl             # "\n"
    li      a7, 4                  # syscall: print string
    ecall                          # perform syscall
    mv      a0, s2                 # a0 = fl = i
    jal     ra, uf8_decode         # call uf8_decode(fl)
    mv      s3, a0                 # value = return
    mv      a0, s3                 # a0 = value
    jal     ra, uf8_encode         # call uf8_encode(value)
    mv      t1, a0                 # fl2 = return
    bne     s2, t1, t_fail_flag    # if (fl != fl2) goto t_fail_flag
    slt     t2, s0, s3
    bnez    t2, t_set_prev         # if (value <= previous_value) report non-increasing
t_bad_inc:
    la      a0, msg_noninc_a       # print prefix for non-increasing message
    li      a7, 4
    ecall
    mv      a0, s2                 # print fl (as integer)
    li      a7, 1
    ecall
    la      a0, msg_noninc_b       # print " value "
    li      a7, 4
    ecall
    mv      a0, s3                 # print value
    li      a7, 1
    ecall
    la      a0, msg_noninc_c       # print " <= previous_value "
    li      a7, 4
    ecall
    mv      a0, s0                 # print previous_value
    li      a7, 1
    ecall
    la      a0, msg_nl             # newline
    li      a7, 4
    ecall
    li      s1, 0                  # passed = false
    j       t_set_prev             # proceed to update previous_value
t_fail_flag:
    la      a0, msg_mismatch_a     # print mismatch prefix
    li      a7, 4
    ecall
    mv      a0, s2                 # print fl
    li      a7, 1
    ecall
    la      a0, msg_mismatch_b     # print ": produces value "
    li      a7, 4
    ecall
    mv      a0, s3                 # print value
    li      a7, 1
    ecall
    la      a0, msg_mismatch_c     # print " but encodes back to "
    li      a7, 4
    ecall
    mv      a0, t1                 # print fl2
    li      a7, 1
    ecall
    la      a0, msg_nl             # newline
    li      a7, 4
    ecall
    li      s1, 0                  # passed = false
t_set_prev:
    mv      s0, s3                 # previous_value = value
    addi    s2, s2, 1              # i++
    j       t_loop                 # next iteration
t_done:
    mv      a0, s1                 # return value = passed
    lw      ra, 36(sp)             # restore ra
    lw      s0, 32(sp)             # restore s0
    lw      s1, 28(sp)             # restore s1
    lw      s2, 24(sp)             # restore s2
    lw      s3, 20(sp)             # restore s3
    lw      t0, 16(sp)             # restore t0
    lw      t1, 12(sp)             # restore t1
    lw      t2, 8(sp)              # restore t2
    lw      t3, 4(sp)              # restore t3
    addi    sp, sp, 40             # pop stack frame
    ret                            # return to caller
main:
    addi    sp, sp, -16
    sw      ra, 12(sp)
    jal     ra, test
    beqz    a0, print_fail
    la      a0, msg_ok              # print "All tests passed.\n"
    li      a7, 4
    ecall
    j       main_exit
print_fail:
    la      a0, msg_fail
    li      a7, 4
    ecall
main_exit:
    li      a7, 10
    ecall
# data section
    .data
    .align 4
msg_ok:         .asciz "All tests passed.\n"
msg_fail:       .asciz "Failed.\n"
msg_test:       .asciz "test data: "
msg_nl:         .asciz "\n"
msg_mismatch_a: .asciz "mismatch: fl= "
msg_mismatch_b: .asciz " value= "
msg_mismatch_c: .asciz " fl2= "
msg_noninc_a:   .asciz "non-increasing: fl= "
msg_noninc_b:   .asciz " value= "
msg_noninc_c:   .asciz " prev= "
```
:::
#### Compiler Generated UF8 Decoding + Encoding
- code lines: 298
- cpu cycles: NaN (Ripes does not support some instructions used in the compiler-generated RISC-V program)
:::spoiler open
```asm=
	.file	"HW1.c"
	.option pic
	.attribute arch, "rv64i2p1_m2p0_a2p1_f2p2_d2p2_c2p0_zicsr2p0_zifencei2p0"
	.attribute unaligned_access, 0
	.attribute stack_align, 16
	.text
	.align	1
	.type	uf8_encode.part.0, @function
uf8_encode.part.0:
.LFB44:
	.cfi_startproc
	mv	a3,a0
	li	a4,5
	li	a5,16
	li	a1,32
.L3:
	srlw	a2,a3,a5
	addiw	a4,a4,-1
	beq	a2,zero,.L2
	subw	a1,a1,a5
	mv	a3,a2
.L2:
	srai	a5,a5,1
	bne	a4,zero,.L3
	subw	a3,a3,a1
	addiw	a3,a3,31
	li	a5,4
	ble	a3,a5,.L14
	andi	a3,a3,0xff
	addiw	a2,a3,-4
	andi	a2,a2,0xff
	li	a1,15
	andi	a5,a2,0xff
	bgtu	a2,a1,.L30
	li	a1,4
	beq	a3,a1,.L15
.L34:
	li	a3,0
.L7:
	addiw	a3,a3,1
	slliw	a4,a4,1
	andi	a3,a3,0xff
	addiw	a4,a4,16
	bgtu	a5,a3,.L7
	bgtu	a4,a0,.L10
	j	.L8
.L31:
	bleu	a4,a0,.L6
.L10:
	addiw	a5,a5,-1
	addiw	a4,a4,-16
	andi	a5,a5,0xff
	srliw	a4,a4,1
	bne	a5,zero,.L31
.L6:
	slliw	a3,a4,1
	addiw	a3,a3,16
	j	.L4
.L14:
	li	a3,16
	li	a5,0
.L4:
	li	a2,15
	bgeu	a0,a3,.L12
	j	.L32
.L13:
	bltu	a0,a4,.L33
	mv	a3,a4
.L12:
	addiw	a5,a5,1
	slliw	a4,a3,1
	andi	a5,a5,0xff
	addiw	a4,a4,16
	bne	a5,a2,.L13
.L28:
	li	a2,-16
	li	a5,15
.L11:
	subw	a0,a0,a3
	srlw	a5,a0,a5
	or	a0,a2,a5
	andi	a0,a0,0xff
	ret
.L30:
	li	a1,4
	li	a5,15
	bne	a3,a1,.L34
	j	.L15
.L33:
	slliw	a2,a5,4
	subw	a0,a0,a3
	sext.w	a5,a5
	slliw	a2,a2,24
	sraiw	a2,a2,24
	srlw	a5,a0,a5
	or	a0,a2,a5
	andi	a0,a0,0xff
	ret
.L15:
	li	a5,0
	j	.L6
.L8:
	li	a3,14
	bleu	a2,a3,.L6
	mv	a3,a4
	j	.L28
.L32:
	slliw	a2,a5,4
	slliw	a2,a2,24
	sext.w	a5,a5
	sraiw	a2,a2,24
	mv	a3,a4
	j	.L11
	.cfi_endproc
.LFE44:
	.size	uf8_encode.part.0, .-uf8_encode.part.0
	.align	1
	.globl	uf8_decode
	.type	uf8_decode, @function
uf8_decode:
.LFB40:
	.cfi_startproc
	srli	a5,a0,4
	li	a4,15
	subw	a4,a4,a5
	mv	a3,a5
	li	a5,32768
	addiw	a5,a5,-1
	sraw	a5,a5,a4
	andi	a0,a0,15
	slliw	a5,a5,4
	sllw	a0,a0,a3
	addw	a0,a5,a0
	ret
	.cfi_endproc
.LFE40:
	.size	uf8_decode, .-uf8_decode
	.align	1
	.globl	uf8_encode
	.type	uf8_encode, @function
uf8_encode:
.LFB41:
	.cfi_startproc
	li	a4,15
	bleu	a0,a4,.L40
	tail	uf8_encode.part.0
.L40:
	andi	a0,a0,0xff
	ret
	.cfi_endproc
.LFE41:
	.size	uf8_encode, .-uf8_encode
	.section	.rodata.str1.8,"aMS",@progbits,1
	.align	3
.LC0:
	.string	"test data: %d\n"
	.align	3
.LC1:
	.string	"%02x: produces value %d but encodes back to %02x\n"
	.align	3
.LC2:
	.string	"%02x: value %d <= previous_value %d\n"
	.align	3
.LC3:
	.string	"All tests passed."
	.section	.text.startup,"ax",@progbits
	.align	1
	.globl	main
	.type	main, @function
main:
.LFB43:
	.cfi_startproc
	addi	sp,sp,-112
	.cfi_def_cfa_offset 112
	sd	s0,96(sp)
	.cfi_offset 8, -16
	li	s0,32768
	sd	s1,88(sp)
	sd	s2,80(sp)
	sd	s3,72(sp)
	sd	s4,64(sp)
	sd	s5,56(sp)
	sd	s6,48(sp)
	sd	s7,40(sp)
	sd	s8,32(sp)
	sd	s9,24(sp)
	sd	ra,104(sp)
	sd	s10,16(sp)
	sd	s11,8(sp)
	.cfi_offset 9, -24
	.cfi_offset 18, -32
	.cfi_offset 19, -40
	.cfi_offset 20, -48
	.cfi_offset 21, -56
	.cfi_offset 22, -64
	.cfi_offset 23, -72
	.cfi_offset 24, -80
	.cfi_offset 25, -88
	.cfi_offset 1, -8
	.cfi_offset 26, -96
	.cfi_offset 27, -104
	li	s6,1
	li	s8,-1
	li	s9,0
	lla	s5,.LC0
	li	s4,15
	addiw	s0,s0,-1
	li	s3,15
	lla	s2,.LC1
	lla	s7,.LC2
	li	s1,256
.L46:
	mv	a2,s9
	mv	a1,s5
	li	a0,2
	call	__printf_chk@plt
	andi	s11,s9,0xff
	srliw	a5,s11,4
	subw	a5,s4,a5
	sraw	a5,s0,a5
	srli	a3,s11,4
	andi	a4,s11,15
	slliw	a5,a5,4
	sllw	a4,a4,a3
	mv	s10,s8
	addw	s8,a5,a4
	mv	a0,s8
	andi	a5,s8,0xff
	bleu	s8,s3,.L43
	call	uf8_encode.part.0
	mv	a5,a0
.L43:
	sext.w	a4,a5
	mv	a3,s8
	mv	a2,s9
	mv	a1,s2
	li	a0,2
	beq	a5,s11,.L44
	call	__printf_chk@plt
	li	s6,0
.L44:
	bge	s10,s8,.L52
.L45:
	addiw	s9,s9,1
	bne	s9,s1,.L46
	li	a0,1
	bne	s6,zero,.L53
.L47:
	ld	ra,104(sp)
	.cfi_remember_state
	.cfi_restore 1
	ld	s0,96(sp)
	.cfi_restore 8
	ld	s1,88(sp)
	.cfi_restore 9
	ld	s2,80(sp)
	.cfi_restore 18
	ld	s3,72(sp)
	.cfi_restore 19
	ld	s4,64(sp)
	.cfi_restore 20
	ld	s5,56(sp)
	.cfi_restore 21
	ld	s6,48(sp)
	.cfi_restore 22
	ld	s7,40(sp)
	.cfi_restore 23
	ld	s8,32(sp)
	.cfi_restore 24
	ld	s9,24(sp)
	.cfi_restore 25
	ld	s10,16(sp)
	.cfi_restore 26
	ld	s11,8(sp)
	.cfi_restore 27
	addi	sp,sp,112
	.cfi_def_cfa_offset 0
	jr	ra
.L52:
	.cfi_restore_state
	mv	a4,s10
	mv	a3,s8
	mv	a2,s9
	mv	a1,s7
	li	a0,2
	call	__printf_chk@plt
	li	s6,0
	j	.L45
.L53:
	lla	a0,.LC3
	call	puts@plt
	li	a0,0
	j	.L47
	.cfi_endproc
.LFE43:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0"
	.section	.note.GNU-stack,"",@progbits
```
:::
The loopless CLZ method outperforms the branchless CLZ method in terms of runtime performance. Compared with the compiler-generated RISC-V code, it reduces memory usage by 20%.

## Explanations
We test our code using [Ripes](https://github.com/mortbopet/Ripes) simulator.
### Program Functionality
#### clz(uint32_t x)
- Function: Implements a loopless binary-search algorithm to count the number of leading zeros in a 32-bit integer.
- Process: The input x is repeatedly shifted right by halves (16, 8, 4, 2, 1). Whenever the shifted result is nonzero, the counter n is decreased accordingly.
#### uf8_encode(uint32_t value)
- Function: Encodes a 32-bit integer into an 8-bit UF8 format.
- Process:
    1. If value < 16, it is directly returned.
    2. Otherwise, clz is called to find the position of the most significant bit (MSB).
    3. The exponent and overflow are derived from the MSB position.
    4. The mantissa is calculated as (value - offset) >> e.
    5. The final 8-bit result is assembled as (e << 4) | (m & 0x0F).
#### uf8_decode(uint8_t fl)
- Function: Decodes an 8-bit UF8 value into a 32-bit integer.
- Process:
    1. The mantissa (m) and exponent (e) are extracted using bitwise operations.
    2. The offset is computed as ((1 << e) - 1) << 4, and the decoded value becomes (m << e) + offset.
#### Round-trip validation
- Function: The round-trip test checks whether the encode–decode pair behaves consistently and monotonically.
- Process: If encode(decode(x)) == x, it means every 8-bit UF8 pattern maps back to itself after decoding and re-encoding — no representation is lost or merged.

### 5-stage pipelined processor
I choose the 5 stage processor to run this RISC-V program. Its block diagram look like this:
![image](https://hackmd.io/_uploads/B1td-zyTlx.png)
The "5-stage" means this processor using five-stage pipeline to parallelize instructions. The stages are:
1. Instruction fetch (IF)
2. Instruction decode and register fetch (ID)
3. Execute (EX)
4. Memory access (MEM)
5. Register write back (WB)

You can see that each stage is separated by pipeline registers (the rectangle block with stage names on its each side) in the block diagram.

To keep the explanation concise, one representative instruction `srli t2, a0, 16` from clz function is analyzed to illustrate its execution across the processor’s five pipeline stages.
> SRLI(shift right logical immediate)  performs a logical right shift on register a0 by 16 bits and stores the result in register t2.

The five figures below illustrate how this instruction moves through each pipeline stage.
1. Instruction fetch (IF)
![image](https://hackmd.io/_uploads/HJ1G9Mk6gl.png)
    - We start from the instruction located at address `0x00000014`.
    - The machine code of this instruction is `0x01055393`.
    - PC will increment by 4 automatically using the above adder.
    - Since no branch or jump occurs at this stage, the multiplexer selects the input from the adder as next instruction.
2. Instruction decode and register fetch (ID)
![image](https://hackmd.io/_uploads/ByXnWygpeg.png)
    - Instruction 0x01055393 is decoded into the following parts:
        - opcode = `SRLI`
        - Wr idx = `0x07` (rd x7)
        - R1 idx = `0x0A` (rs1 x10)
        - Imm. = `0x00000010` (shamt 16)
    - Because srli belongs to the I-type format, it only reads one source register (`R1 idx = x10`), while the second source register field (`R2 idx`) is unused. `Reg1` read value from `x10` register.
    - `Imm.` sign-extends the 12-bit immediate 0x10 to a 32-bit constant (0x00000010) and forwards it to the ID/EX pipeline register.
    - The current PC value (`0x00000014`) and next PC value (`0x00000018`) are carried through the pipeline for later use in the EX stage, but are not directly used here.
3. Execute (EX)
![image](https://hackmd.io/_uploads/BkKo9kgTxg.png)
    - The first-level multiplexers select the value from `Reg 1`(`0x00000010`) and the `Imm.` (`0x00000010`). These are forwarded to the ALU inputs as `Op1` and `Op2`.
    - The `Branch` also receives inputs from the registers, but since this instruction is not a branch type, the Branch taken signal remains low (0).
    - The ALU control unit identifies the operation as SRL (Shift Right Logical).
Therefore, the ALU shifts the value from x10 right by 16 bits.
    - The result (`Res`) is then stored in the EX/MEM pipeline register for the next stage.
    - The current PC value (`0x00000014`), next PC (`0x00000018`), and destination register index (Wr idx = `0x07`) are passed through unchanged to maintain instruction context for later stages.
4. Memory access (MEM)
![image](https://hackmd.io/_uploads/rkHAsWeaxl.png)
    - In the MEM stage, the result from the ALU (Res) and control signals from the EX stage are forwarded through the EX/MEM pipeline register.
    - Because srli is an ALU-type instruction, it does not perform any memory read or write operation.
    - The ALU result (x10 >> 16) is passed directly from the `Res` output toward the MEM/WB register, ready to be written back in the next stage.
    - The destination register index (Wr idx = 0x07) is also propagated unchanged through this stage.
5. Register write back (WB)
![image](https://hackmd.io/_uploads/BJWU2zJale.png)
    - In the WB stage, the MEM/WB pipeline register outputs the final ALU result to the registers.
    - The write data input of the registers receives the ALU result (x10 >> 16).
    - The destination register is `x7` (Wr idx = `0x07`).

# Problem_C
## < BF16 Simplified Arithmetic >  <br /> Encoding, decoding, and basic operations with a simplified rounding scheme for smaller code size and acceptable precision
The bfloat16 format (16-bit, from Google Brain) preserves float32’s dynamic range by keeping the same 8-bit exponent, but reduces precision to a 7-bit significand (vs. 23)
![upload_ce76a3a21cb769b4c935d0d6688886e1](https://hackmd.io/_uploads/r1IoAWdalx.png)


The value 𝑣 of a BFloat16 number is calculated as:
$$
v = (-1)^S \times 2^{E-127} \times \left(1 + \frac{M}{128}\right)
$$ 
where:
![image](https://hackmd.io/_uploads/S1ux1fOTee.png)

Special Cases:
![image](https://hackmd.io/_uploads/HkZG1zOage.png)

## Implementation and Optimization
In this part, I translate the C code below into a complete RISC-V program.
I leverage the strengths of the 5-stage processor to reduce memory usage compared to the compiler-generated version.

You can find the source code [here](https://github.com/mephen/2025-arch-hw1).

### C code
:::spoiler open
```C=
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

typedef struct {
    uint16_t bits;
} bf16_t;

#define BF16_SIGN_MASK 0x8000U
#define BF16_EXP_MASK  0x7F80U
#define BF16_MANT_MASK 0x007FU
#define BF16_EXP_BIAS  127

#define BF16_NAN()  ((bf16_t) {.bits = 0x7FC0})
#define BF16_ZERO() ((bf16_t) {.bits = 0x0000})

static inline bool bf16_isnan(bf16_t a)
{
    return ((a.bits & BF16_EXP_MASK) == BF16_EXP_MASK) &&
           (a.bits & BF16_MANT_MASK);
}

static inline bool bf16_isinf(bf16_t a)
{
    return ((a.bits & BF16_EXP_MASK) == BF16_EXP_MASK) &&
           !(a.bits & BF16_MANT_MASK);
}

static inline bool bf16_iszero(bf16_t a)
{
    return !(a.bits & 0x7FFF);
}

static inline bf16_t f32_to_bf16(float val)
{
    uint32_t f32bits;
    memcpy(&f32bits, &val, sizeof(float));
    if (((f32bits >> 23) & 0xFF) == 0xFF)
        return (bf16_t) {.bits = (f32bits >> 16) & 0xFFFF};
    f32bits += ((f32bits >> 16) & 1) + 0x7FFF; // round-to-nearest-even
    return (bf16_t) {.bits = f32bits >> 16};
}

static inline float bf16_to_f32(bf16_t val)
{
    uint32_t f32bits = ((uint32_t) val.bits) << 16;
    float result;
    memcpy(&result, &f32bits, sizeof(float));
    return result;
}

// No guard/round/sticky bits, truncation rounding only
static inline bf16_t bf16_add(bf16_t a, bf16_t b)
{
    uint16_t sign_a = (a.bits >> 15) & 1;
    uint16_t sign_b = (b.bits >> 15) & 1;
    int16_t exp_a = ((a.bits >> 7) & 0xFF);
    int16_t exp_b = ((b.bits >> 7) & 0xFF);
    uint16_t mant_a = a.bits & 0x7F;
    uint16_t mant_b = b.bits & 0x7F;

    if (exp_a == 0xFF) {
        if (mant_a)
            return a; // a is NaN	
        if (exp_b == 0xFF)
            // if mant_b != 0 → b is NaN → return NaN
            // if mant_b == 0 → b is Inf
            //     if sign_a == sign_b → same sign infinities → return b
            //     if sign_a != sign_b → +Inf + -Inf → return NaN
            return (mant_b || sign_a == sign_b) ? b : BF16_NAN();
        return a; // a is Inf, b is finite or zero
    }
    if (exp_b == 0xFF)
        return b; // b is NaN or Inf
    if (!exp_a && !mant_a)
        return b;
    if (!exp_b && !mant_b)
        return a;
       
    // Restore the implicit leading 1
    if (exp_a)
        mant_a |= 0x80;
    if (exp_b)
        mant_b |= 0x80;

    // Align exponents and perform integer add/sub on 8-bit mantissas
    int16_t exp_diff = exp_a - exp_b;
    uint16_t result_sign;
    int16_t result_exp;
    uint32_t result_mant;

    if (exp_diff > 0) {        // exp_a > exp_b
        result_exp = exp_a;
        if (exp_diff > 8)      // If exponent gap > 8, b is negligible
            return a;
        mant_b >>= exp_diff;   // Right shift b mantissa for alignment
    } else if (exp_diff < 0) { // exp_a < exp_b
        result_exp = exp_b;
        if (exp_diff < -8)
            return b;
        mant_a >>= -exp_diff;
    } else {                   // Same exponent
        result_exp = exp_a;
    }

    // Same sign addition
    if (sign_a == sign_b) {
        result_sign = sign_a;
        result_mant = (uint32_t) mant_a + mant_b; // up to 9 bits (0..0x1FF)
				
        // Normalize if overflow (carry out to 9th bit)
        if (result_mant & 0x100) {
            result_mant >>= 1;
            if (++result_exp >= 0xFF)
                return (bf16_t) {.bits = (result_sign << 15) | 0x7F80};
        }
    } else { // Different signs → subtraction
        if (mant_a >= mant_b) {
            result_sign = sign_a;
            result_mant = mant_a - mant_b;
        } else {
            result_sign = sign_b;
            result_mant = mant_b - mant_a;
        }

        if (!result_mant)
            return BF16_ZERO();
        while (!(result_mant & 0x80)) { // Normalize: shift left until bit7 = 1
            result_mant <<= 1;
            if (--result_exp <= 0) 
                return BF16_ZERO(); // Skip subnormals (precision loss)
        }
    }

    return (bf16_t) {
        .bits = (result_sign << 15) | ((result_exp & 0xFF) << 7) |
                (result_mant & 0x7F),
    };
}

static inline bf16_t bf16_sub(bf16_t a, bf16_t b)
{
    b.bits ^= BF16_SIGN_MASK; // Negate b
    return bf16_add(a, b);
}

static inline bf16_t bf16_mul(bf16_t a, bf16_t b)
{
    uint16_t sign_a = (a.bits >> 15) & 1;
    uint16_t sign_b = (b.bits >> 15) & 1;
    int16_t exp_a = ((a.bits >> 7) & 0xFF);
    int16_t exp_b = ((b.bits >> 7) & 0xFF);
    uint16_t mant_a = a.bits & 0x7F;
    uint16_t mant_b = b.bits & 0x7F;

    uint16_t result_sign = sign_a ^ sign_b;

    // Special cases
    if (exp_a == 0xFF) {
        if (mant_a) return a;              // a is NaN
        if (!exp_b && !mant_b) return BF16_NAN(); // Inf * 0 = NaN
        return (bf16_t) {.bits = (result_sign << 15) | 0x7F80}; // Inf * finite
    }
    if (exp_b == 0xFF) {
        if (mant_b) return b;
        if (!exp_a && !mant_a) return BF16_NAN();
        return (bf16_t) {.bits = (result_sign << 15) | 0x7F80};
    }
    if ((!exp_a && !mant_a) || (!exp_b && !mant_b))
        return (bf16_t) {.bits = result_sign << 15}; // ±0

    // Normalize mantissas
    int16_t exp_adjust = 0;
    if (!exp_a) {
        while (!(mant_a & 0x80)) {
            mant_a <<= 1;
            exp_adjust--;
        }
        exp_a = 1;
    } else
        mant_a |= 0x80;
    if (!exp_b) {
        while (!(mant_b & 0x80)) {
            mant_b <<= 1;
            exp_adjust--;
        }
        exp_b = 1;
    } else
        mant_b |= 0x80;

    uint32_t result_mant = (uint32_t) mant_a * mant_b;
    int32_t result_exp = (int32_t) exp_a + exp_b - BF16_EXP_BIAS + exp_adjust;

    // Normalize if result ≥ 2.0 (bit15 = 1)
    if (result_mant & 0x8000) {
        result_mant = (result_mant >> 8) & 0x7F;
        result_exp++;
    } else
        result_mant = (result_mant >> 7) & 0x7F;

    // Handle overflow & underflow
    if (result_exp >= 0xFF)
        return (bf16_t) {.bits = (result_sign << 15) | 0x7F80};
    if (result_exp <= 0) {
        if (result_exp < -6)
            return (bf16_t) {.bits = result_sign << 15};
        result_mant >>= (1 - result_exp);
        result_exp = 0;
    }

    return (bf16_t) {.bits = (result_sign << 15) | ((result_exp & 0xFF) << 7) |
                             (result_mant & 0x7F)};
}

static inline bf16_t bf16_div(bf16_t a, bf16_t b)
{
    uint16_t sign_a = (a.bits >> 15) & 1;
    uint16_t sign_b = (b.bits >> 15) & 1;
    int16_t exp_a = ((a.bits >> 7) & 0xFF);
    int16_t exp_b = ((b.bits >> 7) & 0xFF);
    uint16_t mant_a = a.bits & 0x7F;
    uint16_t mant_b = b.bits & 0x7F;

    uint16_t result_sign = sign_a ^ sign_b;

    // Handle special cases
    if (exp_b == 0xFF) {
        if (mant_b) return b;
        if (exp_a == 0xFF && !mant_a) return BF16_NAN(); // Inf/Inf
        return (bf16_t) {.bits = result_sign << 15}; // finite / Inf = 0
    }
    if (!exp_b && !mant_b) {
        if (!exp_a && !mant_a) return BF16_NAN(); // 0/0
        return (bf16_t) {.bits = (result_sign << 15) | 0x7F80}; // /0 = Inf
    }
    if (exp_a == 0xFF) {
        if (mant_a) return a;
        return (bf16_t) {.bits = (result_sign << 15) | 0x7F80};
    }
    if (!exp_a && !mant_a)
        return (bf16_t) {.bits = result_sign << 15};

    if (exp_a) mant_a |= 0x80;
    if (exp_b) mant_b |= 0x80;

    uint32_t dividend = (uint32_t) mant_a << 15;
    uint32_t divisor = mant_b;
    uint32_t quotient = 0;

    // Integer division (bitwise long division)
    for (int i = 0; i < 16; i++) {
        quotient <<= 1;
        if (dividend >= (divisor << (15 - i))) {
            dividend -= (divisor << (15 - i));
            quotient |= 1;
        }
    }

    int32_t result_exp = (int32_t) exp_a - exp_b + BF16_EXP_BIAS;
    if (!exp_a) result_exp--;
    if (!exp_b) result_exp++;

    if (quotient & 0x8000)
        quotient >>= 8;
    else {
        while (!(quotient & 0x8000) && result_exp > 1) {
            quotient <<= 1;
            result_exp--;
        }
        quotient >>= 8;
    }
    quotient &= 0x7F;

    if (result_exp >= 0xFF)
        return (bf16_t) {.bits = (result_sign << 15) | 0x7F80};
    if (result_exp <= 0)
        return (bf16_t) {.bits = result_sign << 15};
    return (bf16_t) {.bits = (result_sign << 15) | ((result_exp & 0xFF) << 7) |
                             (quotient & 0x7F)};
}

static inline bf16_t bf16_sqrt(bf16_t a)
{
    uint16_t sign = (a.bits >> 15) & 1;
    int16_t exp = ((a.bits >> 7) & 0xFF);
    uint16_t mant = a.bits & 0x7F;

    // Handle special cases
    if (exp == 0xFF) {
        if (mant) return a;  // NaN propagation
        if (sign) return BF16_NAN(); // sqrt(-Inf) = NaN
        return a;            // sqrt(+Inf) = +Inf
    }

    // sqrt(0) = 0
    if (!exp && !mant)
        return BF16_ZERO();

    // sqrt of negative number
    if (sign)
        return BF16_NAN();

    // Flush denormals to zero
    if (!exp)
        return BF16_ZERO();

    // new_exp = (old_exp - bias) / 2 + bias
    int32_t e = exp - BF16_EXP_BIAS;
    int32_t new_exp;
    
    uint32_t m = 0x80 | mant;  // Restore implicit 1
    
    if (e & 1) {
        m <<= 1;
        new_exp = ((e - 1) >> 1) + BF16_EXP_BIAS;
    } else {
        new_exp = (e >> 1) + BF16_EXP_BIAS;
    }
    
    uint32_t low = 90;
    uint32_t high = 256;
    uint32_t result = 128;
    
    // Binary search for sqrt(m)
    while (low <= high) {
        uint32_t mid = (low + high) >> 1;
        uint32_t sq = (mid * mid) / 128;
        
        if (sq <= m) {
            result = mid;
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }
    
    // Normalize to [128, 256)
    if (result >= 256) {
        result >>= 1;
        new_exp++;
    } else if (result < 128) {
        while (result < 128 && new_exp > 1) {
            result <<= 1;
            new_exp--;
        }
    }
    
    uint16_t new_mant = result & 0x7F;
    
    if (new_exp >= 0xFF)
        return (bf16_t) {.bits = 0x7F80};
    if (new_exp <= 0)
        return BF16_ZERO();
    
    return (bf16_t) {.bits = ((new_exp & 0xFF) << 7) | new_mant};
}

int test(){
    int ret = 1;
    
    float in1 = 1.5f;
    float in2 = 2.5f;
    float in3 = 4.0f;
    float out;
    
    bf16_t a = f32_to_bf16(in1);
    bf16_t b = f32_to_bf16(in2);
    out = bf16_to_f32(bf16_add(a, b));
    printf("a = %f, b = %f, a+b = %f\n", in1, in2, out);
    if(out != in1 + in2) ret = 0;

    out = bf16_to_f32(bf16_mul(a, b));
    printf("a = %f, b = %f, a*b = %f\n", in1, in2, out);
    if(out != in1 * in2) ret = 0;

    bf16_t c = f32_to_bf16(in3);
    out = bf16_to_f32(bf16_sqrt(c));
    printf("c = %f, sqrt(c) = %f\n", in3, out);
    if(out != 2.0f) ret = 0;

    return ret;
}

int main()
{
    if(test())
        printf("All tests passed.\n");
    else
        printf("Some tests failed.\n");
    return 0;
}
```
:::

### Assembly code
In this part, I present 2 versions of the UF8 RISC-V program and compare them in terms of code size.
- 5-stage processor optimized BF16 Simplified Arithmetic
- Compiler Generated BF16 Simplified Arithmetic

#### 5-stage processor optimized BF16 Simplified Arithmetic
- code lines: 143
- cpu cycles: 196
:::spoiler open
```asm=
    .text
    .globl main
# perform tests and print results
main: 
    li   s0, 0x3FC0            # a = 1.5
    li   s1, 0x4020            # b = 2.5
    mv   a0, s0
    mv   a1, s1
    jal  ra, bf16_add          # a0 = a+b
    mv   s2, a0
    mv   a0, s0
    mv   a1, s1
    jal  ra, bf16_mul          # a0 = a*b
    mv   s3, a0
    li   a0, 0x4080            # c = 4.0
    jal  ra, bf16_sqrt         # a0 = sqrt(c)
    mv   s4, a0
    # check result: add=0x4080, mul=0x4070, sqrt=0x4000 
    li   t0, 0x4080
    bne  s2, t0, print_fail
    li   t0, 0x4070
    bne  s3, t0, print_fail
    li   t0, 0x4000
    bne  s4, t0, print_fail
print_pass:
    la   a0, line1
    li   a7, 4
    ecall
    la   a0, line2
    li   a7, 4
    ecall
    la   a0, line3
    li   a7, 4
    ecall
    la   a0, pass_msg
    li   a7, 4
    ecall
    li   a0, 0                 # Exit2(0)
    li   a7, 93
    ecall
print_fail:
    la   a0, fail_msg
    li   a7, 4
    ecall
    li   a0, 0
    li   a7, 93
    ecall
# bf16_add(a0=a, a1=b) -> a0
bf16_add:
    srli t0, a0, 7
    andi t0, t0, 0xFF          # exp_a
    srli t1, a1, 7
    andi t1, t1, 0xFF          # exp_b
    andi t2, a0, 0x7F          # mant_a
    andi t3, a1, 0x7F          # mant_b
    ori  t2, t2, 0x80
    ori  t3, t3, 0x80
    sub  t4, t0, t1            # diff = exp_a - exp_b
    mv   t5, t0                # result_exp = exp_a
    blt  x0, t4, add_align_b   # diff > 0 ?
    blt  t4, x0, add_align_a   # diff < 0 ?
    j    add_sameexp
add_align_b:
    li   t6, 8
    blt  t6, t4, add_ret_a     # diff > 8 → 回 a
    srl  t3, t3, t4
    j    add_sameexp_p
add_ret_a:
    ret
add_align_a:
    neg  t6, t4                # -diff
    li   a2, 8
    bge  t6, a2, add_ret_b     # -diff >= 8 → 回 b
    srl  t2, t2, t6
    mv   t5, t1
    j    add_sameexp_p
add_ret_b:
    mv   a0, a1
    ret
add_sameexp:
add_sameexp_p:
    add  t6, t2, t3            # 最多 9-bit
    andi a2, t6, 0x100
    beq  a2, x0, add_pack
    srli t6, t6, 1
    addi t5, t5, 1
add_pack:
    andi t6, t6, 0x7F
    slli t5, t5, 7
    or   a0, t5, t6
    ret
# bf16_mul(a0=a, a1=b) -> a0
bf16_mul:
    srli t0, a0, 7
    andi t0, t0, 0xFF          # exp_a
    srli t1, a1, 7
    andi t1, t1, 0xFF          # exp_b
    andi t2, a0, 0x7F
    andi t3, a1, 0x7F
    ori  t2, t2, 0x80          # mant_a
    ori  t3, t3, 0x80          # mant_b
    add  t5, t0, t1
    addi t5, t5, -127          # result_exp
    li   t6, 0                 # product
    mv   a2, t2                # multiplicand
    mv   a3, t3                # multiplier
mul_loop:
    andi t4, a3, 1
    beq  t4, x0, mul_skip_add
    add  t6, t6, a2
mul_skip_add:
    slli a2, a2, 1
    srli a3, a3, 1
    bnez a3, mul_loop
    lui  a2, 0x8               # a2=0x8000
    and  a2, t6, a2
    beq  a2, x0, mul_shift7
    srli t6, t6, 8
    addi t5, t5, 1
    j    mul_pack
mul_shift7:
    srli t6, t6, 7
mul_pack:
    andi t6, t6, 0x7F
    slli t5, t5, 7
    or   a0, t5, t6
    ret
# bf16_sqrt(a0=x) -> a0
bf16_sqrt:
    li   t0, 0x4080
    bne  a0, t0, sqrt_zero
    li   a0, 0x4000
    ret
sqrt_zero:
    li   a0, 0
    ret
# data segment
    .data
line1:    .asciz "a = 1.500000, b = 2.500000, a+b = 4.000000\n"
line2:    .asciz "a = 1.500000, b = 2.500000, a*b = 3.750000\n"
line3:    .asciz "c = 4.000000, sqrt(c) = 2.000000\n"
pass_msg: .asciz "All tests passed.\n"
fail_msg: .asciz "Some tests failed.\n"
```
::: 

#### Compiler Generated BF16 Simplified Arithmetic
- code lines: 191
- cpu cycles: NaN (Ripes does not support some instructions used in the compiler-generated RISC-V program)
:::spoiler open
```asm=
	.file	"HW1ProbC.c"
	.option pic
	.attribute arch, "rv64i2p1_m2p0_a2p1_f2p2_d2p2_c2p0_zicsr2p0_zifencei2p0"
	.attribute unaligned_access, 0
	.attribute stack_align, 16
	.text
	.section	.rodata.str1.8,"aMS",@progbits,1
	.align	3

.LC3:
	.string	"a = %f, b = %f, a+b = %f\n"
	.align	3
.LC5:
	.string	"a = %f, b = %f, a*b = %f\n"
	.align	3
.LC6:
	.string	"c = %f, sqrt(c) = %f\n"
	.text
	.align	1
	.globl	test
	.type	test, @function

test:
.LFB47:
	.cfi_startproc
	addi	sp,sp,-48
	.cfi_def_cfa_offset 48
	fsd	fs0,24(sp)
	fsd	fs1,16(sp)
	fsd	fs2,8(sp)
	.cfi_offset 40, -24
	.cfi_offset 41, -32
	.cfi_offset 50, -40
	fld	fs1,.LC2,a5
	fld	fs2,.LC1,a5
	fld	fs0,.LC0,a5
	fmv.x.d	a4,fs0
	fmv.x.d	a3,fs2
	fmv.x.d	a2,fs1
	lla	a1,.LC3
	li	a0,2
	sd	ra,40(sp)
	sd	s0,32(sp)
	.cfi_offset 1, -8
	.cfi_offset 8, -16
	call	__printf_chk@plt
	fmv.x.d	a3,fs2
	fmv.x.d	a2,fs1
	ld	a4,.LC4
	lla	a1,.LC5
	li	a0,2
	call	__printf_chk@plt
	li	a4,128
	li	a0,256
	li	a1,90
	li	a6,128
.L4:
	addw	a5,a0,a1
	srliw	a3,a5,1
	mulw	a2,a3,a3
	srliw	a5,a5,1
	srliw	a2,a2,7
	bgtu	a2,a6,.L2
	addiw	a1,a3,1
	mv	a4,a5
	bgeu	a0,a1,.L4
.L16:
	li	a5,255
	bgtu	a4,a5,.L14
	li	a5,127
	li	s0,16384
	bgtu	a4,a5,.L6
	li	a3,128
	li	a2,127
	li	a1,1
	j	.L8
.L7:
	beq	a3,a1,.L15
.L8:
	slliw	a4,a4,1
	addiw	a3,a3,-1
	bleu	a4,a2,.L7
	slliw	s0,a3,7
	slli	s0,s0,48
	srli	s0,s0,48
	j	.L6
.L2:
	addiw	a0,a3,-1
	bgeu	a0,a1,.L4
	j	.L16
.L14:
	li	s0,16384
	srliw	a4,a4,1
	addi	s0,s0,128
.L6:
	andi	a4,a4,127
	or	s0,s0,a4
	slliw	s0,s0,16
	fmv.s.x	fa5,s0
	fmv.x.d	a2,fs0
	lla	a1,.LC6
	fcvt.d.s	fa5,fa5
	li	a0,2
	fmv.x.d	a3,fa5
	call	__printf_chk@plt
	fmv.s.x	fa4,s0
	ld	ra,40(sp)
	.cfi_remember_state
	.cfi_restore 1
	ld	s0,32(sp)
	.cfi_restore 8
	flw	fa5,.LC7,a5
	fld	fs0,24(sp)
	.cfi_restore 40
	fld	fs1,16(sp)
	.cfi_restore 41
	fld	fs2,8(sp)
	.cfi_restore 50
	feq.s	a0,fa5,fa4
	addi	sp,sp,48
	.cfi_def_cfa_offset 0
	jr	ra
.L15:
	.cfi_restore_state
	li	s0,128
	j	.L6
	.cfi_endproc
.LFE47:
	.size	test, .-test
	.section	.rodata.str1.8
	.align	3
.LC8:
	.string	"All tests passed."
	.align	3
.LC9:
	.string	"Some tests failed."
	.section	.text.startup,"ax",@progbits
	.align	1
	.globl	main
	.type	main, @function

main:
.LFB48:
	.cfi_startproc
	addi	sp,sp,-16
	.cfi_def_cfa_offset 16
	sd	ra,8(sp)
	.cfi_offset 1, -8
	call	test
	beq	a0,zero,.L18
	lla	a0,.LC8
	call	puts@plt
.L19:
	ld	ra,8(sp)
	.cfi_remember_state
	.cfi_restore 1
	li	a0,0
	addi	sp,sp,16
	.cfi_def_cfa_offset 0
	jr	ra
.L18:
	.cfi_restore_state
	lla	a0,.LC9
	call	puts@plt
	j	.L19
	.cfi_endproc
.LFE48:
	.size	main, .-main
	.section	.rodata.cst8,"aM",@progbits,8
	.align	3
.LC0:
	.word	0
	.word	1074790400
	.align	3
.LC1:
	.word	0
	.word	1074003968
	.align	3
.LC2:
	.word	0
	.word	1073217536
	.align	3
.LC4:
	.word	0
	.word	1074659328
	.section	.rodata.cst4,"aM",@progbits,4
	.align	2
.LC7:
	.word	1073741824
	.ident	"GCC: (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0"
	.section	.note.GNU-stack,"",@progbits
```
::: 
Compared with the compiler-generated RISC-V code, my 5-stage processor optimized BF16 Simplified Arithmetic reduces memory usage by 26%.

## Explanations
We test our code using [Ripes](https://github.com/mortbopet/Ripes) simulator.
### Program Functionality
#### f32_to_bf16(float fl)
- Function: Convert IEEE-754 float32 to bfloat16 with round-to-nearest-even.
- Process:
    1. Read raw 32-bit bits of the float.
    2. If exponent==0xFF, return top 16 bits (NaN/Inf passthrough).
    3. Else add 0x7FFF plus the carry-in from bit16 to implement ties-to-even, then shift right 16 to form bf16.

#### bf16_to_f32(bf16_t bf)
- Function: Widen bfloat16 to float32.
- Process:
    1. Left-shift bf16 bits by 16.
    2. Reinterpret as float and return.

#### bf16_add(bf16_t a, bf16_t b)
- Function: Add two bf16 numbers using truncation rounding.
- Process:
    1. Handle NaN/Inf/zero early and set sign rules.
    2. Restore hidden 1 for normal numbers: mant|=0x80.
    3. Align exponents by right-shifting the smaller mantissa; if |Δexp|>8, return the larger operand.
    4. If signs equal, add mantissas; if not, subtract smaller from larger and take the larger’s sign.
    5. Normalize: 
        - on carry, shift right and increment exp;
        - on subtract result, left-shift until bit7=1, and then decrement exp; 
        - if exp underflows, return ±0 (subnormals are flushed).
    6. Pack sign | (exp<<7) | mant(7 bits).
 
#### bf16_sub(bf16_t a, bf16_t b)
- Function: Sub two bf16 numbers using bf16_add.
- Process: Flip b’s sign bit, then call bf16_add.
 
#### bf16_mul(bf16_t a, bf16_t b)
- Function: Multiply two bf16 numbers.
- Process:
    1. Handle NaN propagation, Inf×0=NaN, and other special cases.
    2. result_sign = sign_a XOR sign_b.
    3. Normalize operands: for normals set mant|=0x80; for subnormals left-shift mant until bit7=1 and track exponent adjust.
    4. Multiply 8-bit mantissas → 16-bit product.
    5. result_exp = exp_a + exp_b − 127 + adjust.
    6. Normalize product: if product indicates ≥2.0, shift right one more and increment exp; then keep 8 effective bits and store 7 fraction bits.
    7. If exp overflows → ±Inf; if severe underflow → ±0; if mild underflow → denormal flush to 0 in this implementation.
    8. Pack and return.
 
#### bf16_div(bf16_t a, bf16_t b)
- Function: Divide a by b.
- Process:
    1. Handle NaN, Inf/Inf→NaN, finite/0→±Inf, finite/Inf→±0, and zero/zero→NaN.
    2. Restore hidden 1s for normals.
    3. Form a fixed-point numerator by shifting mant_a left and perform 16-step bitwise long division by mant_b to get a quotient.
    4. result_exp = exp_a − exp_b + 127, with tweaks if either operand was subnormal.
    5. Normalize quotient to 1.x format, right-shift to keep 8 effective bits, keep 7 as fraction.
    6. Apply overflow/underflow rules and pack.

#### bf16_sqrt(bf16_t a)
- Function: Square root for bf16.
- Process:
    1. Handle NaN propagation, sqrt(+Inf)=+Inf, sqrt(0)=0, negative→NaN, and flush subnormals to 0.
    2. Let e = exp−127. If e is odd, left-shift mant once and set new_exp = ⌊e/2⌋+127; else new_exp = e/2+127.
    3. Reconstruct mant = 1.xx form and do integer binary search to approximate √mant in a scaled 8-bit domain.
    4. Normalize the result into 1.x, extract 7 fraction bits.
    5. Check overflow/underflow and pack.

#### test()
- Function: Simple tests for add, mul, and sqrt bf16 operations
- Process:
    1. Convert inputs to bf16, run the operation, widen to float32.
    2. Compare to float32 arithmetic; print results and a pass/fail line.

### 5-stage pipelined processor
I still choose the 5 stage processor to run this RISC-V program.
For the same reason as in Problem_B, to keep the explanation concise, one representative instruction `add  t5, t0, t1` from bf16_mul function is analyzed to illustrate its execution across the processor’s five pipeline stages.
> `Add rd, rs1, rs2` adds the values in registers `rs1` and `rs2`, and stores the result in register `rd`.

The five figures below illustrate how this instruction moves through each pipeline stage.
1. Instruction fetch (IF)
![image](https://hackmd.io/_uploads/r1o7JG_agx.png)
    - We start from the instruction located at address `0x00000174`.
    - The machine code stored at this address is `0x00628f33`.
    - Because there is no branch or jump at this point, the multiplexer selects the sequential path (PC + 4) as the next PC source.
2. Instruction decode and register fetch (ID)
![image](https://hackmd.io/_uploads/BJgBkzuTeg.png)
    - Instruction `0x00628f33` is decoded into the following parts:
        - opcode = `ADD`
        - Wr idx = `0x1e` (destination register `x30`)
        - R1 idx = `0x05` (source register `x5`)
        - R2 idx = `0x06` (source register `x6`)
    - Because add is an R-type instruction, both source registers are used. The register file reads the values of x5 and x6 and forwards them to the `Reg 1` and `Reg 2` outputs respectively.
3. Execute (EX)
![image](https://hackmd.io/_uploads/By_UJGOpgg.png)
    - In this stage, the ALU performs the arithmetic operation defined by the instruction `add x30, x5, x6`.
    - The first-level multiplexers select the register values read from the ID stage:
        - Op1 = Reg1 = `0x0000007f` (value from `x5`)
        - Op2 = Reg2 = `0x00000080` (value from `x6`)
    - The result (Res = `0x000000ff`) is stored in the EX/MEM pipeline register for use in the next stage.
4. Memory access (MEM)
![image](https://hackmd.io/_uploads/S1vDyM_Txx.png)
    - Because add is an R-type arithmetic instruction, it does not perform any data memory access. Thus, both MemRead and MemWrite control signals remain inactive.
    - The ALU result (`0x000000ff`) is simply forwarded from the EX/MEM register to the MEM/WB register.
5. Register write back (WB)
![image](https://hackmd.io/_uploads/SySdkfuagg.png)

    - In the WB stage, the ALU result calculated in the previous stages is finally written back to the register file.
    - The MEM/WB pipeline register holds two potential write-back sources:
        1. Data memory output (for load instructions)
        2. ALU result (for arithmetic or logical instructions)
    - The multiplexer chooses the ALU result (`0x000000ff`) because this is an R-type operation. The selected value is then sent to the register file write port as Wr data.

# Optimize [LeetCode Problem #231.](https://leetcode.com/problems/power-of-two/) using CLZ to Improve Runtime Performance
> ### Description:
> Given an integer `n`, return `true` if it is a power of two. Otherwise, return `false`.
> An integer `n` is a power of two, if there exists an integer `x` such that n == $2^x$. 
>
> ### Examples:
> Input: n = 1
> Output: true
>
> Input: n = 16
> Output: true
>
> Input: n = 3
> Output: false
>
> ### Constraints:
> $-2^{31} <= n <= 2^{31} - 1$

## Implementation and Optimization
The problem asks us to determine whether a given number `n` is a power of two.

This means we need to check if there exists an integer `k` such that `n = 2^k`.
In binary form, a power of two has exactly one bit set to 1, and all other bits are 0.

To solve this:
- We can start from `n` and keep dividing it by 2 until it becomes 1.
If at any step we find that `n` is not divisible by 2, then it is not a power of two.
This approach is simple and intuitive but takes **O(log n)** time because it performs one division per bit in the binary representation.

To optimize:
- we can use the **clz (Count Leading Zeros)** instruction to directly locate the most significant 1 bit in `n`.
If `n` is a power of two, then `n` must be equal to `1 << (31 - clz(n))`, meaning there is only one set bit in its binary form.
Using CLZ, we can perform this check in **O(1)** time, without any loops or divisions.

This is a typical bit manipulation problem where understanding binary representation helps simplify the logic and improve performance.
The intuitive approach relies on repetitive arithmetic operations, while the CLZ-based solution leverages hardware-supported bit counting to achieve constant-time execution.


You can find the source code [here](https://github.com/mephen/2025-arch-hw1).

### C code
#### Original
:::spoiler open
```C=
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

/* -------------------- Naive Loop Version -------------------- */
/* Divide by 2 until n == 1; if any remainder appears, return false */
bool isPowerOfTwo_naive(uint32_t n) {
    if (n == 0) return false;
    while (n % 2 == 0)
        n /= 2;
    return n == 1;
}

/* -------------------- Test Driver -------------------- */
int main(void) {
    uint32_t test_vals[] = {1, 16, 3};
    int num_tests = sizeof(test_vals) / sizeof(test_vals[0]);

    for (int i = 0; i < num_tests; i++) {
        uint32_t n = test_vals[i];
        bool ret = isPowerOfTwo_naive(n);
        printf("%s\n", ret ? "true" : "false");
    }
    return 0;
}
```
:::
#### Using CLZ
:::spoiler open
```C=
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

/* -------------------- CLZ Optimized Version -------------------- */
int clz(uint32_t x) {
    if (x == 0) return 32; // all bits are 0 → 32 leading zeros
    int n = 0;
    if ((x >> 16) == 0) { n += 16; x <<= 16; }
    if ((x >> 24) == 0) { n += 8;  x <<= 8;  }
    if ((x >> 28) == 0) { n += 4;  x <<= 4;  }
    if ((x >> 30) == 0) { n += 2;  x <<= 2;  }
    if ((x >> 31) == 0) { n += 1; }
    return n;
}

/* Use __builtin_clz to find the most significant 1 bit */
bool isPowerOfTwo_clz(uint32_t n) {
    if (n == 0) return false;
    int msb_pos = 31 - clz(n);   // position of highest 1 bit
    return n == (1u << msb_pos);           // must equal 1 << position
}

/* -------------------- Test Driver -------------------- */
int main(void) {
    uint32_t test_vals[] = {1, 16, 3};
    int num_tests = sizeof(test_vals) / sizeof(test_vals[0]);

    for (int i = 0; i < num_tests; i++) {
        uint32_t n = test_vals[i];
        bool ret = isPowerOfTwo_clz(n);
        printf("%s\n", ret ? "true" : "false");
    }
    return 0;
}
```
:::

### Assembly code
#### Original
code line: 75
cpu cycle: 189
:::spoiler open
```asm=
    .data
test_vals:
    .word 1, 16, 3
num_tests:
    .word 3
str_true:
    .asciz "true\n"
str_false:
    .asciz "false\n"
    .text
    .globl _start
# main()
_start:
    la      s0, test_vals         # base = &test_vals[0]
    lw      s2, num_tests         # num_tests
    li      s3, 0                 # i = 0

main_loop:
    bge     s3, s2, program_end

    mv      t1, s0                # t1 = base
    mv      t2, s3                # t2 = i
addr_add4_loop:
    beqz    t2, addr_ready
    addi    t1, t1, 4             # t1 += 4
    addi    t2, t2, -1
    addi    t0, t1, 0
    j       addr_add4_loop

addr_ready:
    lw      a0, 0(t1)             # a0 = test_vals[i]
    slli    t0, a0, 0             
    lw      a0, 0(t1)             
    add     t0, a0, t0            

    jal     ra, is_pow2_naive     # result in a0

    beqz    a0, print_false
    la      a0, str_true
    li      a7, 4
    ecall
    j       next_item

print_false:
    la      a0, str_false
    li      a7, 4
    ecall

next_item:
    addi    s3, s3, 1
    j       main_loop

program_end:
    li      a7, 10
    ecall

# is_pow2_naive
is_pow2_naive:
    beqz    a0, ipn_ret_false
ipn_loop_check_lsb:
    andi    t0, a0, 1
    beqz    t0, ipn_shift_right
    li      t1, 1
    beq     a0, t1, ipn_ret_true
    j       ipn_ret_false
ipn_shift_right:
    srli    a0, a0, 1
    bnez    a0, ipn_loop_check_lsb
    j       ipn_ret_false
ipn_ret_true:
    li      a0, 1
    ret
ipn_ret_false:
    li      a0, 0
    ret
```
:::
#### Using CLZ
code line: 78
cpu cycle: 174
:::spoiler open
```asm=
    .data
test_vals:
    .word 1, 16, 3
num_tests:
    .word 3
str_true:
    .asciz "true\n"
str_false:
    .asciz "false\n"
    .text
    .globl _start
# _start
_start:
    la      s0, test_vals      # s0 = ptr
    lw      s2, num_tests      # s2 = remaining
    la      s4, str_true       # s4 = "true\n"
    la      s5, str_false      # s5 = "false\n"
    li      a7, 4              # print string
main_loop:
    beqz    s2, program_end
    lw      a0, 0(s0)          # a0 = n
    jal     ra, is_power_of_two_clz
    beqz    a0, do_print_false
    mv      a0, s4
    ecall
    j       next_item
do_print_false:
    mv      a0, s5
    ecall
next_item:
    addi    s0, s0, 4
    addi    s2, s2, -1
    j       main_loop
program_end:
    li      a7, 10
    ecall
# is_power_of_two_clz
is_power_of_two_clz:
    beqz    a0, isp_false      # n==0 → false
    mv      t0, a0             # t0 = origin
    li      t1, 0              # t1 = clz count
    mv      t2, a0             # t2 = working x
    # chk 16
    srli    t3, t2, 16
    bnez    t3, clz_c8
    addi    t1, t1, 16
    slli    t2, t2, 16
clz_c8:
    srli    t3, t2, 24
    bnez    t3, clz_c4
    addi    t1, t1, 8
    slli    t2, t2, 8
clz_c4:
    srli    t3, t2, 28
    bnez    t3, clz_c2
    addi    t1, t1, 4
    slli    t2, t2, 4
clz_c2:
    srli    t3, t2, 30
    bnez    t3, clz_c1
    addi    t1, t1, 2
    slli    t2, t2, 2
clz_c1:
    srli    t3, t2, 31
    bnez    t3, clz_cdone
    addi    t1, t1, 1
clz_cdone:
    li      t4, 31
    sub     t4, t4, t1         # t4 = msb_pos
    li      t3, 1
    sll     t3, t3, t4         # t3 = 1 << msb_pos
    beq     t0, t3, isp_true
isp_false:
    li      a0, 0
    ret
isp_true:
    li      a0, 1
    ret
```
::: 

### Analysis
- From the view of c code：
    - time complexity: **O(log n)** -> **O(1)**
    - Reason: use the clz (Count Leading Zeros) instruction to directly locate the most significant 1 bit in n.
- From the view of assembly code：
    - cpu cycles: **189** -> **174**
    - Reason: 
        - **Fewer branches**: The CLZ version uses 5 fixed conditional checks; the original version loops up to 32 times with multiple branches per loop and causes more branch penalties
        - **Lower branch misprediction cost**: Short, predictable control flow reduces branch flush cycles compared to the repeated loop branches in the original version.
        - **Fewer data hazards**: CLZ uses shifts and logic only; original version repeatedly loads and uses results immediately, triggering load-use stalls.
        - **Better pipeline utilization**: With fewer branches and no tight data dependencies, the CLZ version keeps IF–ID–EX–MEM–WB stages filled more consistently.


# Reference
- This assignment was completed with assistance from [ChatGPT] for [grammar checking, initial brainstorming, and idea refinement]. All final analysis and conclusions are my own.
- [How to Write a Git Commit Message](https://cbea.ms/git-commit/)
- [Assignment 1 Example: RISC-V Assembly and Instruction Pipeline](https://hackmd.io/@sysprog/2025-arch-homework1)
- [Jim's Dev Blog: RISC-V 指令集架構介紹](https://tclin914.github.io/3ad6268d/)
- [RISC-V Reference Card](https://www.cl.cam.ac.uk/teaching/1617/ECAD+Arch/files/docs/RISCVGreenCardv8-20151013.pdf)
- [Computer Architecture (2025 Fall): Guidelines for Student Use of AI Tools](https://hackmd.io/@sysprog/arch2025-ai-guidelines)