# Assignment 1: RISC-V Assembly and Instruction Pipeline Contributed by [yuyan7498](https://github.com/yuyan7498) Code: [Github repository](https://github.com/yuyan7498/ca2025-quizzes) This repository contains three folders: * `problem_B_and_c` — RISC-V assembly implementations for Quiz 1, Problem B and Problem C, including both the program sources and their tests. * `next_power_of_two` — clz implementation: Compute the smallest power of two greater than or equal to a 32-bit unsigned integer * `dist_2_points` — bfloat16 implementation: Calculate two-Point minimum distance * * `leetcode231_power_of_two` — extra practice ## Problem B UF8 ⇄ uint32 This write-up explains the overall test harness (main) and the three core routines—`decode`, `encode`, and `clz32`—with design ideas, key code snippets, and quick correctness intuition. ### Target & Data Format UF8: an 8-bit custom integer encoding, `eeee mmmm` - High 4 bits `e` = exponent - Low 4 bits `m` = mantissa Definition: ``` value = ((0x7FFF >> (15 - e)) << 4) + (m << e) = 16 * (2^e - 1) + m * 2^e ``` with `e ∈ [0,15]`, `m ∈ [0,15]`. ### clz32 - Goal: return the number of leading zeros of a 32-bit unsigned integer. - Idea: binary search over 16/8/4/2/1 bit chunks; if the upper chunk is non-zero, shift down and accumulate the bit width. If `a0=0`, return `32`. #### C code ```c static inline unsigned clz(uint32_t x) { int n = 32, c = 16; do { uint32_t y = x >> c; if (y) { n -= c; x = y; } c >>= 1; } while (c); return n - x; } ``` #### Assembly code ```asm # ------------------------------------------------------------ # clz32 # a0=x; returns clz(x). Uses t0=n, t1=c, t2=y # ------------------------------------------------------------ clz32: li t0, 32 # n = 32 li t1, 16 # c = 16 1: srl t2, a0, t1 # y = x >> c beq t2, x0, 2f # if (!y) skip update sub t0, t0, t1 # n -= c mv a0, t2 # x = y 2: srli t1, t1, 1 # c >>= 1 bnez t1, 1b # while (c) sub a0, t0, a0 # return n - x ret ``` ### decode Recall: ``` value = 16 * (2^e - 1) + m * 2^e ``` #### C Code ```c /* 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; return (mantissa << exponent) + offset; } ``` #### Assembly Code ```asm decode: andi t0, a0, 0x0f # m srli t1, a0, 4 # e li t2, 15 sub t2, t2, t1 # 15 - e li t3, 0x7FFF srl t3, t3, t2 # 0x7FFF >> (15 - e) = (2^e - 1) slli t3, t3, 4 # *16 => offset sll t4, t0, t1 # m << e add a0, t4, t3 # value ret ``` #### Example (0x52: e=5, m=2) - `offset = 16*(2^5 − 1) = 496` - `m<<e = 2*32 = 64` - `value = 560` --- ### encode Goal: for a given `value`, find `e, m` s.t. ``` value = 16*(2^e − 1) + m*2^e, with e,m ∈ [0,15]. ``` #### Algorithm steps 1) Small fast path: if `value < 16` ⇒ `e=0`, `m=value` (return value). 2) Initial `e` guess: - `msb = 31 - clz32(value)` → estimate `e ≈ msb - 4` (since `m` is 4-bit). Clamp to `[0,15]`. 3) Build `overflow = 16*(2^e − 1)`: - Start from 0, loop `e` times with `overflow = (overflow<<1) + 16`. 4) Adjust `e` downward if needed: - While `e>0 && value < overflow`: rollback one level - `overflow = (overflow - 16) >> 1`, `e--`. 5) Adjust `e` upward if possible: - If `value ≥ next_overflow = (overflow<<1)+16` and `e<15`, then promote: - `overflow = next_overflow`, `e++`. 6) Solve `m`: `m = (value - overflow) >> e`. 7) Pack: `(e<<4) | m`. #### C code ```c /* 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) { /* Estimate exponent - the formula is empirical */ exponent = msb - 4; if (exponent > 15) exponent = 15; /* Calculate overflow for estimated exponent */ for (uint8_t e = 0; e < exponent; e++) 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; return (exponent << 4) | mantissa; } ``` #### Assembly code ```asm encode: # save ra/s0 addi sp, sp, -16 sw ra, 12(sp) sw s0, 8(sp) mv s0, a0 # value # 1) small fast path li t0, 16 bltu s0, t0, EncodeRetShort # 2) e ≈ msb - 4 (msb = 31 - clz) mv a0, s0 jal ra, clz32 li t5, 31 sub t5, t5, a0 # msb addi t1, t5, -4 # e li t6, 15 bgtu t1, t6, ClipTo15 NoClip: # 3) overflow = 16*(2^e - 1) li t2, 0 li t3, 0 ForELoop: beq t3, t1, ForEDone slli t2, t2, 1 addi t2, t2, 16 addi t3, t3, 1 j ForELoop ForEDone: # 4) adjust down if value < overflow AdjLoop: beqz t1, AdjDone bltu s0, t2, AdjStep j AdjDone AdjStep: addi t2, t2, -16 srli t2, t2, 1 addi t1, t1, -1 j AdjLoop AdjDone: # 5) adjust up if value ≥ next_overflow WhileLoop: li t6, 15 beq t1, t6, WhileDone slli t3, t2, 1 addi t3, t3, 16 # next_overflow bltu s0, t3, WhileDone mv t2, t3 addi t1, t1, 1 j WhileLoop WhileDone: # 6) m = (value - overflow) >> e sub t3, s0, t2 srl t3, t3, t1 # 7) (e<<4) | m slli t4, t1, 4 or a0, t4, t3 j EncodeRet EncodeRetShort: mv a0, s0 # e=0, m=value EncodeRet: lw ra, 12(sp) lw s0, 8(sp) addi sp, sp, 16 ret ClipTo15: li t1, 15 j NoClip ``` #### Why m ∈ [0,15] holds After adjustment, `overflow ≤ value < next_overflow`, where `next_overflow = overflow + 16*2^e`. Let `Δ = value − overflow`, then `0 ≤ Δ < 16*2^e`. Thus `m = (Δ >> e)` ⇒ `0 ≤ m < 16`. #### Example (`value=560`) - `clz32(560)=22` → `msb=9` → estimate `e=5` - `overflow = 16*(2^5−1)=496`, `next_overflow=496+512=1008` - `overflow ≤ 560 < next_overflow` (no further adjust) - `m = (560−496)>>5 = 64>>5 = 2` - result `(e<<4)|m = 0x52` #### result ![image](https://hackmd.io/_uploads/BJr85Dqaex.png) ![image](https://hackmd.io/_uploads/r1ND9w9pxg.png) ![image](https://hackmd.io/_uploads/r1yOqD5ale.png) ![image](https://hackmd.io/_uploads/SJ6_5v9agx.png) ## Problem C — BF16 Arithmetic ### Bit Layout & Constants (bf16) - **sign**: bit 15 - **exponent**: bits 14..7 (8 bits), bias = 127 - **mantissa**: bits 6..0 (7 bits) #### Key masks & special values: - `BF16_SIGN_MASK = 0x8000` - `BF16_EXP_MASK = 0x7F80` - `BF16_MANT_MASK = 0x007F` - `+Inf = 0x7F80`, `-Inf = 0xFF80`, `NaN (canonical) = 0x7FC0` - `+0 = 0x0000`, `-0 = 0x8000` --- ### bf16_isnan **Idea**: In IEEE formats, exp=all-ones means NaN or Inf; mantissa≠0 ⇒ NaN. #### C code ```c static inline bool bf16_isnan(bf16_t a) { return ((a.bits & BF16_EXP_MASK) == BF16_EXP_MASK) && (a.bits & BF16_MANT_MASK); } ``` #### Assembly code ```asm # return ((bits & 0x7F80)==0x7F80) && (bits & 0x007F) bf16_isnan: li t2, 0x7F80 and t0, a0, t2 bne t0, t2, Lnan_false li t3, 0x007F and t1, a0, t3 beq t1, x0, Lnan_false li a0, 1 ret Lnan_false: mv a0, x0 ret ``` --- ### bf16_isinf **Idea**: exp=all-ones and mantissa=0 ⇒ Inf. #### C code ```c static inline bool bf16_isinf(bf16_t a) { return ((a.bits & BF16_EXP_MASK) == BF16_EXP_MASK) && !(a.bits & BF16_MANT_MASK); } ``` #### Assembly code ```asm # return ((bits & 0x7F80)==0x7F80) && !(bits & 0x007F) bf16_isinf: li t2, 0x7F80 and t0, a0, t2 bne t0, t2, Linf_false li t3, 0x007F and t1, a0, t3 bne t1, x0, Linf_false li a0, 1 ret Linf_false: mv a0, x0 ret ``` --- ### bf16_iszero **Idea**: ±0 are zeros; ignore sign bit. #### C code ```c static inline bool bf16_iszero(bf16_t a) { return !(a.bits & 0x7FFF); } ``` #### Assembly code ```asm # return !(bits & 0x7FFF) bf16_iszero: li t0, 0x7FFF and t0, a0, t0 bne t0, x0, Lzero_false li a0, 1 ret Lzero_false: mv a0, x0 ret ``` --- ### f32_to_bf16 (RNE, ties-to-even) #### Idea: - If exp==0xFF (Inf/NaN), pass-through high16 (preserves NaN payload). - Else round-to-nearest-even (RNE) by +0x7FFF and +(high16&1) before chopping. #### C code ```c 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; // RNE ties-to-even return (bf16_t){ .bits = f32bits >> 16 }; } ``` #### Assembly code ```asm # if exp==0xFF: return high16(f32bits) # else: RNE by adding ((high16&1)+0x7FFF), then return high16 f32_to_bf16: srli t0, a0, 23 andi t0, t0, 0xFF li t1, 0xFF beq t0, t1, Lfte_special srli t2, a0, 16 # high16 andi t2, t2, 1 # (high16 & 1) li t3, 0x7FFF add t2, t2, t3 # increment for RNE add a0, a0, t2 srli a0, a0, 16 ret Lfte_special: srli a0, a0, 16 andi a0, a0, 0xFFFF ret ``` --- ### bf16_to_f32 **Idea**: Widen: place bf16 in the high 16 bits of a float's 32-bit layout. #### C code ```c static inline float bf16_to_f32(bf16_t val) { uint32_t f32bits = ((uint32_t)val.bits) << 16; float out; memcpy(&out, &f32bits, sizeof(out)); return out; } ``` #### Assembly code ```asm # return ((uint32_t)bits)<<16 bf16_to_f32: andi a0, a0, 0xFFFF slli a0, a0, 16 ret ``` --- ### bf16_add / bf16_sub - Special cases first: NaN propagation; Inf rules; zeros shortcuts. - Align exponents: shift the smaller mant right; large gaps short-circuit. - Same sign: add mant, handle carry (>>1; exp++), overflow ⇒ ±Inf. - Different signs: subtract big–small; if zero ⇒ signed zero; else left-normalize until hidden-1. - Pack: (sign<<15) | ((exp&0xFF)<<7) | (mant&0x7F). #### C code ```c static inline bf16_t bf16_add(bf16_t a, bf16_t b) { uint16_t sa=(a.bits>>15)&1, sb=(b.bits>>15)&1; int16_t ea=(a.bits>>7)&0xFF, eb=(b.bits>>7)&0xFF; uint16_t ma=a.bits&0x7F, mb=b.bits&0x7F; if (ea==0xFF) { if (ma) return a; if (eb==0xFF) return (mb||sa==sb)? b: BF16_NAN(); return a; } if (eb==0xFF) return b; if (!ea && !ma) return b; if (!eb && !mb) return a; if (ea) ma|=0x80; if (eb) mb|=0x80; int16_t ed=ea-eb, re; uint16_t rs; uint32_t rm; if (ed>0){ re=ea; if (ed>8) return a; mb >>= ed; } else if (ed<0){ re=eb; if (ed<-8) return b; ma >>= -ed; } else re=ea; if (sa==sb){ rs=sa; rm=(uint32_t)ma+mb; if (rm&0x100){ rm>>=1; if (++re>=0xFF) return (bf16_t){.bits=(rs<<15)|0x7F80}; } } else { if (ma>=mb){ rs=sa; rm=ma-mb; } else { rs=sb; rm=mb-ma; } if (!rm) return BF16_ZERO(); while (!(rm&0x80)){ rm<<=1; if (--re<=0) return BF16_ZERO(); } } return (bf16_t){ .bits=(rs<<15)|((re&0xFF)<<7)|(rm&0x7F) }; } static inline bf16_t bf16_sub(bf16_t a, bf16_t b) { b.bits ^= BF16_SIGN_MASK; // flip sign return bf16_add(a, b); } ``` #### Assembly code ```asm # Extract fields srli t0, a0, 15; andi t0, t0, 1 # sign_a srli t1, a1, 15; andi t1, t1, 1 # sign_b srli t2, a0, 7; andi t2, t2, 0xFF # exp_a srli t3, a1, 7; andi t3, t3, 0xFF # exp_b andi t4, a0, 0x7F # mant_a andi t5, a1, 0x7F # mant_b # Specials (NaN/Inf/0) — see full routine # ... # Align exponents sub a2, t2, t3 beq a2, x0, 1f blt x0, a2, gt_path # exp_a > exp_b # exp_a < exp_b neg a3, a2 li t6, 8 blt t6, a3, ret_b srl t4, t4, a3 # mant_a >>= -diff mv t6, t3 # result_exp=exp_b j aligned gt_path: li t6, 8 blt t6, a2, ret_a srl t5, t5, a2 # mant_b >>= diff mv t6, t2 aligned: # Same sign → add → normalize on carry # Diff sign → subtract → left-normalize until hidden 1 # ... # Pack slli a3, a3, 15 andi a5, t6, 0xFF; slli a5, a5, 7 andi a4, a4, 0x7F or a3, a3, a5 or a0, a3, a4 ret ``` **Rounding note**: the final mantissa is truncated to 7 bits (no GRS), i.e. fraction rounding is effectively toward-zero. To switch to RNE, add guard/round/sticky before packing. --- ### bf16_mul - Handle NaN/Inf/0 first (Inf*0 ⇒ NaN etc.). - Normalize mantissas: add hidden-1 for normals; left-normalize subnormals and track an exponent adjust. - Do integer 8×8 multiply (shift-add). - Exponent: (ea + eb + subnorm_adjust) − bias. - Normalize product: choose >>8 or >>7 to land 7-bit mantissa; bump exp on >>8. - Overflow ⇒ ±Inf; underflow ⇒ ±0 (or denormal if near). #### C code ```c static inline bf16_t bf16_mul(bf16_t a, bf16_t b) { uint16_t sa=(a.bits>>15)&1, sb=(b.bits>>15)&1; int16_t ea=(a.bits>>7)&0xFF, eb=(b.bits>>7)&0xFF; uint16_t ma=a.bits&0x7F, mb=b.bits&0x7F; uint16_t rs = sa ^ sb; if (ea==0xFF){ if (ma) return a; if (!eb && !mb) return BF16_NAN(); return (bf16_t){.bits=(rs<<15)|0x7F80}; } if (eb==0xFF){ if (mb) return b; if (!ea && !ma) return BF16_NAN(); return (bf16_t){.bits=(rs<<15)|0x7F80}; } if ((!ea && !ma) || (!eb && !mb)) return (bf16_t){.bits=rs<<15}; int16_t adj=0; if (!ea){ while(!(ma&0x80)){ ma<<=1; adj--; } ea=1; } else ma|=0x80; if (!eb){ while(!(mb&0x80)){ mb<<=1; adj--; } eb=1; } else mb|=0x80; uint32_t prod=(uint32_t)ma*mb; int32_t re=(int32_t)ea+eb-BF16_EXP_BIAS+adj; uint32_t mm; if (prod & 0x8000){ mm=(prod>>8)&0x7F; re++; } else mm=(prod>>7)&0x7F; if (re>=0xFF) return (bf16_t){.bits=(rs<<15)|0x7F80}; if (re<=0) { if (re<-6) return (bf16_t){.bits=rs<<15}; mm >>= (1-re); re=0; } return (bf16_t){.bits=(rs<<15)|((re&0xFF)<<7)|(mm&0x7F)}; } ``` #### Assembly code ```asm # prod = mant_a * mant_b via shift-add (8 steps) mv a3, zero # prod mv a5, t6 # multiplier (mant_b) mv a2, t5 # multiplicand (mant_a) li t0, 8 Lmul_loop: andi t1, a5, 1 beqz t1, 1f add a3, a3, a2 1: slli a2, a2, 1 srli a5, a5, 1 addi t0, t0, -1 bnez t0, Lmul_loop ``` --- ### bf16_div - Normalize mantissas (add hidden-1 for normals). - Restoring division over 16 steps to form a 16-bit quotient accumulator. - Exponent: (ea − eb) + bias with subnormal adjustments. - Normalize quotient to have MSB in bit15; then take mantissa from >>8. - Overflow ⇒ ±Inf; exp<=0 ⇒ signed zero; else pack. #### C code ```c static inline bf16_t bf16_div(bf16_t a, bf16_t b) { uint16_t sa=(a.bits>>15)&1, sb=(b.bits>>15)&1; int16_t ea=(a.bits>>7)&0xFF, eb=(b.bits>>7)&0xFF; uint16_t ma=a.bits&0x7F, mb=b.bits&0x7F; uint16_t rs = sa ^ sb; if (eb==0xFF){ if (mb) return b; if (ea==0xFF && !ma) return BF16_NAN(); return (bf16_t){.bits=rs<<15}; } if (!eb && !mb){ if (!ea && !ma) return BF16_NAN(); return (bf16_t){.bits=(rs<<15)|0x7F80}; } if (ea==0xFF){ if (ma) return a; return (bf16_t){.bits=(rs<<15)|0x7F80}; } if (!ea && !ma) return (bf16_t){.bits=rs<<15}; if (ea) ma|=0x80; if (eb) mb|=0x80; uint32_t dividend=((uint32_t)ma)<<15, divisor=mb, q=0; for (int i=0;i<16;i++){ q <<= 1; if (dividend >= (divisor << (15-i))){ dividend -= (divisor << (15-i)); q |= 1; } } int32_t re = (int32_t)ea - eb + BF16_EXP_BIAS; if (!ea) re--; if (!eb) re++; if (q & 0x8000) q >>= 8; else { while (!(q & 0x8000) && re > 1){ q <<= 1; re--; } q >>= 8; } q &= 0x7F; if (re>=0xFF) return (bf16_t){.bits=(rs<<15)|0x7F80}; if (re<=0) return (bf16_t){.bits=rs<<15}; return (bf16_t){.bits=(rs<<15)|((re&0xFF)<<7)|(q&0x7F)}; } ``` #### Assembly code ```asm # quotient = (mant_a << 15) / mant_b slli a2, t4, 15 # remainder (dividend) mv a3, t5 # divisor li a4, 0 # quotient li a5, 0 # i=0 Ldiv_loop: li a7, 16 bge a5, a7, Ldiv_done slli a4, a4, 1 li a7, 15 sub a7, a7, a5 sll a7, a3, a7 bltu a2, a7, 1f sub a2, a2, a7 ori a4, a4, 1 1: addi a5, a5, 1 j Ldiv_loop Ldiv_done: ``` --- ### bf16_sqrt - Specials: sqrt(NaN)=NaN, sqrt(+Inf)=+Inf, sqrt(-Inf)=NaN, sqrt(±0)=0, negative finite ⇒ NaN. - Build integer mantissa m = 0x80 | mant. - Half the unbiased exponent: if odd, m <<= 1, new_exp=((e-1)/2)+bias; else new_exp=(e/2)+bias. - Binary search integer root result s.t. ((result*result)>>7) <= m (use shift-add multiply). - Normalize result into [128, 255]; adjust new_exp. - Overflow/underflow checks; pack. #### C code ```c static inline bf16_t bf16_sqrt(bf16_t a) { uint16_t s=(a.bits>>15)&1; int16_t e=(a.bits>>7)&0xFF; uint16_t m=a.bits&0x7F; if (e==0xFF){ if (m) return a; if (s) return BF16_NAN(); return a; } if (!e && !m) return BF16_ZERO(); if (s) return BF16_NAN(); if (!e) return BF16_ZERO(); int32_t ue = e - BF16_EXP_BIAS, new_exp; uint32_t mm = 0x80 | m; // [128,256) if (ue & 1){ mm <<= 1; new_exp=((ue-1)>>1)+BF16_EXP_BIAS; } else new_exp=(ue>>1)+BF16_EXP_BIAS; uint32_t low=90, high=256, result=128; while (low <= high){ uint32_t mid=(low+high)>>1; uint32_t sq=(mid*mid)/128; // >>7 if (sq <= mm){ result=mid; low=mid+1; } else high=mid-1; } 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}; } ``` #### Assembly code ```asm # m in s2, new_exp in s3; result in t5 li s4, 90 # low li t6, 256 # high li t5, 128 # result sqrt_bsearch_loop: bgt s4, t6, sqrt_bsearch_done add t0, s4, t6 srai t0, t0, 1 # mid # sq = (mid*mid)>>7 via shift-add multiply mv a2, t0; mv a3, t0 li t2, 0; li s5, 16 sqrt_imul_loop: andi a4, a3, 1 beqz a4, 1f add t2, t2, a2 1: slli a2, a2, 1 srli a3, a3, 1 addi s5, s5, -1 bnez s5, sqrt_imul_loop mv t1, t2 srli t1, t1, 7 # sq bgt t1, s2, sq_gt_m mv t5, t0 # result = mid addi s4, t0, 1 # low = mid+1 j sqrt_bsearch_loop sq_gt_m: addi t6, t0, -1 # high = mid-1 j sqrt_bsearch_loop sqrt_bsearch_done: ``` --- ### Correctness & Trade-offs #### Rounding: - f32_to_bf16 implements RNE (ties-to-even). - Arithmetic ops truncate to 7-bit mantissa (no G/R/S), effectively toward-zero. ### Result ![image](https://hackmd.io/_uploads/SyviWF5Tel.png) ![image](https://hackmd.io/_uploads/Hyth-Y5age.png) ![image](https://hackmd.io/_uploads/Hk06bt5pxg.png) ![image](https://hackmd.io/_uploads/BywbzF9pgl.png) ![image](https://hackmd.io/_uploads/BkPmzYc6ee.png) ![image](https://hackmd.io/_uploads/BkwPGY96ex.png) ![image](https://hackmd.io/_uploads/HJuFMt5Txl.png) ![image](https://hackmd.io/_uploads/r1t9zK56xe.png) ![image](https://hackmd.io/_uploads/BJ_sfK5all.png) --- ## Problem Description: Next Power of Two Compute the smallest power of two greater than or equal to a 32-bit unsigned integer `x`. Return that power of two as a `uint32_t`. This operation shows how `clz` (count-leading-zeros) turns a bit-level insight into a constant-time primitive used in memory allocators, GPU buffers, ring buffers, and alignment routines. We’ll reuse the `clz32` routine from Quiz 1 and show a clean RV32I implementation calling it. All implementations in this article were carried out using the Ripes simulator with a 5-stage pipeline design. ### Concept To find the smallest power of two ≥ `x`, observe: - `floor_log2(x) = 31 - clz(x)` (for nonzero `x`) - `next_pow2(x) = 1 << (32 - clz(x - 1))` Example mapping: | x | Binary (32-bit) | clz(x-1) | Next Power of Two | |----|------------------|----------|-------------------| | 1 | 000…0001 | 31 | 1 | | 2 | 000…0010 | 30 | 2 | | 3 | 000…0011 | 30 | 4 | | 5 | 000…0101 | 29 | 8 | | 9 | 000…1001 | 28 | 16 | | 17 | 0001…0001 | 27 | 32 | Intuition: - If `x` is already a power of two, then `x-1` fills all lower bits with `1`. - `clz(x-1)` gives the position distance to the top `1`. - Shifting `1u << (32 - clz(x-1))` lands exactly on the next power-of-two bit. ### C Implementations #### clz32 (binary search style) + next_power_of_two ```c #include <stdio.h> #include <stdint.h> static inline unsigned clz(uint32_t x) { int n = 32, c = 16; do { uint32_t y = x >> c; if (y) { n -= c; x = y; } c >>= 1; } while (c); return n - x; } // ------------------------------------------------------ // Compute Next Power of Two using CLZ (32-bit) // ------------------------------------------------------ static inline uint32_t next_power_of_two(uint32_t x) { if (x == 0u) return 1u; unsigned lz = clz(x - 1u); // clz(x-1) return 1u << (32u - lz); // 1 << (32 - clz(x-1)) } int main(void) { const uint32_t in[] = {0,1,2,3,5,9,17,33}; const uint32_t exp[] = {1,1,2,4,8,16,32,64}; const int N = (int)(sizeof(in)/sizeof(in[0])); int pass = 0; puts("=== Next Power of Two ==="); for (int i = 0; i < N; ++i) { uint32_t got = next_power_of_two(in[i]); if (got == exp[i]) { printf("PASS %u -> %u\n", in[i], got); ++pass; } else { printf("FAIL %u exp=%u got=%u\n", in[i], exp[i], got); } } printf("PASS %d/%d\n", pass, N); return 0; } ``` result: ![image](https://hackmd.io/_uploads/SyE-gE9axl.png) ![image](https://hackmd.io/_uploads/r1TWxEqagx.png) Explanation (why it works): - If `x` is power of two → `(x-1)` sets all less-significant bits to 1. - `clz(x-1)` pinpoints how far the highest `1` is from the MSB. - Shift a single `1` to that next position to get the next power of two. Examples: - `next_power_of_two(9) → 16` - `next_power_of_two(17) → 32` - `next_power_of_two(33) → 64` ### RISC-V Assembly Implementation #### clz32 (binary search style) + next_power_of_two ```asm .text .globl main # ===================================================== # main — Next Power of Two test # ===================================================== main: li s1, 0 # pass_count = 0 la a0, STR_HDR # print header li a7, 4 ecall la s2, npt_cases_in # inputs ptr la s3, npt_cases_exp # expected ptr lw s4, 0(s2) # N addi s2, s2, 4 addi s3, s3, 4 li s0, 0 # i = 0 Loop: beq s0, s4, Summary lw a0, 0(s2) # a0 = x mv s5, a0 # keep original x in s5 (callee won't clobber) jal ra, next_power_of_two mv t1, a0 # got lw t2, 0(s3) # exp bne t1, t2, PrintFail # PASS la a0, STR_PASS li a7, 4 ecall li a7, 1 # print x (from s5) mv a0, s5 ecall la a0, STR_ARROW li a7, 4 ecall li a7, 1 # print got mv a0, t1 ecall la a0, STR_NL li a7, 4 ecall addi s1, s1, 1 # pass_count++ j Cont PrintFail: la a0, STR_FAIL li a7, 4 ecall li a7, 1 mv a0, s5 # print x (from s5) ecall la a0, STR_ARROW li a7, 4 ecall li a7, 1 mv a0, t1 # print got ecall la a0, STR_NL li a7, 4 ecall Cont: addi s0, s0, 1 addi s2, s2, 4 addi s3, s3, 4 j Loop Summary: la a0, STR_PASS_WORD li a7, 4 ecall li a7, 1 mv a0, s1 ecall la a0, STR_SLASH li a7, 4 ecall li a7, 1 # print N_TOTAL la a0, N_TOTAL lw a0, 0(a0) ecall la a0, STR_NL2 li a7, 4 ecall li a7, 10 ecall # ===================================================== # next_power_of_two(x) # a0: x -> a0: next pow2 >= x # ===================================================== .globl next_power_of_two next_power_of_two: beq a0, x0, NPT_ret1 # if x==0 -> 1 mv t3, ra # save caller's ra addi a0, a0, -1 # a0 = x - 1 jal ra, clz32 # a0 = clz(x-1) mv ra, t3 # restore ra li t0, 32 sub t0, t0, a0 # t0 = 32 - clz(x-1) li a0, 1 sll a0, a0, t0 # a0 = 1 << (32 - clz(x-1)) ret NPT_ret1: li a0, 1 ret # ===================================================== # clz32(x) # a0: x -> a0: clz(x) # ===================================================== .globl clz32 clz32: li t0, 32 # n = 32 li t1, 16 # c = 16 CLZ_loop: srl t2, a0, t1 # y = x >> c beq t2, x0, CLZ_skip sub t0, t0, t1 # n -= c mv a0, t2 # x = y CLZ_skip: srli t1, t1, 1 # c >>= 1 bnez t1, CLZ_loop sub a0, t0, a0 # return n - x ret # ===================================================== # Data # ===================================================== .data N_TOTAL: .word 8 npt_cases_in: .word 8 .word 0,1,2,3,5,9,17,33 npt_cases_exp: .word 8 .word 1,1,2,4,8,16,32,64 STR_HDR: .string "=== Next Power of Two ===\n" STR_PASS: .string "PASS " STR_FAIL: .string "FAIL " STR_PASS_WORD: .string "PASS " STR_SLASH: .string "/" STR_ARROW: .string " -> " STR_NL: .string "\n" STR_NL2: .string "\n" ``` result: ![image](https://hackmd.io/_uploads/rkbCy45agg.png) ![image](https://hackmd.io/_uploads/HyT01Eq6xg.png) --- #### Test Data Example ```asm .data npt_cases_in: .word 8 .word 0,1,2,3,5,9,17,33 npt_cases_exp: .word 8 .word 1,1,2,4,8,16,32,64 ``` --- #### Running in Ripes Simulator (RV32I, 5-stage) ![Screenshot 2025-10-12 153249](https://hackmd.io/_uploads/B1PnKAuTxx.png) Expected Console Output: ```text === Next Power of Two === PASS 0 -> 1 PASS 1 -> 1 PASS 2 -> 2 PASS 3 -> 4 PASS 5 -> 8 PASS 9 -> 16 PASS 17 -> 32 PASS 33 -> 64 PASS 8/8 ``` --- #### Pipeline Visualization Consider the final shift that materializes the result: ```asm sll a0, a0, t0 # a0 = 1 << (32 - clz(x-1)) ``` - IF: PC shows the next sequential address; instruction memory outputs the `SLL` word. ![image](https://hackmd.io/_uploads/rk-WZ49Tel.png) 1. The address from which this cycle’s instruction is being read. 1. PC register value = PC_next = PC_fetch + 4 = 0x00000174 1. Fetched instruction word = 0x00551533 As derived above for sll a0, a0, t0. - ID: `rs1=a0` (initialized to 1), `rs2=t0` (shift amount), `funct3=001` (SLL), `opcode=OP`. ![image](https://hackmd.io/_uploads/S18zWVcagg.png) 1. funct7=0000000 (0)、rs2=x5 (t0=5)、rs1=x10 (a0=10)、funct3=001 (SLL)、rd=x10 (10)、opcode=0110011 (0x33) → 0x00551533 1. The Reg1 and Reg2 value is both 0x00000020, the actual shift amount for this SLL is the forwarded result of sub t0, t0, a0 (computed in the prior ALU stage) - EX: Barrel shifter applies variable shift from `t0`; result becomes the next power of two. ![image](https://hackmd.io/_uploads/S12N-E5axe.png) 1. Operand A = a0 = 0x00000001 1. The base value to be shifted. 1. Operand B = effective shift amount = (t0 & 0x1F) 1. Important: the EX forwarding mux picks up the new t0 (the output of sub t0, t0, a0 from the previous cycle). Thus, in EX you should see the correct shift distance. 1. ALU result = a0 << (t0 & 0x1F) - MEM: No data memory access for SLL. ![image](https://hackmd.io/_uploads/SJei-49pxg.png) - WB: Result written back to `a0`; no hazards if `t0` was produced in the prior ALU stage with proper forwarding. ![image](https://hackmd.io/_uploads/SkHhZ4qaee.png) 1. Destination register = rd = x10 (a0). 1. Selected value = ALU result (not PC+4). 1. Final write: a0 ← next_power_of_two(x) per the table above. --- ✅ Results Summary | Input | Expected | Computed | |-------|----------|----------| | 0 | 1 | 1 | | 1 | 1 | 1 | | 2 | 2 | 2 | | 3 | 4 | 4 | | 5 | 8 | 8 | | 9 | 16 | 16 | | 17 | 32 | 32 | | 33 | 64 | 64 | All cases PASS ✅ ### LeetCode Context Although LeetCode doesn’t have a dedicated “Next Power of Two” problem, it appears frequently inside system design and optimization tasks: | Problem ID | Title | Relation | |------------|-----------------------|-------------------------------------------------------| | 231 | Power of Two | Core idea (single-bit property) | | 707 / 622 | Design Linked List / Circular Queue | Choose capacity = next power of two to use `& (cap-1)` indexing | | 191 | Number of 1 Bits | Same bitwise family | | 50 | Pow(x, n) | Power computation & bit manipulation | Additionally, this GitHub repo contains my implementation and a few optimizations for LeetCode 231 (Power of Two). I had planned to center this write-up on that problem, but because it doesn’t make direct use of clz, I pivoted to Next Power of Two instead. Example (circular queue wrap-around optimization): ```c index = (index + 1) & (capacity - 1); // works only if capacity is a power of two ``` ### Discussion and Conclusion `next_power_of_two` is a compact, high-performance primitive built on `clz`. It converts an arbitrary size `x` to the smallest power of two ≥ `x`, which is exactly what alignment and capacity sizing need. The core idea: compute `x−1`, use `clz(x−1)` to locate the top bit, then shift 1 to that position to materialize the aligned capacity. - With hardware CLZ (e.g., Zbb), this becomes just a handful of cycles. - Even in pure RV32I, a software `clz32` plus a few ALU ops provides a fast, branch-light primitive. Common uses include: - Memory allocation (aligning heap blocks) - Graphics/compute pipelines (buffer sizing) - FFT / audio processing (rounding sample counts) ### References - [LeetCode 231: Power of Two](https://leetcode.com/problems/power-of-two/) - RISC-V ISA Manual (RV32I) - Bit Twiddling Hacks (next power of two via clz/bit-spreading) ## Two-Point Minimum Distance (BF16) ### Objective Given two point sets A and B (point coordinates stored in bf16 encoding), calculate the minimum cross-set distance: $$\min_{p \in A, q \in B} \sqrt{(q_x - p_x)^2 + (q_y - p_y)^2}$$ Output results for each test case, compare with "expected answers", and print PASS/FAIL. ### Main Flow 1. Read n_a, n_b and the starting addresses of point arrays A and B. 2. Call `find_min_distance(n_a, n_b, ptrA, ptrB)` to get the minimum distance (bf16 bits). 3. Navigate to `expected_min_dist` at the end of the case and perform bitwise equality comparison with the result: - Equal → Print PASS - Otherwise → Print FAIL (exp: ..., got: ...) 4. Move to the beginning of the next test case until all are completed. ### Code Note: The full implementation is on GitHub ```assembly .data # ========== Message Strings ========== start_msg: .string "===== Two-Point Minimum Distance Calculation (BF16) =====" end_msg: .string "===== Calculation Complete =====" input_msg: .string "Test Case " result_msg: .string "Minimum Distance: " newline: .string "\n" pass_msg: .string "PASS\n" fail_msg_prefix: .string "FAIL (exp: " fail_msg_mid: .string ", got: " fail_msg_tail: .string ")\n" arrow_msg: .string " -> " # ========== Test Data (Examples) ========== test_data: .word 3 # 3 test cases # Test Case 1: A={(0,0), (1,0)}, B={(0,1), (1,1)}, exp = 1.0 (0x3F80) .word 2 # n_a .word 2 # n_b # A .word 0x0000, 0x0000 # (0, 0) .word 0x3F80, 0x0000 # (1, 0) # B .word 0x0000, 0x3F80 # (0, 1) .word 0x3F80, 0x3F80 # (1, 1) # exp .word 0x3F80 # Test Case 2: A={(0,0)}, B={(3,4)}, exp = 5.0 (0x40A0) .word 1 # n_a .word 1 # n_b # A .word 0x0000, 0x0000 # (0, 0) # B .word 0x4040, 0x4080 # (3, 4) # exp .word 0x40A0 # Test Case 3: A={(0,0), (2,0)}, B={(1,1), (3,1)}, exp = sqrt(2) ≈ 1.414 (0x3FB5) .word 2 # n_a .word 2 # n_b # A .word 0x0000, 0x0000 # (0, 0) .word 0x4000, 0x0000 # (2, 0) # B .word 0x3F80, 0x3F80 # (1, 1) .word 0x4040, 0x3F80 # (3, 1) # exp .word 0x3FB5 .text .globl main # ========================================== # main # ========================================== main: # Print header la a0, start_msg li a7, 4 ecall li a0, 10 # '\n' li a7, 11 ecall la s0, test_data lw s1, 0(s0) # s1 = number of test cases addi s0, s0, 4 li s2, 0 # s2 = current case index main_test_loop: bge s2, s1, main_done # Print "Test Case i" la a0, input_msg # "Test Case " li a7, 4 ecall addi a0, s2, 1 li a7, 1 # print int ecall li a0, 10 li a7, 11 # print char '\n' ecall # Read case header lw a0, 0(s0) # n_a lw a1, 4(s0) # n_b addi a2, s0, 8 # A array starting address # B array starting address = A start + n_a * 8 bytes slli t0, a0, 3 # n_a * 8 add a3, a2, t0 # Preserve outer pointers addi sp, sp, -16 sw s0, 0(sp) sw s1, 4(sp) sw s2, 8(sp) sw ra, 12(sp) # Call minimum distance function jal ra, find_min_distance mv s3, a0 # s3 = calculation result (bf16 bits) # Restore lw s0, 0(sp) lw s1, 4(sp) lw s2, 8(sp) lw ra, 12(sp) addi sp, sp, 16 # Calculate expected answer address: # total_points_bytes = (n_a + n_b) * 8 # header 2 words = 8 bytes # t3 = s0 + 8 + (n_a+n_b)*8 lw t0, 0(s0) # n_a lw t1, 4(s0) # n_b add t2, t0, t1 slli t2, t2, 3 # *8 addi t2, t2, 8 # + header(2 words) add t3, s0, t2 lw s4, 0(t3) # s4 = expected answer (bf16 bits) # Print current result and PASS/FAIL la a0, result_msg li a7, 4 ecall mv a0, s3 # got li a7, 1 ecall la a0, arrow_msg # " -> " li a7, 4 ecall beq s3, s4, __print_pass # FAIL: Print "FAIL (exp: <exp>, got: <got>)\n" la a0, fail_msg_prefix li a7, 4 ecall mv a0, s4 # exp li a7, 1 ecall la a0, fail_msg_mid li a7, 4 ecall mv a0, s3 # got li a7, 1 ecall la a0, fail_msg_tail li a7, 4 ecall j __after_passfail __print_pass: la a0, pass_msg li a7, 4 ecall __after_passfail: addi s0, t3, 4 addi s2, s2, 1 j main_test_loop main_done: la a0, end_msg li a7, 4 ecall li a0, 10 li a7, 11 ecall li a0, 0 li a7, 10 # exit ecall # ========================================== # find_min_distance # Parameters: a0=n_a, a1=n_b, a2=ptrA, a3=ptrB # Return: a0 = minimum distance (bf16 bits) # ========================================== find_min_distance: addi sp, sp, -32 sw ra, 28(sp) sw s0, 24(sp) sw s1, 20(sp) sw s2, 16(sp) sw s3, 12(sp) sw s4, 8(sp) sw s5, 4(sp) mv s0, a0 # n_a mv s1, a1 # n_b mv s2, a2 # ptrA mv s3, a3 # ptrB li s4, 0x7F80 # min_dist = +Inf (bf16 bits) li s5, 0 # i find_outer_loop: bge s5, s0, find_done # A[i] address = ptrA + i*8 slli t0, s5, 3 add t0, s2, t0 lw a0, 0(t0) # ax lw a1, 4(t0) # ay li t1, 0 # j find_inner_loop: bge t1, s1, find_next_i # B[j] address = ptrB + j*8 slli t2, t1, 3 add t2, s3, t2 lw a2, 0(t2) # bx lw a3, 4(t2) # by # Save needed registers addi sp, sp, -12 sw a0, 0(sp) sw a1, 4(sp) sw t1, 8(sp) jal ra, calc_distance # a0 = dist (bf16 bits) mv t3, a0 # Restore lw a0, 0(sp) lw a1, 4(sp) lw t1, 8(sp) addi sp, sp, 12 # If dist < min_dist → update bltu t3, s4, find_update_min j find_continue find_update_min: mv s4, t3 find_continue: addi t1, t1, 1 j find_inner_loop find_next_i: addi s5, s5, 1 j find_outer_loop find_done: mv a0, s4 # return min lw ra, 28(sp) lw s0, 24(sp) lw s1, 20(sp) lw s2, 16(sp) lw s3, 12(sp) lw s4, 8(sp) lw s5, 4(sp) addi sp, sp, 32 ret # ========================================== # calc_distance(ax, ay, bx, by) → bf16 distance # ========================================== calc_distance: addi sp, sp, -24 sw ra, 20(sp) sw s0, 16(sp) sw s1, 12(sp) sw s2, 8(sp) sw s3, 4(sp) mv s3, a0 # ax mv s1, a1 # ay mv s2, a3 # by # dx = bx - ax mv a0, a2 # bx mv a1, s3 # ax jal ra, bf16_sub # a0 = dx mv a1, a0 jal ra, bf16_mul # a0 = dx^2 mv s0, a0 # s0 = dx^2 # dy = by - ay mv a0, s2 # by mv a1, s1 # ay jal ra, bf16_sub # a0 = dy mv a1, a0 jal ra, bf16_mul # a0 = dy^2 # dx^2 + dy^2 mv a1, a0 # dy^2 mv a0, s0 # dx^2 jal ra, bf16_add # a0 = sum # sqrt(sum) jal ra, bf16_sqrt # a0 = distance lw ra, 20(sp) lw s0, 16(sp) lw s1, 12(sp) lw s2, 8(sp) lw s3, 4(sp) addi sp, sp, 24 ret ``` ### Result ![image](https://hackmd.io/_uploads/rJ1kioiTge.png) Where 16256=0x3F80(=1.0), 16544=0x40A0(=5.0), 16309≈0x3FB5(≈1.414). ![image](https://hackmd.io/_uploads/HJOyisj6xe.png)