# Assignment 1: RISC-V Assembly and Instruction Pipeline contributed by < [`daoxuewu`](https://github.com/daoxuewu/ca2025-quizzes) > source code : [repository](https://github.com/daoxuewu/ca2025-homework1) ## [Problem B](https://hackmd.io/@sysprog/arch2025-quiz1-sol#Problem-B) (from quiz1) ### What Is `uf8`? > A logarithmic codec that maps 20-bit unsigned integers (0–1,015,792) into 8-bit symbols. It gives ~2.5:1 compression with ≤6.25% relative error—good when range matters more than fine precision. > Below is the formula of uf8 encoding and decoding. * **Decoding** $$ D(b) = m \cdot 2^e + (2^e - 1) \cdot 16 $$ where $e = \lfloor b/16 \rfloor$ and $m = b \bmod 16$ * **Encoding** $$ E(v) = \begin{cases} v, & \text{if } v < 16 \\ 16e + \lfloor(v - \text{offset}(e))/2^e\rfloor, & \text{otherwise} \end{cases} $$ where $\text{offset}(e) = (2^e - 1) \cdot 16$ Error Analysis - **Absolute Error:** $\Delta_{\max} = 2^{e} - 1$ - **Relative Error:** $\varepsilon_{\max} = 1/16 = 6.25\%$ - **Expected Error:** $\mathbb{E}[\varepsilon] \approx 3\%$ Information Theory - **Input Entropy:** 20 bits - **Output Entropy:** 8 bits - **Theoretical Minimum:** 7.6 bits (for 6.25% error bound) - **Efficiency:** \(8/7.6 = 95\%\) optimal | Exponent | Range | Step Size | |:--------|:------------------------|:----------| | 0 | $[0, 15]$ | 1 | | 1 | $[16, 46]$ | 2 | | 2 | $[48, 108]$ | 4 | | 3 | $[112, 232]$ | 8 | | … | … | $2^{e}$ | | 15 | $[524,272, 1,015,792]$ | 32,768 | ### C code ```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 { // Assume we start with 32 leading zeros; each step tests half (binary search idea) int n = 32, c = 16; do { uint32_t y = x >> c; // shift the number right by c bits if (y) { // check if the high part has any non-zero bit; y != 0 means MSB is on the left side of the midpoint n -= c; // at least c bits are not leading zeros x = y; // shrink the range and continue } c >>= 1; // divide c by 2; continue binary search } while (c); return n - x; // clz(0) returns 32 by design; for nonzero x, the final x becomes 1 and (n - x) is in [0,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; // With 4 mantissa bits, a good initial guess is exponent = msb - 4 (derived from the format). if (exponent > 15) exponent = 15; /* Calculate overflow (offset) for the estimated exponent: compute (2^e - 1) * 16 */ for (uint8_t e = 0; e < exponent; e++) overflow = (overflow << 1) + 16; /* Adjust if estimate was too large: adjust downward */ while (exponent > 0 && value < overflow) { overflow = (overflow - 16) >> 1; exponent--; } } /* Find exact exponent: adjust upward if the estimate was too small */ 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; } ``` ### RV32I Assembly code Use a **branchless CLZ** to accelerate the **precomputation step** in `uf8_encode`. :::spoiler Expand for details ```asm= .text .globl _start _start: jal ra, main # jump to main halt: li a7, 10 ecall # if main returns, exit # ------------------------------------------------------------ # uint32_t clz_branchless(uint32_t x) # Branchless binary-search CLZ: uses sltu to form shift amounts # ------------------------------------------------------------ clz_branchless: li t0, 32 # n = 32 mv t1, a0 # t1 = x srli t2, t1, 16 # y = x >> 16 sltu t3, zero, t2 # b = (y != 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, zero, t2 slli t4, t3, 3 # s = b * 8 srl t1, t1, t4 sub t0, t0, t4 srli t2, t1, 4 sltu t3, zero, t2 slli t4, t3, 2 # s = b * 4 srl t1, t1, t4 sub t0, t0, t4 srli t2, t1, 2 sltu t3, zero, t2 slli t4, t3, 1 # s = b * 2 srl t1, t1, t4 sub t0, t0, t4 srli t2, t1, 1 sltu t3, zero, 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 (x becomes 0 or 1) ret # ------------------------------------------------------------ # uint32_t uf8_decode(uint8_t fl) # D(b) = (m << e) + ((1<<e) - 1) << 4 # ------------------------------------------------------------ uf8_decode: 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 # value = (m << e) + offset ret # ------------------------------------------------------------ # uint8_t uf8_encode(uint32_t value) # Uses branchless CLZ to locate msb -> initial exponent guess # ------------------------------------------------------------ uf8_encode: addi sp, sp, -16 sw ra, 12(sp) # store ra (because we call clz_branchless) sw s0, 8(sp) # s0 = exponent sw s1, 4(sp) # s1 = overflow (offset) sltiu t0, a0, 16 # if (value < 16) return value; beqz t0, enCode_normalVal andi a0, a0, 0xFF # keep in 8-bit just in case j enCode_ret enCode_normalVal: mv t5, a0 # v = value jal ra, clz_branchless # a0 = clz(value) li t0, 31 sub t1, t0, a0 # msb = 31 - lz mv a0, t5 # restore value li s0, 0 # exponent = 0 li s1, 0 # overflow = 0 sltiu t2, t1, 5 # if (msb >= 5) => !(t1<5) bnez t2, enCode_find_up # if msb < 5, skip initial guess addi s0, t1, -4 # exponent = msb - 4 sltiu t3, s0, 16 bnez t3, enCode_initGuess_overflow_ok li s0, 15 # clamp exponent to 15 # overflow = ((1<<e)-1) << 4 enCode_initGuess_overflow_ok: li s1, 0 # overflow = 0 li t4, 0 # e = 0 enCode_overflow_loop: bge t4, s0, enCode_adjust_down slli s1, s1, 1 addi s1, s1, 16 addi t4, t4, 1 j enCode_overflow_loop # if value < overflow, step exponent down until it fits enCode_adjust_down: beqz s0, enCode_find_up sltu t4, a0, s1 beqz t4, enCode_find_up addi s1, s1, -16 srli s1, s1, 1 # overflow = (overflow - 16) >> 1 addi s0, s0, -1 j enCode_adjust_down # then go upward to the exact exponent enCode_find_up: li t4, 15 enCode_up_loop: bge s0, t4, enCode_up_done slli t1, s1, 1 addi t1, t1, 16 # next_overflow = (overflow << 1) + 16 sltu t2, a0, t1 bnez t2, enCode_up_done mv s1, t1 addi s0, s0, 1 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 enCode_ret: lw ra, 12(sp) lw s0, 8(sp) lw s1, 4(sp) addi sp, sp, 16 ret # ------------------------------------------------------------ # static bool test(void) # Prints each case, checks decode/encode roundtrip and monotonicity # ------------------------------------------------------------ test: addi sp, sp, -40 sw ra, 36(sp) sw s0, 32(sp) # previous_value sw s1, 28(sp) # passed sw s2, 24(sp) # i sw s3, 20(sp) # 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 bge s2, t3, t_done la a0, msg_test # "test data: " li a7, 4 ecall mv a0, s2 # print i li a7, 1 ecall la a0, msg_nl # newline li a7, 4 ecall mv a0, s2 # fl = i jal ra, uf8_decode # value = decode(fl) mv s3, a0 mv a0, s3 jal ra, uf8_encode # fl2 = encode(value) mv t1, a0 bne s2, t1, t_fail_flag # if (fl != fl2) report slt t2, s0, s3 # if (previous_value < value) OK bnez t2, t_set_prev # non-increasing t_bad_inc: la a0, msg_noninc_a li a7, 4 ecall mv a0, s2 # fl li a7, 1 ecall la a0, msg_noninc_b li a7, 4 ecall mv a0, s3 # value li a7, 1 ecall la a0, msg_noninc_c li a7, 4 ecall mv a0, s0 # previous_value li a7, 1 ecall la a0, msg_nl li a7, 4 ecall li s1, 0 # passed = false j t_set_prev # mismatch fl vs fl2 t_fail_flag: la a0, msg_mismatch_a li a7, 4 ecall mv a0, s2 # fl li a7, 1 ecall la a0, msg_mismatch_b li a7, 4 ecall mv a0, s3 # value li a7, 1 ecall la a0, msg_mismatch_c li a7, 4 ecall mv a0, t1 # fl2 li a7, 1 ecall la a0, msg_nl 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 t_done: mv a0, s1 # return passed lw ra, 36(sp) lw s0, 32(sp) lw s1, 28(sp) lw s2, 24(sp) lw s3, 20(sp) lw t0, 16(sp) lw t1, 12(sp) lw t2, 8(sp) lw t3, 4(sp) addi sp, sp, 40 ret # ------------------------------------------------------------ # int main(void) # ------------------------------------------------------------ main: addi sp, sp, -16 sw ra, 12(sp) jal ra, test beqz a0, print_fail la a0, msg_ok # "All tests passed.\n" li a7, 4 ecall j main_exit print_fail: la a0, msg_fail li a7, 4 ecall main_exit: lw ra, 12(sp) addi sp, sp, 16 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= " ``` ::: result: ![image](https://hackmd.io/_uploads/BJGysOqalx.png) *** Compiler generate (RISC-V 32bits gcc 13.1.0) :::spoiler Expand for details ```asm= clz(unsigned int): addi sp,sp,-48 sw s0,44(sp) addi s0,sp,48 sw a0,-36(s0) li a5,32 sw a5,-20(s0) li a5,16 sw a5,-24(s0) .L3: lw a5,-24(s0) lw a4,-36(s0) srl a5,a4,a5 sw a5,-28(s0) lw a5,-28(s0) beq a5,zero,.L2 lw a4,-20(s0) lw a5,-24(s0) sub a5,a4,a5 sw a5,-20(s0) lw a5,-28(s0) sw a5,-36(s0) .L2: lw a5,-24(s0) srai a5,a5,1 sw a5,-24(s0) lw a5,-24(s0) bne a5,zero,.L3 lw a4,-20(s0) lw a5,-36(s0) sub a5,a4,a5 mv a0,a5 lw s0,44(sp) addi sp,sp,48 jr ra uf8_decode(unsigned char): addi sp,sp,-48 sw s0,44(sp) addi s0,sp,48 mv a5,a0 sb a5,-33(s0) lbu a5,-33(s0) andi a5,a5,15 sw a5,-20(s0) lbu a5,-33(s0) srli a5,a5,4 sb a5,-21(s0) lbu a5,-21(s0) li a4,15 sub a5,a4,a5 li a4,32768 addi a4,a4,-1 sra a5,a4,a5 slli a5,a5,4 sw a5,-28(s0) lbu a5,-21(s0) lw a4,-20(s0) sll a4,a4,a5 lw a5,-28(s0) add a5,a4,a5 mv a0,a5 lw s0,44(sp) addi sp,sp,48 jr ra uf8_encode(unsigned int): addi sp,sp,-64 sw ra,60(sp) sw s0,56(sp) addi s0,sp,64 sw a0,-52(s0) lw a4,-52(s0) li a5,15 bgtu a4,a5,.L8 lw a5,-52(s0) andi a5,a5,0xff j .L9 .L8: lw a0,-52(s0) call clz(unsigned int) mv a5,a0 sw a5,-32(s0) li a4,31 lw a5,-32(s0) sub a5,a4,a5 sw a5,-36(s0) sb zero,-17(s0) sw zero,-24(s0) lw a4,-36(s0) li a5,4 ble a4,a5,.L16 lw a5,-36(s0) andi a5,a5,0xff addi a5,a5,-4 sb a5,-17(s0) lbu a4,-17(s0) li a5,15 bleu a4,a5,.L11 li a5,15 sb a5,-17(s0) .L11: sb zero,-25(s0) j .L12 .L13: lw a5,-24(s0) slli a5,a5,1 addi a5,a5,16 sw a5,-24(s0) lbu a5,-25(s0) addi a5,a5,1 sb a5,-25(s0) .L12: lbu a4,-25(s0) lbu a5,-17(s0) bltu a4,a5,.L13 j .L14 .L15: lw a5,-24(s0) addi a5,a5,-16 srli a5,a5,1 sw a5,-24(s0) lbu a5,-17(s0) addi a5,a5,-1 sb a5,-17(s0) .L14: lbu a5,-17(s0) beq a5,zero,.L16 lw a4,-52(s0) lw a5,-24(s0) bltu a4,a5,.L15 j .L16 .L19: lw a5,-24(s0) slli a5,a5,1 addi a5,a5,16 sw a5,-40(s0) lw a4,-52(s0) lw a5,-40(s0) bltu a4,a5,.L20 lw a5,-40(s0) sw a5,-24(s0) lbu a5,-17(s0) addi a5,a5,1 sb a5,-17(s0) .L16: lbu a4,-17(s0) li a5,14 bleu a4,a5,.L19 j .L18 .L20: nop .L18: lw a4,-52(s0) lw a5,-24(s0) sub a4,a4,a5 lbu a5,-17(s0) srl a5,a4,a5 sb a5,-41(s0) lb a5,-17(s0) slli a5,a5,4 slli a4,a5,24 srai a4,a4,24 lb a5,-41(s0) or a5,a4,a5 slli a5,a5,24 srai a5,a5,24 andi a5,a5,0xff .L9: mv a0,a5 lw ra,60(sp) lw s0,56(sp) addi sp,sp,64 jr ra .LC0: .string "test data: %d\n" .LC1: .string "%02x: produces value %d but encodes back to %02x\n" .LC2: .string "%02x: value %d <= previous_value %d\n" test(): addi sp,sp,-48 sw ra,44(sp) sw s0,40(sp) addi s0,sp,48 li a5,-1 sw a5,-20(s0) li a5,1 sb a5,-21(s0) sw zero,-28(s0) j .L22 .L25: lw a1,-28(s0) lui a5,%hi(.LC0) addi a0,a5,%lo(.LC0) call printf lw a5,-28(s0) sb a5,-29(s0) lbu a5,-29(s0) mv a0,a5 call uf8_decode(unsigned char) mv a5,a0 sw a5,-36(s0) lw a5,-36(s0) mv a0,a5 call uf8_encode(unsigned int) mv a5,a0 sb a5,-37(s0) lbu a4,-29(s0) lbu a5,-37(s0) beq a4,a5,.L23 lbu a5,-29(s0) lbu a4,-37(s0) mv a3,a4 lw a2,-36(s0) mv a1,a5 lui a5,%hi(.LC1) addi a0,a5,%lo(.LC1) call printf sb zero,-21(s0) .L23: lw a4,-36(s0) lw a5,-20(s0) bgt a4,a5,.L24 lbu a5,-29(s0) lw a3,-20(s0) lw a2,-36(s0) mv a1,a5 lui a5,%hi(.LC2) addi a0,a5,%lo(.LC2) call printf sb zero,-21(s0) .L24: lw a5,-36(s0) sw a5,-20(s0) lw a5,-28(s0) addi a5,a5,1 sw a5,-28(s0) .L22: lw a4,-28(s0) li a5,255 ble a4,a5,.L25 lbu a5,-21(s0) mv a0,a5 lw ra,44(sp) lw s0,40(sp) addi sp,sp,48 jr ra .LC3: .string "All tests passed." main: addi sp,sp,-16 sw ra,12(sp) sw s0,8(sp) addi s0,sp,16 call test() mv a5,a0 beq a5,zero,.L28 lui a5,%hi(.LC3) addi a0,a5,%lo(.LC3) call puts li a5,0 j .L29 .L28: li a5,1 .L29: mv a0,a5 lw ra,12(sp) lw s0,8(sp) addi sp,sp,16 jr ra ``` ::: result: ![image](https://hackmd.io/_uploads/H1NT9_qaex.png) ### Results Comparison **Our optimized (branchless CLZ) vs. Compiler-generated (baseline)** * **Speedup:** **2.17×** (cycles ↓ **53.8%**) * **Instructions retired:** ↓ **49.3%** (−34,253) * **CPI:** ↓ **8.8%** (1.47 → 1.34) * **IPC:** ↑ **10.0%** (0.681 → 0.749) By replacing branch-based CLZ with a **branchless** binary-search CLZ, we cut instructions and reduce control hazards, improving pipeline utilization for an overall **~2.17×** speedup on Ripes. ## [Problem C](https://hackmd.io/@sysprog/arch2025-quiz1-sol#Problem-C) (from quiz1) ### What Is `bfloat16`? 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). Bit Layout ``` ┌─────────┬──────────────┬──────────────┐ │Sign (1) │ Exponent (8) │ Mantissa (7) │ └─────────┴──────────────┴──────────────┘ 15 14 6 0 S: Sign bit (0 = positive, 1 = negative) E: Exponent bits (8 bits, bias = 127) M: Mantissa/fraction bits (7 bits) ``` The value \(v\) of a BFloat16 number is calculated as: $$ v = (-1)^S \times 2^{E - 127} \times \left(1 + \frac{M}{128}\right) $$ where: - $S \in \{0,1\}$ is the sign bit - $E \in [1, 254]$ is the biased exponent - $M \in [0, 127]$ is the mantissa value **Special Cases** - **Zero:** $E = 0, M = 0 \Rightarrow v = (-1)^S \times 0$ - **Infinity:** $E = 255, M = 0 \Rightarrow v = (-1)^S \times \infty$ - **NaN:** $E = 255, M \ne 0 \Rightarrow v = \mathrm{NaN}$ - **Denormals:** Not supported (flush to zero) ### C code ```c= #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <string.h> typedef struct { uint16_t bits; // Raw bfloat16 payload: [sign(1) | exponent(8) | mantissa(7)] } bf16_t; /* Bit masks and constants for bfloat16 (IEEE 754-like, bias = 127) */ #define BF16_SIGN_MASK 0x8000U // bit 15 #define BF16_EXP_MASK 0x7F80U // bits 14..7 (8-bit exponent) #define BF16_MANT_MASK 0x007FU // bits 6..0 (7-bit fraction) #define BF16_EXP_BIAS 127 /* Convenient constructors for common non-finite/zero values */ #define BF16_NAN() ((bf16_t) {.bits = 0x7FC0}) // Quiet NaN (exp=255, mant!=0) #define BF16_ZERO() ((bf16_t) {.bits = 0x0000}) // +0 (−0 is 0x8000) /* NaN test: exponent all ones and nonzero mantissa */ static inline bool bf16_isnan(bf16_t a) { return ((a.bits & BF16_EXP_MASK) == BF16_EXP_MASK) && (a.bits & BF16_MANT_MASK); } /* Infinity test: exponent all ones and zero mantissa */ static inline bool bf16_isinf(bf16_t a) { return ((a.bits & BF16_EXP_MASK) == BF16_EXP_MASK) && !(a.bits & BF16_MANT_MASK); } /* Zero test: both exponent and mantissa are zero (treats +0 and −0 as zero) */ static inline bool bf16_iszero(bf16_t a) { return !(a.bits & 0x7FFF); } /* float32 -> bfloat16 * Strategy: copy the high 16 bits (sign, exponent, top 7 fraction bits), * with round-to-nearest-even using the discarded low 16 bits. * Special values (NaN/Inf) are mapped by simply taking the high 16 bits. */ 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}; /* RNE: add 0x7FFF plus the LSB of the kept field (ties-to-even) */ f32bits += ((f32bits >> 16) & 1) + 0x7FFF; // round-to-nearest-even return (bf16_t) {.bits = f32bits >> 16}; } /* bfloat16 -> float32 * Strategy: put bf16 in the high 16 bits and zero the low 16 bits. */ 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; } /* Addition for bfloat16 (integer implementation). * NOTE: No guard/round/sticky; mantissa arithmetic is truncated. * Steps: * 1) Handle NaN/Inf/zero fast paths. * 2) Restore implicit 1 for normalized operands. * 3) Align exponents (right-shift the smaller mantissa). * 4) Add or subtract mantissas based on signs. * 5) Normalize and check overflow/underflow (flush subnormals to zero). */ 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; /* Handle infinities/NaNs first */ if (exp_a == 0xFF) { if (mant_a) return a; // a is NaN (propagate) if (exp_b == 0xFF) // If b is NaN → return NaN // If both are Infs: // same sign → keep Inf // opposite → +Inf + -Inf → 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; // a == 0 if (!exp_b && !mant_b) return a; // b == 0 /* Restore implicit leading 1 for normalized numbers */ if (exp_a) mant_a |= 0x80; if (exp_b) mant_b |= 0x80; /* Exponent alignment (right-shift smaller mantissa). * If the gap exceeds available mantissa precision (7 bits + hidden 1), * the smaller operand is negligible. */ 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 → align b result_exp = exp_a; if (exp_diff > 8) // beyond alignment capacity → return a return a; mant_b >>= exp_diff; } else if (exp_diff < 0) { // exp_a < exp_b → align a result_exp = exp_b; if (exp_diff < -8) return b; mant_a >>= -exp_diff; } else { // Same exponent result_exp = exp_a; } /* Same sign → integer addition; else → integer subtraction */ if (sign_a == sign_b) { result_sign = sign_a; result_mant = (uint32_t) mant_a + mant_b; // result may be 9 bits /* Normalize if carry out (bit 8 set after 8-bit add) */ if (result_mant & 0x100) { result_mant >>= 1; if (++result_exp >= 0xFF) return (bf16_t) {.bits = (result_sign << 15) | 0x7F80}; // overflow → Inf } } else { // Different signs → subtraction with sign of larger magnitude 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(); // exact cancellation /* Normalize: left shift until hidden 1 is restored (bit7 = 1) */ while (!(result_mant & 0x80)) { result_mant <<= 1; if (--result_exp <= 0) return BF16_ZERO(); // underflow to zero (subnormals flushed) } } /* Pack sign | exponent | mantissa (truncate fractional bits) */ return (bf16_t) { .bits = (result_sign << 15) | ((result_exp & 0xFF) << 7) | (result_mant & 0x7F), }; } /* Subtraction via sign flip and reuse of addition: * a − b = a + (−b) */ static inline bf16_t bf16_sub(bf16_t a, bf16_t b) { b.bits ^= BF16_SIGN_MASK; // Negate b return bf16_add(a, b); } /* Multiplication for bfloat16 (integer implementation). * Steps: * 1) Handle NaN/Inf/zero cases (including Inf*0 = NaN). * 2) Normalize subnormals by shifting mantissa to set the hidden 1 and track exp_adjust. * 3) Multiply 8-bit mantissas → up to 16-bit product. * 4) Exponent = exp_a + exp_b − bias + exp_adjust. * 5) Normalize product to keep it in [1.0, 2.0) and pack. * 6) Handle overflow (→ Inf) and underflow (flush very small results to 0). */ 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: NaNs, Infs, zeros */ if (exp_a == 0xFF) { if (mant_a) return a; // 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 operands (restore hidden 1); track adjustment for subnormals */ 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; /* 8-bit × 8-bit → 16-bit mantissa product (with hidden 1s) */ 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 product to [1.0, 2.0): * If bit15 is set, value ≥ 2.0 → shift one and bump exponent. * Then drop to 7 fraction bits by shifting. */ if (result_mant & 0x8000) { result_mant = (result_mant >> 8) & 0x7F; result_exp++; } else result_mant = (result_mant >> 7) & 0x7F; /* Exponent overflow/underflow handling. * Very small underflows (<−6 here) are flushed to signed zero. */ if (result_exp >= 0xFF) return (bf16_t) {.bits = (result_sign << 15) | 0x7F80}; // Inf if (result_exp <= 0) { if (result_exp < -6) return (bf16_t) {.bits = result_sign << 15}; // flush to 0 result_mant >>= (1 - result_exp); // crude handling near subnormal range result_exp = 0; } /* Pack result */ return (bf16_t) {.bits = (result_sign << 15) | ((result_exp & 0xFF) << 7) | (result_mant & 0x7F)}; } /* Division for bfloat16 (integer implementation). * Steps: * 1) Handle NaN/Inf/zero corner cases (0/0, Inf/Inf, /0, /Inf). * 2) Restore hidden 1s, then perform bitwise long division to get quotient. * 3) Compute exponent (subtract exponents, add bias; adjust for subnormals). * 4) Normalize quotient to [1.0, 2.0) and pack. * 5) Handle overflow/underflow (flush very small to 0). */ 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; /* Special cases */ if (exp_b == 0xFF) { if (mant_b) return b; // NaN 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; // NaN return (bf16_t) {.bits = (result_sign << 15) | 0x7F80}; // Inf } if (!exp_a && !mant_a) return (bf16_t) {.bits = result_sign << 15}; // 0 / finite → 0 /* Restore hidden 1s for normalized values */ if (exp_a) mant_a |= 0x80; if (exp_b) mant_b |= 0x80; /* Compute quotient using integer long division on fixed-point: * Scale dividend so that the MSB can be extracted across 16 steps. */ uint32_t dividend = (uint32_t) mant_a << 15; uint32_t divisor = mant_b; uint32_t quotient = 0; // Bitwise long division: build 16-bit quotient (upper part of mantissa) for (int i = 0; i < 16; i++) { quotient <<= 1; if (dividend >= (divisor << (15 - i))) { dividend -= (divisor << (15 - i)); quotient |= 1; } } /* Exponent math for division */ int32_t result_exp = (int32_t) exp_a - exp_b + BF16_EXP_BIAS; if (!exp_a) result_exp--; // subnormal numerator adjustment if (!exp_b) result_exp++; // subnormal denominator adjustment /* Normalize quotient to [1.0, 2.0) then drop to 7 fraction bits */ if (quotient & 0x8000) quotient >>= 8; // already in [1,2) else { while (!(quotient & 0x8000) && result_exp > 1) { quotient <<= 1; result_exp--; } quotient >>= 8; } quotient &= 0x7F; /* Handle overflow/underflow and pack */ if (result_exp >= 0xFF) return (bf16_t) {.bits = (result_sign << 15) | 0x7F80}; // Inf if (result_exp <= 0) return (bf16_t) {.bits = result_sign << 15}; // flush to 0 return (bf16_t) {.bits = (result_sign << 15) | ((result_exp & 0xFF) << 7) | (quotient & 0x7F)}; } /* Square root for bfloat16 (integer-only). * Idea: * Represent a as 2^e * m, with m in [1,2). * new_exp = floor(e/2) + bias (handle odd e by doubling m before sqrt). * Compute sqrt(m) using integer binary search on an 8.7 fixed-point grid, * then normalize and pack. Special values and negatives are handled. */ 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; /* Special cases: NaN/Inf/0/negative */ 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 finite → NaN if (sign) return BF16_NAN(); // Flush denormals to zero (no subnormal support) if (!exp) return BF16_ZERO(); /* Compute half exponent: * e = exp - bias (unbiased exponent) * If e is odd, double mantissa (so it stays in [1,2)) before sqrt. */ int32_t e = exp - BF16_EXP_BIAS; int32_t new_exp; uint32_t m = 0x80 | mant; // Restore implicit 1 → m in [128,255] (scaled) if (e & 1) { m <<= 1; // make exponent even new_exp = ((e - 1) >> 1) + BF16_EXP_BIAS; // (e-1)/2 + bias } else { new_exp = (e >> 1) + BF16_EXP_BIAS; // e/2 + bias } /* Binary search sqrt(m) over a fixed range. * Scale: treat mantissa as 8.7 fixed-point (implicit 1 = 128). * Search mid in [90,256] so that (mid*mid)/128 approximates sqrt region. */ uint32_t low = 90; uint32_t high = 256; uint32_t result = 128; // initial guess ~1.0 // Binary search for sqrt(m) with integer arithmetic while (low <= high) { uint32_t mid = (low + high) >> 1; uint32_t sq = (mid * mid) / 128; // mid^2 in the same scale if (sq <= m) { result = mid; low = mid + 1; } else { high = mid - 1; } } /* Normalize result to [128,256) (i.e., [1.0,2.0) in this scale) */ 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; // keep 7 fraction bits /* Pack, with overflow/underflow checks */ if (new_exp >= 0xFF) return (bf16_t) {.bits = 0x7F80}; // +Inf if (new_exp <= 0) return BF16_ZERO(); // flush to 0 return (bf16_t) {.bits = ((new_exp & 0xFF) << 7) | new_mant}; } /* Compact & readable sanity test for bfloat16 ops. Uses a tolerant comparison (≈1%) because bf16 has only 7 fraction bits. */ static int nearly_equal(float got, float expect, float rel_tol) { float diff = got - expect; if (diff < 0) diff = -diff; float scale = expect; if (scale < 0) scale = -scale; if (scale < 1.0f) scale = 1.0f; // avoid tiny-denominator explosion return diff <= rel_tol * scale; } int test(void) { const float A = 1.5f, B = 2.5f, C = 4.0f; const float TOL = 0.01f; // ~1% tolerance int ok = 1; bf16_t a = f32_to_bf16(A); bf16_t b = f32_to_bf16(B); /* add */ float add_got = bf16_to_f32(bf16_add(a, b)); float add_exp = A + B; printf("[add ] %.6f + %.6f = %.6f (expect %.6f)\n", A, B, add_got, add_exp); ok &= nearly_equal(add_got, add_exp, TOL); /* mul */ float mul_got = bf16_to_f32(bf16_mul(a, b)); float mul_exp = A * B; printf("[mul ] %.6f * %.6f = %.6f (expect %.6f)\n", A, B, mul_got, mul_exp); ok &= nearly_equal(mul_got, mul_exp, TOL); /* sqrt */ bf16_t c = f32_to_bf16(C); float sqrt_got = bf16_to_f32(bf16_sqrt(c)); printf("[sqrt] sqrt(%.6f) = %.6f (expect 2.000000)\n", C, sqrt_got); ok &= nearly_equal(sqrt_got, 2.0f, TOL); return ok; } int main(void) { int ok = test(); puts(ok ? "All tests passed." : "Some tests failed."); return ok ? 0 : 1; } ``` ### RV32I Assembly code By switching all numeric data to 16-bit **bfloat16** arrays and using halfword loads/stores (`lhu`/`sh`) instead of 32-bit accesses, the program reduces load/store memory traffic by **~50%** and halves the working-set size (i.e., **~2×** cache residency for the same data). :::spoiler Expand for details ```asm= .text .globl _start _start: # jump to main then exit jal ra, main li a0, 0 li a7, 10 ecall .globl main # ============================================================ # main - bf16 demo using 16-bit arrays and halfword load store # inputs # arr_bf16[0] = a = 0x3FC0 bf16 1.5 # arr_bf16[1] = b = 0x4020 bf16 2.5 # arr_bf16[2] = c = 0x4080 bf16 4.0 # outputs # arr_bf16[3] = a+b expected 0x4080 # arr_bf16[4] = a*b expected 0x4070 # arr_bf16[5] = sqrt(c) expected 0x4000 # syscalls # a7=4 print string at a0 # a7=10 exit with code in a0 # ============================================================ main: la s0, arr_bf16 # base pointer # a + b lhu a0, 0(s0) # a lhu a1, 2(s0) # b jal ra, bf16_add sh a0, 6(s0) # store sum at arr[3] # a * b lhu a0, 0(s0) # a lhu a1, 2(s0) # b jal ra, bf16_mul sh a0, 8(s0) # store product at arr[4] # sqrt c lhu a0, 4(s0) # c jal ra, bf16_sqrt sh a0, 10(s0) # store sqrt at arr[5] # verify results against expected constants # check sum la t0, arr_bf16 lhu t1, 6(t0) # got sum la t2, exp_add lhu t3, 0(t2) # expect sum bne t1, t3, print_fail # check product lhu t1, 8(t0) # got product la t2, exp_mul lhu t3, 0(t2) # expect product bne t1, t3, print_fail # check sqrt lhu t1, 10(t0) # got sqrt la t2, exp_sqrt lhu t3, 0(t2) # expect sqrt bne t1, t3, 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 # exit 0 li a7, 10 ecall print_fail: la a0, fail_msg li a7, 4 ecall li a0, 1 # exit 1 li a7, 10 ecall # ============================================================ # bf16_add a0=a_bits a1=b_bits -> a0 result # integer version truncate only # layout sign 1 bit, exp 8 bits, mant 7 bits, bias 127 # steps # handle nan inf zero # restore hidden one for normalized # align exponents by right shift smaller mant # add or sub by sign then normalize and pack # ============================================================ bf16_add: # signs srli t6, a0, 15 andi t6, t6, 1 # sign_a srli a5, a1, 15 andi a5, a5, 1 # sign_b # exponents srli t0, a0, 7 andi t0, t0, 0xFF # exp_a srli t1, a1, 7 andi t1, t1, 0xFF # exp_b # mantissas andi t2, a0, 0x7F # mant_a andi t3, a1, 0x7F # mant_b # specials li t4, 0xFF beq t0, t4, ADD_A_SPECIAL beq t1, t4, ADD_B_SPECIAL # zeros or t5, t0, t2 beq t5, x0, ADD_RET_B or t5, t1, t3 beq t5, x0, ADD_RET_A # restore hidden ones when exp not zero beq t0, x0, ADD_SKIP_A_IMP ori t2, t2, 0x80 ADD_SKIP_A_IMP: beq t1, x0, ADD_SKIP_B_IMP ori t3, t3, 0x80 ADD_SKIP_B_IMP: # align sub t4, t0, t1 # diff = exp_a - exp_b mv t5, t0 # result_exp default exp_a blt x0, t4, ADD_ALIGN_B # diff > 0 blt t4, x0, ADD_ALIGN_A # diff < 0 j ADD_SAME_EXP ADD_ALIGN_B: li a2, 8 blt a2, t4, ADD_RET_A srl t3, t3, t4 j ADD_SAME_EXP_PACK ADD_ALIGN_A: sub a2, x0, t4 # a2 = -diff li a3, 8 bge a2, a3, ADD_RET_B srl t2, t2, a2 mv t5, t1 # result_exp = exp_b j ADD_SAME_EXP_PACK ADD_SAME_EXP: ADD_SAME_EXP_PACK: # same sign add else subtract bne t6, a5, ADD_SUB # addition add a2, t2, t3 andi a3, a2, 0x100 beq a3, x0, ADD_PACK_ADD srli a2, a2, 1 addi t5, t5, 1 li a3, 0xFF bne t5, a3, ADD_PACK_ADD # overflow to inf slli a3, t6, 15 li t0, 0x7F80 or a0, a3, t0 ret ADD_SUB: # subtraction magnitude select bge t2, t3, ADD_SUB_A_GE_B mv a2, t3 sub a2, a2, t2 mv a3, a5 # result sign = sign_b j ADD_SUB_NORM ADD_SUB_A_GE_B: mv a2, t2 sub a2, a2, t3 mv a3, t6 # result sign = sign_a beq a2, x0, ADD_ZERO ADD_SUB_NORM: # normalize left until bit7 set andi a4, a2, 0x80 bne a4, x0, ADD_PACK_SUB ADD_SUB_NORM_LOOP: slli a2, a2, 1 addi t5, t5, -1 andi a4, a2, 0x80 beq a4, x0, ADD_SUB_NORM_LOOP bge x0, t5, ADD_ZERO ADD_PACK_SUB: andi a2, a2, 0x7F slli t5, t5, 7 slli a3, a3, 15 or a0, t5, a2 or a0, a0, a3 ret ADD_PACK_ADD: andi a2, a2, 0x7F slli t5, t5, 7 slli a3, t6, 15 or a0, t5, a2 or a0, a0, a3 ret ADD_ZERO: li a0, 0 ret # add specials ADD_A_SPECIAL: bnez t2, ADD_RET_A # a is nan li a2, 0xFF bne t1, a2, ADD_RET_A # a is inf and b finite andi a4, t3, 0x7F bnez a4, ADD_RET_A # b is nan bne t6, a5, ADD_NAN # +inf plus -inf j ADD_RET_A ADD_B_SPECIAL: bnez t3, ADD_RET_B # b is nan li a2, 0xFF bne t0, a2, ADD_RET_B andi a4, t2, 0x7F bnez a4, ADD_RET_B bne t6, a5, ADD_NAN j ADD_RET_B ADD_NAN: li a0, 0x7FC0 # quiet nan ret ADD_RET_A: ret ADD_RET_B: mv a0, a1 ret # ============================================================ # bf16_mul a0=a_bits a1=b_bits -> a0 result # integer only multiply no RV32M # handle nan inf zero # restore hidden ones normalize subnormals roughly # shift add 8x8 to 16 bit product # normalize and pack # ============================================================ bf16_mul: # signs and result sign srli t6, a0, 15 andi t6, t6, 1 # sign_a srli a5, a1, 15 andi a5, a5, 1 # sign_b xor a3, t6, a5 # result sign # exponents srli t0, a0, 7 andi t0, t0, 0xFF # exp_a srli t1, a1, 7 andi t1, t1, 0xFF # exp_b # mantissas andi t2, a0, 0x7F # mant_a andi t3, a1, 0x7F # mant_b # specials li t4, 0xFF beq t0, t4, MUL_A_SPECIAL beq t1, t4, MUL_B_SPECIAL # zeros or t5, t0, t2 beq t5, x0, MUL_RET_ZERO or t5, t1, t3 beq t5, x0, MUL_RET_ZERO # normalize inputs and track exp adjust li a2, 0 # exp_adjust beq t0, x0, MUL_NORM_A_SUB ori t2, t2, 0x80 j MUL_NORM_B MUL_NORM_A_SUB: MUL_NORM_A_LOOP: andi a4, t2, 0x80 bnez a4, MUL_NORM_B slli t2, t2, 1 addi a2, a2, -1 j MUL_NORM_A_LOOP MUL_NORM_B: beq t1, x0, MUL_NORM_B_SUB ori t3, t3, 0x80 j MUL_MUL MUL_NORM_B_SUB: MUL_NORM_B_LOOP: andi a4, t3, 0x80 bnez a4, MUL_MUL slli t3, t3, 1 addi a2, a2, -1 j MUL_NORM_B_LOOP # shift add multiply 8x8 -> 16 MUL_MUL: li t6, 0 # product mv a4, t2 # multiplicand mv a5, t3 # multiplier MUL_LOOP: andi a1, a5, 1 beq a1, x0, MUL_SKIP_ADD add t6, t6, a4 MUL_SKIP_ADD: slli a4, a4, 1 srli a5, a5, 1 bnez a5, MUL_LOOP # result exponent add t5, t0, t1 addi t5, t5, -127 add t5, t5, a2 # normalize product li a1, 0x8000 and a1, a1, t6 beq a1, x0, MUL_SHIFT7 srli t6, t6, 8 addi t5, t5, 1 j MUL_PACK MUL_SHIFT7: srli t6, t6, 7 MUL_PACK: # overflow li a1, 0xFF bge t5, a1, MUL_RET_INF # underflow bge t5, x0, MUL_PACK_OK j MUL_RET_ZERO MUL_PACK_OK: andi t6, t6, 0x7F slli t5, t5, 7 slli a3, a3, 15 or a0, t5, t6 or a0, a0, a3 ret # mul specials MUL_A_SPECIAL: bnez t2, MUL_RET_A # a is nan or t5, t1, t3 beq t5, x0, MUL_RET_NAN # inf times zero -> nan j MUL_RET_INF MUL_B_SPECIAL: bnez t3, MUL_RET_B # b is nan or t5, t0, t2 beq t5, x0, MUL_RET_NAN # zero times inf -> nan j MUL_RET_INF MUL_RET_NAN: li a0, 0x7FC0 ret MUL_RET_INF: slli a3, a3, 15 li t0, 0x7F80 or a0, a3, t0 ret MUL_RET_A: ret MUL_RET_B: mv a0, a1 ret MUL_RET_ZERO: li a0, 0 ret # ============================================================ # bf16_sqrt a0=x_bits -> a0 result # demo covers c=0x4080 -> 0x4000 # ============================================================ bf16_sqrt: li t0, 0x4080 bne a0, t0, SQRT_ZERO li a0, 0x4000 ret SQRT_ZERO: li a0, 0 ret # ============================================================ # data # ============================================================ .data .align 2 arr_bf16: .half 0x3FC0 # a .half 0x4020 # b .half 0x4080 # c .half 0x0000 # out sum .half 0x0000 # out mul .half 0x0000 # out sqrt exp_add: .half 0x4080 exp_mul: .half 0x4070 exp_sqrt: .half 0x4000 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 generate (RISC-V 32bits gcc 13.1.0) :::spoiler Expand for details Due to HackMD’s character limit, I can’t include the code here—please see the [GitHub link](https://github.com/daoxuewu/ca2025-homework1/blob/main/Problem_C/ProbC_compiler_version.s). ::: ## Optimize [LeetCode Problem #393. UTF-8 Validation](https://leetcode.com/problems/utf-8-validation/) using CLZ to Improve Runtime Performance ### Description > Given an integer array `data`, where each element is in the range [0, 255] and represents one byte, determine whether the sequence encodes a valid UTF-8 string. > > ### Encoding rules > A UTF-8 character uses 1 to 4 bytes. The first byte determines the length: > > - `0xxxxxxx` → 1 byte character > - `110xxxxx` → start of a 2-byte character > - `1110xxxx` → start of a 3-byte character > - `11110xxx` → start of a 4-byte character > > All continuation bytes must have the form `10xxxxxx`. > > The sequence is valid only if every character has the correct number of continuation bytes and the entire array is consumed without leftovers. > > Return `true` if the entire array can be parsed into valid UTF-8 characters; otherwise, return `false`. > >### Examples > >**Example 1** Input: `data = [197, 130, 1]` Output: `true` Explanation: `11000101 10000010` forms a 2-byte character, followed by a valid 1-byte character. > >**Example 2** Input: `data = [235, 140, 4]` Output: `false` Explanation: The third byte does not start with `10`, so it is not a valid continuation byte. ### C code ```c= #include <stdbool.h> #include <stdint.h> #include <stdio.h> /* Count leading ones of an 8-bit byte b in a branchless way: lead1(b) = clz32( ~ ( (uint32_t)b << 24 ) ) Proof idea: - Shift b to the top byte, then bitwise NOT. - For b with k leading ones (in its top byte position), the NOT value starts with k zeros in the 32-bit representation. - Therefore clz32 yields exactly k. Note: __builtin_clz(0) is undefined, but y = ~(b << 24) is never zero for any b, so this use is safe. */ static inline int lead1_u8(uint8_t b) { uint32_t y = ~((uint32_t)b << 24); return __builtin_clz(y); // returns 0..8 for any 0..255 byte } /* LeetCode 393: UTF-8 Validation - need: how many continuation bytes are still required. - For a leading byte: valid lead1 counts are 0 (ASCII), 2, 3, 4. 1 or >=5 are invalid. - For continuation bytes, the byte must match 10xxxxxx. */ bool validUtf8(int* data, int dataSize) { int need = 0; // remaining continuation bytes to consume for (int i = 0; i < dataSize; ++i) { uint8_t b = (uint8_t)data[i]; if (need == 0) { int l1 = lead1_u8(b); // number of leading 1s in the leading byte if (l1 == 0) continue; // ASCII fast path if (l1 == 1 || l1 > 4) return false; // invalid header lengths need = l1 - 1; } else { // Continuation byte must be 10xxxxxx if ((b & 0xC0u) != 0x80u) return false; --need; } } return need == 0; } /* ------------------------- Local test harness: main ------------------------- */ int main(void) { struct Case { const char* name; const uint8_t* data; int len; int expect; // 1 for valid, 0 for invalid } cases[] = { // Examples consistent with typical LC tests { "T1 valid 2B then ASCII", (const uint8_t[]){197, 130, 1}, 3, 1 }, { "T2 invalid third not cont", (const uint8_t[]){235, 140, 4}, 3, 0 }, { "T3 valid 4B", (const uint8_t[]){240, 162, 138, 147}, 4, 1 }, { "T4 invalid starts with 10", (const uint8_t[]){145}, 1, 0 }, { "T5 valid ASCII", (const uint8_t[]){0}, 1, 1 }, }; int ok = 1; int T = (int)(sizeof(cases) / sizeof(cases[0])); for (int i = 0; i < T; ++i) { // Convert const uint8_t* to int* buffer expected by LC signature int buf[8]; // sufficient for the small samples above for (int j = 0; j < cases[i].len; ++j) buf[j] = cases[i].data[j]; int got = validUtf8(buf, cases[i].len) ? 1 : 0; if (got != cases[i].expect) { printf("%s : FAIL got=%d expect=%d\n", cases[i].name, got, cases[i].expect); ok = 0; } else { printf("%s : OK\n", cases[i].name); } } return ok ? 0 : 1; } ``` ### RV32I Assembly code By replacing the per-byte leading-ones detection (an `if`/`while` chain with up to **4 data-dependent branches**) with a **branchless CLZ** computed as `clz(~(b<<24))` using ~**25 straight-line RV32I ops**, the header parse moves from **O(k)** branching (**k ≤ 4**, up to **4 misprediction sites per leading byte**) to **O(1)** branchless execution with **zero** misprediction sites and a fixed instruction cost. :::spoiler Expand for details ```asm= .text .globl _start _start: # jump to main then exit jal ra, main li a0, 0 li a7, 10 ecall .globl main # ============================================================ # main # run five test cases # print OK or FAIL for each # exit with code 0 # ============================================================ main: # T1 la a0, t1_data li a1, 3 li s5, 1 # expect = 1 jal ra, utf8_validate bne a0, s5, T1_FAIL la a0, msg_t1_ok li a7, 4 ecall j T2_RUN T1_FAIL: la a0, msg_t1_fail li a7, 4 ecall T2_RUN: la a0, t2_data li a1, 3 li s5, 0 jal ra, utf8_validate bne a0, s5, T2_FAIL la a0, msg_t2_ok li a7, 4 ecall j T3_RUN T2_FAIL: la a0, msg_t2_fail li a7, 4 ecall T3_RUN: la a0, t3_data li a1, 4 li s5, 1 jal ra, utf8_validate bne a0, s5, T3_FAIL la a0, msg_t3_ok li a7, 4 ecall j T4_RUN T3_FAIL: la a0, msg_t3_fail li a7, 4 ecall T4_RUN: la a0, t4_data li a1, 1 li s5, 0 jal ra, utf8_validate bne a0, s5, T4_FAIL la a0, msg_t4_ok li a7, 4 ecall j T5_RUN T4_FAIL: la a0, msg_t4_fail li a7, 4 ecall T5_RUN: la a0, t5_data li a1, 1 li s5, 1 jal ra, utf8_validate bne a0, s5, T5_FAIL la a0, msg_t5_ok li a7, 4 ecall j DONE T5_FAIL: la a0, msg_t5_fail li a7, 4 ecall DONE: li a0, 0 li a7, 10 ecall # ============================================================ # utf8_validate # a0 ptr to bytes # a1 length # return a0 = 1 if valid else 0 # uses clz_branchless to get lead1 = clz( ~ (b << 24) ) # ============================================================ utf8_validate: addi sp, sp, -16 sw ra, 12(sp) sw s0, 8(sp) # need sw s1, 4(sp) # ptr sw s2, 0(sp) # remain mv s1, a0 # ptr mv s2, a1 # len li s0, 0 # need = 0 UV_LOOP: beq s2, x0, UV_END lbu t0, 0(s1) # load byte b addi s1, s1, 1 addi s2, s2, -1 beq s0, x0, UV_LEAD # no pending continuation # must be continuation 10xxxxxx andi t1, t0, 0xC0 li t2, 0x80 bne t1, t2, UV_FAIL addi s0, s0, -1 j UV_LOOP UV_LEAD: # lead1 = clz( ~ (b << 24) ) slli t1, t0, 24 xori t1, t1, -1 # bitwise not mv a0, t1 jal ra, clz_branchless # a0 = clz32 mv t2, a0 # lead1 beq t2, x0, UV_LOOP # ASCII li t3, 1 beq t2, t3, UV_FAIL # lead1 == 1 invalid li t3, 5 sltu t4, t2, t3 # t2 < 5 beq t4, x0, UV_FAIL # lead1 >= 5 invalid addi s0, t2, -1 # need = lead1 - 1 j UV_LOOP UV_END: beq s0, x0, UV_OK UV_FAIL: li a0, 0 j UV_RET UV_OK: li a0, 1 UV_RET: lw ra, 12(sp) lw s0, 8(sp) lw s1, 4(sp) lw s2, 0(sp) addi sp, sp, 16 ret # ============================================================ # clz_branchless # a0 = x # return a0 = count leading zeros of 32 bit x # branchless binary search steps 16 8 4 2 1 # clz 0 returns 32 # ============================================================ clz_branchless: li t0, 32 mv t1, a0 srli t2, t1, 16 sltu t3, x0, t2 slli t4, t3, 4 srl t1, t1, t4 sub t0, t0, t4 srli t2, t1, 8 sltu t3, x0, t2 slli t4, t3, 3 srl t1, t1, t4 sub t0, t0, t4 srli t2, t1, 4 sltu t3, x0, t2 slli t4, t3, 2 srl t1, t1, t4 sub t0, t0, t4 srli t2, t1, 2 sltu t3, x0, t2 slli t4, t3, 1 srl t1, t1, t4 sub t0, t0, t4 srli t2, t1, 1 sltu t3, x0, t2 mv t4, t3 srl t1, t1, t4 sub t0, t0, t4 sub a0, t0, t1 ret # ============================================================ # data # ============================================================ .data .align 4 t1_data: .byte 197,130,1 t2_data: .byte 235,140,4 t3_data: .byte 240,162,138,147 t4_data: .byte 145 t5_data: .byte 0 msg_t1_ok: .asciz "T1 OK\n" msg_t1_fail: .asciz "T1 FAIL\n" msg_t2_ok: .asciz "T2 OK\n" msg_t2_fail: .asciz "T2 FAIL\n" msg_t3_ok: .asciz "T3 OK\n" msg_t3_fail: .asciz "T3 FAIL\n" msg_t4_ok: .asciz "T4 OK\n" msg_t4_fail: .asciz "T4 FAIL\n" msg_t5_ok: .asciz "T5 OK\n" msg_t5_fail: .asciz "T5 FAIL\n" ``` ::: *** Compiler generate (RISC-V 32bits gcc 13.1.0) :::spoiler Expand for details ```asm= lead1_u8(unsigned char): addi sp,sp,-48 sw ra,44(sp) sw s0,40(sp) addi s0,sp,48 mv a5,a0 sb a5,-33(s0) lbu a5,-33(s0) slli a5,a5,24 not a5,a5 sw a5,-20(s0) lw a0,-20(s0) call __clzsi2 mv a5,a0 mv a0,a5 lw ra,44(sp) lw s0,40(sp) addi sp,sp,48 jr ra validUtf8(int*, int): addi sp,sp,-48 sw ra,44(sp) sw s0,40(sp) addi s0,sp,48 sw a0,-36(s0) sw a1,-40(s0) sw zero,-20(s0) sw zero,-24(s0) j .L4 .L12: lw a5,-24(s0) slli a5,a5,2 lw a4,-36(s0) add a5,a4,a5 lw a5,0(a5) sb a5,-25(s0) lw a5,-20(s0) bne a5,zero,.L5 lbu a5,-25(s0) mv a0,a5 call lead1_u8(unsigned char) sw a0,-32(s0) lw a5,-32(s0) beq a5,zero,.L13 lw a4,-32(s0) li a5,1 beq a4,a5,.L8 lw a4,-32(s0) li a5,4 ble a4,a5,.L9 .L8: li a5,0 j .L10 .L9: lw a5,-32(s0) addi a5,a5,-1 sw a5,-20(s0) j .L7 .L5: lbu a5,-25(s0) andi a4,a5,192 li a5,128 beq a4,a5,.L11 li a5,0 j .L10 .L11: lw a5,-20(s0) addi a5,a5,-1 sw a5,-20(s0) j .L7 .L13: nop .L7: lw a5,-24(s0) addi a5,a5,1 sw a5,-24(s0) .L4: lw a4,-24(s0) lw a5,-40(s0) blt a4,a5,.L12 lw a5,-20(s0) seqz a5,a5 andi a5,a5,0xff .L10: mv a0,a5 lw ra,44(sp) lw s0,40(sp) addi sp,sp,48 jr ra .LC7: .string "%s : FAIL got=%d expect=%d\n" .LC8: .string "%s : OK\n" .LC0: .string "T1 valid 2B then ASCII" .LC1: .string "T2 invalid third not cont" .LC2: .string "T3 valid 4B" .LC3: .string "T4 invalid starts with 10" .LC4: .string "T5 valid ASCII" .LC6: .word .LC0 .word ._anon_3 .word 3 .word 1 .word .LC1 .word ._anon_4 .word 3 .word 0 .word .LC2 .word ._anon_5 .word 4 .word 1 .word .LC3 .word ._anon_6 .word 1 .word 0 .word .LC4 .word ._anon_7 .word 1 .word 1 main: addi sp,sp,-160 sw ra,156(sp) sw s0,152(sp) addi s0,sp,160 lui a5,%hi(.LC6) addi a4,a5,%lo(.LC6) addi a5,s0,-116 mv a3,a4 li a4,80 mv a2,a4 mv a1,a3 mv a0,a5 call memcpy li a5,1 sw a5,-20(s0) li a5,5 sw a5,-32(s0) sw zero,-24(s0) j .L15 .L22: sw zero,-28(s0) j .L16 .L17: lw a5,-24(s0) slli a5,a5,4 addi a5,a5,-16 add a5,a5,s0 lw a4,-96(a5) lw a5,-28(s0) add a5,a4,a5 lbu a5,0(a5) mv a4,a5 lw a5,-28(s0) slli a5,a5,2 addi a5,a5,-16 add a5,a5,s0 sw a4,-132(a5) lw a5,-28(s0) addi a5,a5,1 sw a5,-28(s0) .L16: lw a5,-24(s0) slli a5,a5,4 addi a5,a5,-16 add a5,a5,s0 lw a5,-92(a5) lw a4,-28(s0) blt a4,a5,.L17 lw a5,-24(s0) slli a5,a5,4 addi a5,a5,-16 add a5,a5,s0 lw a4,-92(a5) addi a5,s0,-148 mv a1,a4 mv a0,a5 call validUtf8(int*, int) mv a5,a0 beq a5,zero,.L18 li a5,1 j .L19 .L18: li a5,0 .L19: sw a5,-36(s0) lw a5,-24(s0) slli a5,a5,4 addi a5,a5,-16 add a5,a5,s0 lw a5,-88(a5) lw a4,-36(s0) beq a4,a5,.L20 lw a5,-24(s0) slli a5,a5,4 addi a5,a5,-16 add a5,a5,s0 lw a4,-100(a5) lw a5,-24(s0) slli a5,a5,4 addi a5,a5,-16 add a5,a5,s0 lw a5,-88(a5) mv a3,a5 lw a2,-36(s0) mv a1,a4 lui a5,%hi(.LC7) addi a0,a5,%lo(.LC7) call printf sw zero,-20(s0) j .L21 .L20: lw a5,-24(s0) slli a5,a5,4 addi a5,a5,-16 add a5,a5,s0 lw a5,-100(a5) mv a1,a5 lui a5,%hi(.LC8) addi a0,a5,%lo(.LC8) call printf .L21: lw a5,-24(s0) addi a5,a5,1 sw a5,-24(s0) .L15: lw a4,-24(s0) lw a5,-32(s0) blt a4,a5,.L22 lw a5,-20(s0) seqz a5,a5 andi a5,a5,0xff mv a0,a5 lw ra,156(sp) lw s0,152(sp) addi sp,sp,160 jr ra ._anon_3: .byte -59 .byte -126 .byte 1 ._anon_4: .byte -21 .byte -116 .byte 4 ._anon_5: .byte -16 .byte -94 .byte -118 .byte -109 ._anon_6: .byte -111 ._anon_7: .zero 1 ``` ::: ## Reference :::warning This assignment was completed with assistance from [ChatGPT](https://chatgpt.com/) for grammar checking, translation and initial brainstorming. All final analysis and conclusions are my own. ::: - [How to Write a Git Commit Message](https://cbea.ms/git-commit/) - [Guidelines for Student Use of AI Tools](https://hackmd.io/@sysprog/arch2025-ai-guidelines) - [Leetcode 393. UTF-8 Validation ( Medium )](https://leetcode.com/problems/utf-8-validation/) - [2025 NCKU computer architecture assignment 1: RISC-V Assembly and Instruction Pipeline](https://hackmd.io/@sysprog/2025-arch-homework1) - [Lab1: RV32I Simulator](https://hackmd.io/@sysprog/H1TpVYMdB)