# Assignment1: RISC-V Assembly and Instruction Pipeline
contributed by [< rara0857 >](https://github.com/rara0857/ca2025-quizzes)
## Problem B
### Descripition
Assume uf8 implements a logarithmic 8-bit codec that maps 20-bit unsigned integers $([0,1{,}015{,}792])$ to 8-bit symbols via logarithmic quantization, delivering 2.5:1 compression and ≤6.25% relative error.
**UF8_Decode:**
$$
D(b) = m \cdot 2^e + (2^e - 1) \cdot 16
$$
**UF8_Encode**
$$
E(v) =
\begin{cases}
v, & \text{if } v < 16 \\
16e + \lfloor (v - \text{offset}(e)) / 2^e \rfloor, & \text{otherwise}
\end{cases}
$$
---
### Implementation
During encoding, we need to determine which power-of-two interval the input value belongs to, i.e., compute an integer $\log_2$.
Since RISC-V has no native instruction for this, we use CLZ (Count Leading Zeros) to locate the highest set bit, and obtain the exponent by $e = 31 - \text{CLZ}(x)$.
To find CLZ, we can repeatedly shifting the value right (16 → 8 → 4 → 2 → 1) until the highest ‘1’ bit is detected.
**C Code**
```c=
typedef uint8_t uf8;
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**
```s=
clz:
li t0, 32 # n
li t1, 16 # c
clz_loop:
srl t2, a0, t1 # uint32_t y = x >> c
beqz t2, clz_end # if (y)
sub t0, t0, t1 # n -= c
mv a0, t2 # x = y
clz_end:
srli t1, t1, 1
bnez t1, clz_loop
sub a0, t0, a0
ret
```
### **CLZ Optimization**
In this version's implementation, the `clz` routine is optimized by **removing all looping and branching**.
Instead of relying on a conditional loop, it performs a fixed sequence of right shifts (16→8→4→2→1) using `snez` masks.
This approach eliminates loop overhead and unpredictable branch delays, achieving **constant-time execution** with slightly larger code size.
```s=
clz:
li t0, 32 # n = 32
# 16-bit
srli t2, a0, 16 # y = x >> 16
snez t3, t2 # y != 0
slli t1, t3, 4 # t1 = 16 or 0
sub t0, t0, t1 # n -= t1
srl a0, a0, t1 # x >>= t1
# 8-bit
srli t2, a0, 8
snez t3, t2
slli t1, t3, 3 # t1 = 8 or 0
sub t0, t0, t1
srl a0, a0, t1
# 4-bit
srli t2, a0, 4
snez t3, t2
slli t1, t3, 2 # t1 = 4 or 0
sub t0, t0, t1
srl a0, a0, t1
# 2-bit
srli t2, a0, 2
snez t3, t2
slli t1, t3, 1 # t1 = 2 or 0
sub t0, t0, t1
srl a0, a0, t1
# 1-bit
srli t2, a0, 1
snez t3, t2
sub t0, t0, t3 # n -= 1
srl a0, a0, t3
sub a0, t0, a0 # return n - x
ret
```
### C Code
:::spoiler 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)
{
int n = 32, c = 16;
do {
uint32_t y = x >> c;
if (y) {
n -= c;
x = y;
}
c >>= 1;
} while (c);
return n - x;
}
/* 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;
}
/* 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;
}
/* Test encode/decode round-trip */
static bool test(void)
{
int test_cases[] = {0, 50, 100, 150, 200, 255};
int num=6;
for (int i = 0; i < 6; i++) {
uint8_t fl = test_cases[i];
int32_t value = uf8_decode(fl);
uint8_t fl2 = uf8_encode(value);
printf("test case: %d, decode -> %d, encode -> %d", fl,
value, fl2);
if (fl != fl2) {
printf(", Fail\n");
}
else {
printf(", Pass\n");
}
}
}
int main(void)
{
test();
}
```
:::
### Assembly Code
:::spoiler Assembly Code
```s=
.data
case:
.word 0, 50, 100, 150, 200, 255
count:
.word 6
msg1: .asciz "test case: "
msg2: .asciz ", decode -> "
msg3: .asciz ", encode -> "
msg4: .asciz ", Pass\n"
msg5: .asciz ", Fail\n"
.text
main:
la s0, case
lw s1, count
li s2, 0
test_loop:
bge s2, s1, exit
lw s3, 0(s0)
la a0, msg1
li a7, 4
ecall
mv a0, s3
li a7, 1
ecall
la a0, msg2
li a7, 4
ecall
mv a0, s3
jal ra, uf8_decode
mv s4, a0
mv a0, s4
li a7, 1
ecall
la a0, msg3
li a7, 4
ecall
mv a0, s4
jal ra, uf8_encode
li a7, 1
ecall
bne a0, s3, failed
la a0, msg4
li a7, 4
ecall
addi s0, s0, 4
addi s2, s2, 1
j test_loop
failed:
la a0, msg5
li a7, 4
ecall
addi s0, s0, 4
addi s2, s2, 1
j test_loop
exit:
li a7, 10
ecall
### Functions
clz:
li t0, 32 # n = 32
# 16-bit
srli t2, a0, 16 # y = x >> 16
snez t3, t2 # y != 0
slli t1, t3, 4 # t1 = 16 or 0
sub t0, t0, t1 # n -= t1
srl a0, a0, t1 # x >>= t1
# 8-bit
srli t2, a0, 8
snez t3, t2
slli t1, t3, 3 # t1 = 8 or 0
sub t0, t0, t1
srl a0, a0, t1
# 4-bit
srli t2, a0, 4
snez t3, t2
slli t1, t3, 2 # t1 = 4 or 0
sub t0, t0, t1
srl a0, a0, t1
# 2-bit
srli t2, a0, 2
snez t3, t2
slli t1, t3, 1 # t1 = 2 or 0
sub t0, t0, t1
srl a0, a0, t1
# 1-bit
srli t2, a0, 1
snez t3, t2
sub t0, t0, t3 # n -= 1
srl a0, a0, t3
sub a0, t0, a0 # return n - x
ret
uf8_decode:
andi t0, a0, 0x0f
srli t1, a0, 4
li t2, 15
sub t2, t2, t1
li t3, 0x7FFF
srl t3, t3, t2
slli t3, t3, 4
sll t0, t0, t1
add a0, t0, t3
ret
uf8_encode:
addi sp, sp, -8
sw ra, 4(sp)
sw s0, 0(sp)
mv s0, a0
# if (value < 16) return value
li t0, 16
blt s0, t0, encode_ret
# lz = clz(value)
# msb = 31 - lz
jal ra, clz
li t0, 31
sub t0, t0, a0 # t0 = msb
li t1, 0 # exponent
li t2, 0 # overflow
# if (msb >= 5)
li t3, 5
blt t0, t3, find_exact_exponent
# exponent = msb - 4
# if (exponent > 15) exponent = 15
addi t1, t0, -4
li t3, 15
li t3, 15
ble t1, t3, done1
mv t1, t3
done1:
li t3, 0 # e = 0
# for (uint8_t e = 0; e < exponent; e++)
# overflow = (overflow << 1) + 16
for_loop:
bge t3, t1, adjust
slli t2, t2, 1
addi t2, t2, 16
addi t3, t3, 1
j for_loop
# while (exponent > 0 && value < overflow)
adjust:
blez t1, find_exact_exponent
bge s0, t2, find_exact_exponent
addi t2, t2, -16
srli t2, t2, 1
addi t1, t1, -1
j adjust
find_exact_exponent:
li t3, 15
bge t1, t3, done2 # while (exponent < 15)
slli t4, t2, 1
addi t4, t4, 16
blt s0, t4, done2
mv t2, t4
addi t1, t1, 1
j find_exact_exponent
# mantissa = (value - overflow) >> exponent
# return (exponent << 4) | mantissa;
done2:
sub t3, s0, t2
srl t3, t3, t1
slli t1, t1, 4
or a0, t1, t3
j end
encode_ret:
mv a0, s0
end:
lw s0, 0(sp)
lw ra, 4(sp)
addi sp, sp, 8
ret
```
:::
### Analysis
Test with 6 test case `0,50,100,150,200,255`
**Console output**

**Comparison**
| | C Code | ASM | CLZ opt. |
|:----------------:|:---------:|:--------:|:--------:|
| **Cycles** | 27466 | 1216 | 1141 |
| **Instrs. Ret.** | 21055 | 836 | 827 |
| **CPI** | 1.3 | 1.45 | 1.38 |
| **IPC** | 0.767 | 0.688 | 0.725 |
| **Clock rate** | 23.40 KHz | 1.27 KHz | 2.54 KHz |
## Problem C
### Description
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 7 6 0
S: Sign bit (0 = positive, 1 = negative)
E: Exponent bits (8 bits, bias = 127)
M: Mantissa/fraction bits (7 bits)
**Value Representation**
The value 𝑣 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\}$ — sign bit
- $E \in [1, 254]$ — biased exponent
- $M \in [0, 127]$ — 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 \neq 0 \Rightarrow v = \text{NaN}$
* Denormals: Not supported (flush to zero)
---
### Implementation
To finish the code, the following components are required:
#### Basic Checks
- `bf16_isnan`, `bf16_isinf`, `bf16_iszero`: Detect NaN, ±Inf, ±0.
#### Format Conversion
- `f32_to_bf16(float val)`: Converts 32-bit float → bfloat16 with rounding (RNE) to reduce precision loss.
- `bf16_to_f32(bf16_t val)`: Converts bfloat16 → 32-bit float by padding mantissa bits with zeros.
#### Arithmetic Operations
- `bf16_add(a, b)`: Addition
- `bf16_sub(a, b)`: Subtraction
- `bf16_mul(a, b)`: Multiplication
- `bf16_div(a, b)`: Division
- `bf16_sqrt(a)`: Square root via exponent halving and mantissa normalization
#### Comparison Functions
- `bf16_eq`, `bf16_lt`, `bf16_gt`: Comparison functions (any NaN → false).
### C Code
:::spoiler C Code
```c=
#include <stdbool.h>
#include <stdint.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;
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;
}
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;
if (exp_b == 0xFF)
return (mant_b || sign_a == sign_b) ? b : BF16_NAN();
return a;
}
if (exp_b == 0xFF)
return b;
if (!exp_a && !mant_a)
return b;
if (!exp_b && !mant_b)
return a;
if (exp_a)
mant_a |= 0x80;
if (exp_b)
mant_b |= 0x80;
int16_t exp_diff = exp_a - exp_b;
uint16_t result_sign;
int16_t result_exp;
uint32_t result_mant;
if (exp_diff > 0) {
result_exp = exp_a;
if (exp_diff > 8)
return a;
mant_b >>= exp_diff;
} else if (exp_diff < 0) {
result_exp = exp_b;
if (exp_diff < -8)
return b;
mant_a >>= -exp_diff;
} else {
result_exp = exp_a;
}
if (sign_a == sign_b) {
result_sign = sign_a;
result_mant = (uint32_t) mant_a + mant_b;
if (result_mant & 0x100) {
result_mant >>= 1;
if (++result_exp >= 0xFF)
return (bf16_t) {.bits = (result_sign << 15) | 0x7F80};
}
} else {
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)) {
result_mant <<= 1;
if (--result_exp <= 0)
return BF16_ZERO();
}
}
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;
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;
if (exp_a == 0xFF) {
if (mant_a)
return a;
if (!exp_b && !mant_b)
return BF16_NAN();
return (bf16_t) {.bits = (result_sign << 15) | 0x7F80};
}
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};
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;
if (result_mant & 0x8000) {
result_mant = (result_mant >> 8) & 0x7F;
result_exp++;
} else
result_mant = (result_mant >> 7) & 0x7F;
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;
if (exp_b == 0xFF) {
if (mant_b)
return b;
/* Inf/Inf = NaN */
if (exp_a == 0xFF && !mant_a)
return BF16_NAN();
return (bf16_t) {.bits = result_sign << 15};
}
if (!exp_b && !mant_b) {
if (!exp_a && !mant_a)
return BF16_NAN();
return (bf16_t) {.bits = (result_sign << 15) | 0x7F80};
}
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;
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 (handle both +0 and -0) */
if (!exp && !mant)
return BF16_ZERO();
/* sqrt of negative number is NaN */
if (sign)
return BF16_NAN();
/* Flush denormals to zero */
if (!exp)
return BF16_ZERO();
/* Direct bit manipulation square root algorithm */
/* For sqrt: new_exp = (old_exp - bias) / 2 + bias */
int32_t e = exp - BF16_EXP_BIAS;
int32_t new_exp;
/* Get full mantissa with implicit 1 */
uint32_t m = 0x80 | mant; /* Range [128, 256) representing [1.0, 2.0) */
/* Adjust for odd exponents: sqrt(2^odd * m) = 2^((odd-1)/2) * sqrt(2*m) */
if (e & 1) {
m <<= 1; /* Double mantissa for odd exponent */
new_exp = ((e - 1) >> 1) + BF16_EXP_BIAS;
} else {
new_exp = (e >> 1) + BF16_EXP_BIAS;
}
/* Now m is in range [128, 256) or [256, 512) if exponent was odd */
/* Binary search for integer square root */
/* We want result where result^2 = m * 128 (since 128 represents 1.0) */
uint32_t low = 90; /* Min sqrt (roughly sqrt(128)) */
uint32_t high = 256; /* Max sqrt (roughly sqrt(512)) */
uint32_t result = 128; /* Default */
/* Binary search for square root of m */
while (low <= high) {
uint32_t mid = (low + high) >> 1;
uint32_t sq = (mid * mid) / 128; /* Square and scale */
if (sq <= m) {
result = mid; /* This could be our answer */
low = mid + 1;
} else {
high = mid - 1;
}
}
/* result now contains sqrt(m) * sqrt(128) / sqrt(128) = sqrt(m) */
/* But we need to adjust the scale */
/* Since m is scaled where 128=1.0, result should also be scaled same way */
/* Normalize to ensure result is in [128, 256) */
if (result >= 256) {
result >>= 1;
new_exp++;
} else if (result < 128) {
while (result < 128 && new_exp > 1) {
result <<= 1;
new_exp--;
}
}
/* Extract 7-bit mantissa (remove implicit 1) */
uint16_t new_mant = result & 0x7F;
/* Check for overflow/underflow */
if (new_exp >= 0xFF)
return (bf16_t) {.bits = 0x7F80}; /* +Inf */
if (new_exp <= 0)
return BF16_ZERO();
return (bf16_t) {.bits = ((new_exp & 0xFF) << 7) | new_mant};
}
static inline bool bf16_eq(bf16_t a, bf16_t b)
{
if (bf16_isnan(a) || bf16_isnan(b))
return false;
if (bf16_iszero(a) && bf16_iszero(b))
return true;
return a.bits == b.bits;
}
static inline bool bf16_lt(bf16_t a, bf16_t b)
{
if (bf16_isnan(a) || bf16_isnan(b))
return false;
if (bf16_iszero(a) && bf16_iszero(b))
return false;
bool sign_a = (a.bits >> 15) & 1, sign_b = (b.bits >> 15) & 1;
if (sign_a != sign_b)
return sign_a > sign_b;
return sign_a ? a.bits > b.bits : a.bits < b.bits;
}
static inline bool bf16_gt(bf16_t a, bf16_t b)
{
return bf16_lt(b, a);
}
```
:::
### Assembly Code
:::spoiler Assembly Code
```s=
.data
BF16_SIGN_MASK: .word 0x8000
BF16_EXP_MASK: .word 0x7F80
BF16_MANT_MASK: .word 0x007F
BF16_EXP_BIAS: .word 127
BF16_NAN: .word 0x7FC0
BF16_ZERO: .word 0x0000
test:
.word 0x3F80 # 1.0
.word 0x4000 # 2.0
.word 0xC000 # -2.0
.word 0x0000 # +0.0
.word 0x4080 # 4.0
.text
.globl main
main:
la t0, test
lw a2, 0(t0) # 1.0
lw a3, 4(t0) # 2.0
lw a4, 16(t0) # 4.0
lw a5, 8(t0) # -2.0
# 2 + 3 = 5
li a0, 0x4000 # 2.0
li a1, 0x4040 # 3.0
jal ra, bf16_add
jal ra, print_hex # expect 0x40A0 (5.0)
# 2 - 3 = -1
li a0, 0x4000 # 2.0
li a1, 0x4040 # 3.0
jal ra, bf16_sub
jal ra, print_hex # expect 0xBF80 (-1.0)
# 2 * 3 = 6
li a0, 0x4000 # 2.0
li a1, 0x4040 # 3.0
jal ra, bf16_mul
jal ra, print_hex # expect 0x40C0 (6.0)
# 4 / 2 = 2
li a0, 0x4080 # 4.0
li a1, 0x4000 # 2.0
jal ra, bf16_div
jal ra, print_hex # expect 0x4000 (2.0)
# sqrt(4) = 2
li a0, 0x4080 # 4.0
jal ra, bf16_sqrt
jal ra, print_hex # expect 0x4000 (2.0)
# end
li a7, 10
ecall
print_hex:
addi sp, sp, -16
sw ra, 12(sp)
sw a0, 8(sp)
li a7, 11
li a0, 48 # '0' = 48
ecall
li a0, 120 # 'x' = 120
ecall
lw a0, 8(sp)
li t0, 4
li t1, 12
print_hex_loop:
beqz t0, print_hex_done
lw t2, 8(sp)
srl t2, t2, t1
andi t2, t2, 0xF
li t3, 10
blt t2, t3, print_hex_digit
addi t2, t2, -10
addi t2, t2, 65 # 'A' = 65
j print_hex_char
print_hex_digit:
addi t2, t2, 48 # '0' = 48
print_hex_char:
mv a0, t2
li a7, 11
ecall
addi t0, t0, -1
addi t1, t1, -4
j print_hex_loop
print_hex_done:
li a7, 11
li a0, 10 # '\n' = 10
ecall
lw ra, 12(sp)
addi sp, sp, 16
ret
bf16_isnan:
la t0,BF16_EXP_MASK
lw t0,0(t0)
la t1,BF16_MANT_MASK
lw t1,0(t1)
and t2, a0, t0
xor t2, t2, t0 # (a.bits & BF16_EXP_MASK) == BF16_EXP_MASK
sltiu t4, t2, 1 # cond1
and t3, a0, t1 # a.bits & BF16_MANT_MASK
snez t5, t3 # !cond2
and a0, t4, t5 # check if (cond1 & !cond2) ==1
ret
bf16_isinf:
la t0,BF16_EXP_MASK
lw t0,0(t0)
la t1,BF16_MANT_MASK
lw t1,0(t1)
and t2, a0, t0
xor t2, t2, t0 # (a.bits & BF16_EXP_MASK) == BF16_EXP_MASK
sltiu t4, t2, 1 # cond1
and t3, a0, t1 # a.bits & BF16_MANT_MASK
sltiu t5, t3, 1 # cond2
and a0, t4, t5 # check if (cond1 & cond2) ==1
ret
bf16_iszero:
li t0,0x7fff
and t1,a0,t0
sltiu a0,t1,1
ret
f32_to_bf16:
#(f32bits >> 23) & 0xFF
srli t0, a0, 23
andi t0, t0, 0xFF
li t1, 0xFF
bne t0, t1, f32_round
# return (bf16_t) {.bits = (f32bits >> 16) & 0xFFFF}
srli a0, a0, 16
ret
f32_round:
# f32bits += ((f32bits >> 16) & 1) + 0x7FFF
srli t0, a0, 16
andi t0, t0, 1
li t1, 0x7FFF
add t0, t0, t1
add a0, a0, t0
# return (bf16_t) {.bits = f32bits >> 16}
srli a0, a0, 16
ret
bf16_to_f32:
# uint32_t f32bits = ((uint32_t) val.bits) << 16
slli a0, a0, 16
ret
bf16_add:
srli s1, a0, 15 # sign_a
srli s2, a1, 15 # sign_b
srli s3, a0, 7
andi s3, s3, 0xFF # exp_a
srli s4, a1, 7
andi s4, s4, 0xFF # exp_b
andi s5, a0, 0x7F # mant_a
andi s6, a1, 0x7F # mant_b
li t0, 0xFF
beq s3, t0, add_sp # a==NaN/inf
beq s4, t0, add_sp # b==NaN/inf
or t0, s3, s5 # a==0, return b
beqz t0, add_ret_b
or t1, s4, s6 # b==0, return a
beqz t1, add_return
li t0, 0x80
or s5, s5, t0 # mant_a |= 0x80
or s6, s6, t0 # mant_b |= 0x80
sub s7, s3, s4 # s7 = exp_diff
mv s8, s3 # result_exp = exp_a
blez s7, add_align
srl s6, s6, s7 # align mant_b
j add_sign_check
add_align:
mv s8, s4 # result_exp = exp_b
neg s7, s7
srl s5, s5, s7 # align mant_a
add_sign_check:
beq s1, s2, add_add
bge s5, s6, add_sub_a_minus_b
mv s9, s2 # result_sign = sign_b
sub s10, s6, s5 # result_mant = mant_b - mant_a
j add_norm
add_sub_a_minus_b:
mv s9, s1 # result_sign = sign_a
sub s10, s5, s6 # result_mant = mant_a - mant_a
add_norm:
li a0, 0
beqz s10, add_return # if result is zero, return zero
add_norm_loop:
li t0, 0x80
and t1, s10, t0
bnez t1, add_pack
slli s10, s10, 1
addi s8, s8, -1
blez s8, add_return # return zero if exp underflows
j add_norm_loop
add_add:
mv s9, s1 # result_sign = sign_a
add s10, s5, s6 # result_mant
andi t0, s10, 0x100
beqz t0, add_pack
# Mantissa overflow
srli s10, s10, 1
addi s8, s8, 1
li t0, 0xFF
bge s8, t0, add_return_inf
add_pack:
slli s9, s9, 15
andi s8, s8, 0xFF
slli s8, s8, 7
andi s10, s10, 0x7F
or a0, s9, s8
or a0, a0, s10
j add_return
add_sp:
bnez s5, add_return # a==NaN, return a
li t0, 0xFF
bne s4, t0, add_return # b!=NaN/inf, return a
bnez s6, add_ret_b # b==NaN, return b
beq s1, s2, add_ret_b # inf+inf, return b
li a0, 0x7FC0 # +inf + -inf, return NaN
j add_return
add_ret_b:
mv a0, a1
j add_return
add_return_inf:
slli a0, s9, 15
li t0, 0x7F80
or a0, a0, t0
add_return:
ret
bf16_sub:
li t0, 0x8000
xor a1, a1, t0
j bf16_add
bf16_mul:
addi sp, sp, -40
sw s0, 0(sp)
sw s1, 4(sp)
sw s2, 8(sp)
sw s3, 12(sp)
sw s4, 16(sp)
sw s5, 20(sp)
sw s6, 24(sp)
sw s7, 28(sp)
sw s8, 32(sp)
sw s9, 36(sp)
# a.bits -> sign, exp, mant
srli s0, a0, 15 # s0 = sign_a
srli s2, a0, 7
andi s2, s2, 0xFF # s2 = exp_a
andi s4, a0, 0x7F # s4 = mant_a
# b.bits -> sign, exp, mant
srli s1, a1, 15 # s1 = sign_b
srli s3, a1, 7
andi s3, s3, 0xFF # s3 = exp_b
andi s5, a1, 0x7F # s5 = mant_b
xor s6, s0, s1 # s6 = result_sign
# handle a == NaN or Inf
li t0, 0xFF
bne s2, t0, mul_check_b_sp
bnez s4, mul_ret_a # a is NaN, return a
or t0, s3, s5 # if (b == 0)
bnez t0, mul_ret_inf # Inf * Num = Inf
la t0, BF16_NAN
lw a0, 0(t0) # Inf * 0 = NaN
j mul_ret
mul_check_b_sp:
# handle b == NaN or Inf
li t0, 0xFF
bne s3, t0, mul_check_zero
bnez s5, mul_ret_b # b is NaN, return b
or t0, s2, s4 # if (a == 0)
bnez t0, mul_ret_inf # Num * Inf = Inf
la t0, BF16_NAN
lw a0, 0(t0) # 0 * Inf = NaN
j mul_ret
mul_check_zero:
# handle a == 0 or b == 0
or t0, s2, s4
beqz t0, mul_ret_zero # if a==0
or t0, s3, s5
beqz t0, mul_ret_zero # if b==0
# normalize denormals
li s9, 0 # s9 = exp_adjust
bnez s2, mul_norm_a_done
mul_norm_a_loop:
# normalize a
andi t0, s4, 0x80
bnez t0, mul_norm_a_end
slli s4, s4, 1
addi s9, s9, -1
j mul_norm_a_loop
mul_norm_a_end:
li s2, 1
mul_norm_a_done:
ori s4, s4, 0x80 # mant_a |= implicit 1
bnez s3, mul_norm_b_done
mul_norm_b_loop:
# normalize b
andi t0, s5, 0x80
bnez t0, mul_norm_b_end
slli s5, s5, 1
addi s9, s9, -1
j mul_norm_b_loop
mul_norm_b_end:
li s3, 1
mul_norm_b_done:
ori s5, s5, 0x80 # mant_b |= implicit 1
# main calculation
mul s8, s4, s5 # s8 = result_mant
la t0, BF16_EXP_BIAS
lw t0, 0(t0)
add s7, s2, s3 # s7 = result_exp
sub s7, s7, t0
add s7, s7, s9
# normalize result
li t0, 0x8000
and t0, s8, t0
beqz t0, mul_norm_res_else
srli s8, s8, 8
andi s8, s8, 0x7F
addi s7, s7, 1
j mul_check_overflow
mul_norm_res_else:
srli s8, s8, 7
andi s8, s8, 0x7F
mul_check_overflow:
# check overflow / underflow
li t0, 0xFF
bge s7, t0, mul_ret_inf
blez s7, mul_underflow
j mul_pack
mul_underflow:
li t0, -6
blt s7, t0, mul_ret_zero
li t0, 1
sub t0, t0, s7
srl s8, s8, t0
li s7, 0
mul_pack:
# pack result bits
slli s6, s6, 15
andi s7, s7, 0xFF
slli s7, s7, 7
andi s8, s8, 0x7F
or a0, s6, s7
or a0, a0, s8
j mul_ret
mul_ret_a:
mv a0, a0
j mul_ret
mul_ret_b:
mv a0, a1
j mul_ret
mul_ret_zero:
slli a0, s6, 15
j mul_ret
mul_ret_inf:
slli a0, s6, 15
li t0, 0x7F80
or a0, a0, t0
mul_ret:
# restore s0-s9
lw s0, 0(sp)
lw s1, 4(sp)
lw s2, 8(sp)
lw s3, 12(sp)
lw s4, 16(sp)
lw s5, 20(sp)
lw s6, 24(sp)
lw s7, 28(sp)
lw s8, 32(sp)
lw s9, 36(sp)
addi sp, sp, 40
ret
bf16_div:
# --- Prologue: Save registers ---
addi sp, sp, -44
sw ra, 0(sp)
sw s0, 4(sp)
sw s1, 8(sp)
sw s2, 12(sp)
sw s3, 16(sp)
sw s4, 20(sp)
sw s5, 24(sp)
sw s6, 28(sp)
sw s7, 32(sp)
sw s8, 36(sp)
sw s9, 40(sp)
addi sp, sp, -4
sw s10, 0(sp) # Save s10
srli s0, a0, 15 # s0 = sign_a
srli s2, a0, 7
andi s2, s2, 0xFF # s2 = exp_a
andi s4, a0, 0x7F # s4 = mant_a
srli s1, a1, 15 # s1 = sign_b
srli s3, a1, 7
andi s3, s3, 0xFF # s3 = exp_b
andi s5, a1, 0x7F # s5 = mant_b
xor s6, s0, s1 # s6 = result_sign
# Special Case
# if (exp_b == 0xFF),b is Inf or NaN
li t0, 0xFF
bne s3, t0, div_check_div_by_zero
bnez s5, div_ret_b # if (mant_b) return b (NaN)
# b is Inf
li t0, 0xFF
beq s2, t0, div_check_inf_div_inf # Check if a is also Inf
# a is a normal number, Num / Inf = 0
j div_ret_zero
div_check_inf_div_inf:
bnez s4, div_ret_a # a is NaN, return a
# Inf / Inf = NaN
j div_ret_nan
div_check_div_by_zero:
# if (!exp_b && !mant_b) -> b is 0
or t0, s3, s5
bnez t0, div_check_a_sp
# b is 0
or t0, s2, s4 # Check if a is also 0
beqz t0, div_ret_nan # 0 / 0 = NaN
# Num / 0 = Inf
j div_ret_inf
div_check_a_sp:
# if (exp_a == 0xFF) -> a is Inf or NaN
li t0, 0xFF
bne s2, t0, div_check_a_zero
bnez s4, div_ret_a # a is NaN, return a
# a is Inf, Inf / Num = Inf
j div_ret_inf
div_check_a_zero:
# if (!exp_a && !mant_a) -> a is 0
or t0, s2, s4
bnez t0, div_pre_calc
# a is 0, 0 / Num = 0
j div_ret_zero
div_pre_calc:
# --- Add implicit leading bit for normal numbers ---
bnez s2, div_norm_a_done
# a is denormal, mant_a has no implicit 1
j div_norm_b_check
div_norm_a_done:
ori s4, s4, 0x80
div_norm_b_check:
bnez s3, div_norm_b_done
# b is denormal
j div_main_calc
div_norm_b_done:
ori s5, s5, 0x80
div_main_calc:
# --- Core Division Loop (Restoring Division) ---
# uint32_t dividend = (uint32_t) mant_a << 15;
slli s8, s4, 15 # s8 = dividend
mv s9, s5 # s9 = divisor
li s10, 0 # s10 = quotient
li t0, 0 # t0 = loop counter i
div_loop_start:
slli s10, s10, 1 # quotient <<= 1
# Calculate shifted divisor: divisor << (15 - i)
li t1, 15
sub t1, t1, t0
sll t2, s9, t1 # t2 = divisor << (15 - i)
# if (dividend >= shifted_divisor)
bltu s8, t2, div_loop_skip_sub
sub s8, s8, t2 # dividend -= shifted_divisor
ori s10, s10, 1 # quotient |= 1
div_loop_skip_sub:
addi t0, t0, 1
li t1, 16
blt t0, t1, div_loop_start
# --- Exponent Calculation ---
la t0, BF16_EXP_BIAS
lw t0, 0(t0)
sub s7, s2, s3 # s7 = result_exp = exp_a - exp_b
add s7, s7, t0 # s7 += BIAS
beqz s2, div_exp_adj_a_done
addi s7, s7, -1 # if a was denormal
div_exp_adj_a_done:
beqz s3, div_exp_adj_b_done
addi s7, s7, 1 # if b was denormal
div_exp_adj_b_done:
# --- Normalize Quotient ---
li t0, 0x8000
and t0, s10, t0
bnez t0, div_norm_q_shift_8 # if (quotient & 0x8000)
div_norm_q_loop:
li t0, 0x8000
and t0, s10, t0
bnez t0, div_norm_q_shift_8 # if quotient is normalized
li t1, 1
ble s7, t1, div_norm_q_shift_8 # if exp <= 1, stop shifting
slli s10, s10, 1
addi s7, s7, -1
j div_norm_q_loop
div_norm_q_shift_8:
srli s10, s10, 8
andi s10, s10, 0x7F # quotient &= 0x7F
# --- Check Overflow / Underflow ---
li t0, 0xFF
bge s7, t0, div_ret_inf # Overflow -> Inf
blez s7, div_ret_zero # Underflow -> 0 (simplified from C code)
div_pack:
slli s6, s6, 15 # result_sign
andi s7, s7, 0xFF
slli s7, s7, 7 # result_exp
andi s10, s10, 0x7F # result_mant
or a0, s6, s7
or a0, a0, s10
j div_ret
div_ret_a:
mv a0, a0
j div_ret
div_ret_b:
mv a0, a1
j div_ret
div_ret_nan:
la t0, BF16_NAN
lw a0, 0(t0)
j div_ret
div_ret_zero:
slli a0, s6, 15
j div_ret
div_ret_inf:
slli a0, s6, 15
li t0, 0x7F80
or a0, a0, t0
div_ret:
# --- Epilogue: Restore registers ---
lw s10, 0(sp)
addi sp, sp, 4
lw ra, 0(sp)
lw s0, 4(sp)
lw s1, 8(sp)
lw s2, 12(sp)
lw s3, 16(sp)
lw s4, 20(sp)
lw s5, 24(sp)
lw s6, 28(sp)
lw s7, 32(sp)
lw s8, 36(sp)
lw s9, 40(sp)
addi sp, sp, 44
ret
bf16_sqrt:
# save s0-s7
addi sp, sp, -32
sw s0, 0(sp)
sw s1, 4(sp)
sw s2, 8(sp)
sw s3, 12(sp)
sw s4, 16(sp)
sw s5, 20(sp)
sw s6, 24(sp)
sw s7, 28(sp)
# decompose a
srli s0, a0, 15 # s0 = sign
srli s1, a0, 7
andi s1, s1, 0xFF # s1 = exp
andi s2, a0, 0x7F # s2 = mant
# handle special cases
li t0, 0xFF
bne s1, t0, sqrt_check_zero
bnez s2, sqrt_ret_a # a is NaN
bnez s0, sqrt_ret_nan # sqrt(-Inf)
j sqrt_ret_a # sqrt(+Inf)
sqrt_check_zero:
or t0, s1, s2
bnez t0, sqrt_check_neg
la a0, BF16_ZERO
lw a0, 0(t0) # sqrt(0)
j sqrt_ret
sqrt_check_neg:
bnez s0, sqrt_ret_nan # sqrt(negative)
beqz s1, sqrt_ret_zero # denormal -> 0
# calculate exponent
la t0, BF16_EXP_BIAS
lw t0, 0(t0)
sub s3, s1, t0 # s3 = e
ori s4, s2, 0x80 # s4 = m
andi t1, s3, 1
beqz t1, sqrt_exp_even
# e is odd
slli s4, s4, 1
addi s3, s3, -1
srli s3, s3, 1
add s7, s3, t0 # s7 = new_exp
j sqrt_core
sqrt_exp_even:
# e is even
srli s3, s3, 1
add s7, s3, t0 # s7 = new_exp
sqrt_core:
# binary search
li s5, 90 # low
li s6, 256 # high
li s2, 128 # result
sqrt_loop:
ble s6, s5, sqrt_norm
add t0, s5, s6 # mid
srli t0, t0, 1
mul t1, t0, t0 # sq = mid * mid
srli t1, t1, 7 # sq /= 128
ble t1, s4, sqrt_update_low
addi s6, t0, -1 # high = mid - 1
j sqrt_loop
sqrt_update_low:
mv s2, t0 # result = mid
addi s5, t0, 1 # low = mid + 1
j sqrt_loop
sqrt_norm:
# normalize result
li t0, 256
blt s2, t0, sqrt_norm_check_low
srli s2, s2, 1
addi s7, s7, 1
sqrt_norm_check_low:
li t0, 128
bge s2, t0, sqrt_norm_end
li t0, 1
ble s7, t0, sqrt_norm_end
slli s2, s2, 1
addi s7, s7, -1
j sqrt_norm_check_low
sqrt_norm_end:
andi s2, s2, 0x7F # new_mant
# check overflow / underflow
li t0, 0xFF
bge s7, t0, sqrt_ret_inf
blez s7, sqrt_ret_zero
sqrt_pack:
# pack result
andi s7, s7, 0xFF
slli s7, s7, 7
or a0, s7, s2
j sqrt_ret
sqrt_ret_a:
mv a0, a0
j sqrt_ret
sqrt_ret_nan:
la t0, BF16_NAN
lw a0, 0(t0)
j sqrt_ret
sqrt_ret_zero:
la t0, BF16_ZERO
lw a0, 0(t0)
j sqrt_ret
sqrt_ret_inf:
li a0, 0x7F80
sqrt_ret:
# restore s0-s7
lw s0, 0(sp)
lw s1, 4(sp)
lw s2, 8(sp)
lw s3, 12(sp)
lw s4, 16(sp)
lw s5, 20(sp)
lw s6, 24(sp)
lw s7, 28(sp)
addi sp, sp, 32
ret
bf16_eq:
addi sp, sp, -16
sw ra, 12(sp)
sw a0, 8(sp) # a
sw a1, 4(sp) # b
# check if (bf16_isnan(a) || bf16_isnan(b))
jal ra, bf16_isnan
bnez a0, eq_false # isnan(b), return false
lw a0, 4(sp) # b
jal ra, bf16_isnan
bnez a0, eq_false # isnan(b), return false
# check if (bf16_iszero(a) && iszero(b))
lw a0, 8(sp) # a
jal ra, bf16_iszero
beqz a0, eq_cmp_bits
lw a0, 4(sp) # b
jal ra, bf16_iszero
beqz a0, eq_cmp_bits
# a==0 && b==0, return true
li a0, 1
j eq_end
eq_cmp_bits:
# return a.bits == b.bits;
lw t0, 8(sp) # a.bits
lw t1, 4(sp) # b.bits
xor a0, t0, t1 # a0 = a.bits ^ b.bits
sltiu a0, a0, 1 # a0 == 0 return true, else false
j eq_end
eq_false:
li a0, 0 # false
eq_end:
lw ra, 12(sp)
addi sp, sp, 16
ret
bf16_lt:
# store return addr & a,b
addi sp, sp, -16
sw ra, 12(sp)
sw a0, 8(sp) # a
sw a1, 4(sp) # b
# isnan(a)
jal ra, bf16_isnan
bnez a0, lt_false
# isnan(b)
lw a0, 4(sp)
jal ra, bf16_isnan
bnez a0, lt_false
# check if (iszero(a) && iszero(b))
lw a0, 8(sp)
jal ra, bf16_iszero
beqz a0, lt_cmp_sign
lw a0, 4(sp) # b
jal ra, bf16_iszero
bnez a0, lt_false
lt_cmp_sign:
lw t0, 8(sp) # a.bits
lw t1, 4(sp) # b.bits
srli t2, t0, 15 # sign_a = (a.bits >> 15) & 1
srli t3, t1, 15 # sign_b = (b.bits >> 15) & 1
beq t2, t3, lt_eq
sltu a0, t3, t2 # sign_a > sign_b
j lt_end
lt_eq:
bnez t2, lt_both_negative
# return a.bits < b.bits
sltu a0, t0, t1
j lt_end
lt_both_negative:
# return a.bits > b.bits
sltu a0, t1, t0
j lt_end
lt_false:
li a0, 0 # false
lt_end:
lw ra, 12(sp)
addi sp, sp, 16
ret
bf16_gt:
# swap (a,b), call bf16_lt
mv t0, a0
mv a0, a1
mv a1, t0
j bf16_lt
```
:::
### Analysis
**Comparison**
| | C Code | ASM |
|:----------------:|:------:|:-------:|
| **Cycles** | | 1247 |
| **Instrs. Ret.** | | 873 |
| **CPI** | | 1.43 |
| **IPC** | | 0.7 |
| **Clock rate** | | 1.43KHz |
## [LeetCode 1611. Minimum One Bit Operations to Make Integers Zero](https://leetcode.com/problems/minimum-one-bit-operations-to-make-integers-zero/description/)
### Description
Given an integer `n`, you must transform it into `0` using the following operations any number of times:
- Change the rightmost (0th) bit in the binary representation of `n`.
- Change the `i`th bit in the binary representation of `n` **if** the `(i−1)`th bit is set to `1` and the `(i−2)`th through `0`th bits are set to `0`.
Return the *minimum number of operations* to transform `n` into `0`.
---
**Example 1:**
> Input: n = 3
> Output: 2
> Explanation:
> The binary representation of 3 is `"11"`.
> `"11"` → `"01"` with the 2nd operation since the 0th bit is 1.
> `"01"` → `"00"` with the 1st operation.
---
**Example 2:**
> Input: n = 6
> Output: 4
> Explanation:
> The binary representation of 6 is `"110"`.
> `"110"` → `"010"` with the 2nd operation since the 1st bit is 1 and 0th through 0th bits are 0.
> `"010"` → `"011"` with the 1st operation.
> `"011"` → `"001"` with the 2nd operation since the 0th bit is 1.
> `"001"` → `"000"` with the 1st operation.
---
**Constraints**
* `0 <= n <= 10⁹`
---
### Implementation
For a given integer $n$, each operation flips a single bit following the rule of Gray code transition.
Thus, the minimum number of operations equals the **Gray code value decoded to binary**.
The Gray code encoding is:
$$
g = n \oplus (n \gg 1)
$$
and decoding (the reverse process) is:
$$
f(n) = n \oplus (n \gg 1) \oplus (n \gg 2) \oplus (n \gg 3) \oplus \dots
$$
Iteratively, this can be computed by:
```c=
while (n) {
res ^= n;
n >>= 1;
}
```
Alternatively, the recursive form based on the most significant bit (MSB) is:
$$
f(0) = 0, \quad
f(n) = (1 \ll (k+1)) - 1 - f(n \oplus (1 \ll k))
$$
where $k = \text{MSB position of } n$.
To find $k$ efficiently, `clz` (Count Leading Zeros) is used:
$$
k = 31 - \text{clz}(n)
$$
This avoids the $O(\log n)$ loop that shifts `n` until zero,
reducing the total complexity from $O((\log n)^2)$ to $O(\log n)$.
### C Code
```c=
#include <stdio.h>
#include <stdint.h>
#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;
}
int minimumOneBitOperations(int n) {
if (!n) return 0;
int k = 31 - (int)clz((uint32_t)n);
return ((1 << (k + 1)) - 1) - minimumOneBitOperations(n ^ (1 << k));
}
int main(void) {
printf("%d\n", minimumOneBitOperations(1000000));
printf("%d\n", minimumOneBitOperations(2000000));
printf("%d\n", minimumOneBitOperations(3000000));
}
```
### Assembly Code
:::spoiler Assembly Code
```s=
.data
case:
.word 1000000,2000000,3000000
count:
.word 3
.text
.globl main
main:
la s0, case
lw t1, count
loop:
beqz t1, exit
lw a0, 0(s0)
addi sp, sp, -16
sw ra, 12(sp)
sw s0, 8(sp)
sw t1, 4(sp)
jal ra, minimumOneBitOperations
lw t1, 4(sp)
lw s0, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
li a7, 1
ecall
li a7, 11
li a0, 10
ecall
addi s0, s0, 4
addi t1, t1, -1
j loop
exit:
li a7, 10
ecall
minimumOneBitOperations:
beq a0, zero, mbo_ret0
addi sp, sp, -20
sw ra, 16(sp)
sw s0, 12(sp)
sw a0, 8(sp) # n
# k = 31 - clz(n)
jal ra, clz # a0 = clz(n)
lw t0, 8(sp) # t0 = original n
li t1, 31
sub t2, t1, a0 # t2 = k
# s0 = ((1 << (k+1)) - 1)
li t3, 1
addi t4, t2, 1
sll t3, t3, t4 # t3 = 1<<(k+1)
addi t3, t3, -1 # t3 = (1<<(k+1))-1
mv s0, t3
# a0 = n ^ (1<<k)
li t5, 1
sll t5, t5, t2 # t5 = 1<<k
xor a0, t0, t5 # a0 = n ^ (1<<k)
jal ra, minimumOneBitOperations
sub a0, s0, a0
lw s0, 12(sp)
lw ra, 16(sp)
addi sp, sp, 20
ret
mbo_ret0:
li a0, 0
ret
clz:
li t0, 32 # n = 32
li t1, 16 # c = 16
clz_loop:
srl t2, a0, t1 # y = x >> c
beqz t2, clz_skip
sub t0, t0, t1 # n -= c
mv a0, t2 # x = y
clz_skip:
srli t1, t1, 1
bnez t1, clz_loop
sub a0, t0, a0 # return n - x
ret
```
:::
---
Of course, we can optimize it with the **branchless CLZ** version already implemented in Problem B.
:::spoiler Assembly Code with branchless CLZ
```s=
.data
case:
.word 1000000,2000000,3000000
count:
.word 3
.text
.globl main
main:
la s0, case
lw t1, count
loop:
beqz t1, exit
lw a0, 0(s0)
addi sp, sp, -16
sw ra, 12(sp)
sw s0, 8(sp)
sw t1, 4(sp)
jal ra, minimumOneBitOperations
lw t1, 4(sp)
lw s0, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
li a7, 1
ecall
li a7, 11
li a0, 10
ecall
addi s0, s0, 4
addi t1, t1, -1
j loop
exit:
li a7, 10
ecall
minimumOneBitOperations:
beq a0, zero, mbo_ret0
addi sp, sp, -20
sw ra, 16(sp)
sw s0, 12(sp)
sw a0, 8(sp) # n
# k = 31 - clz(n)
jal ra, clz # a0 = clz(n)
lw t0, 8(sp) # t0 = original n
li t1, 31
sub t2, t1, a0 # t2 = k
# s0 = ((1 << (k+1)) - 1)
li t3, 1
addi t4, t2, 1
sll t3, t3, t4 # t3 = 1<<(k+1)
addi t3, t3, -1 # t3 = (1<<(k+1))-1
mv s0, t3
# a0 = n ^ (1<<k)
li t5, 1
sll t5, t5, t2 # t5 = 1<<k
xor a0, t0, t5 # a0 = n ^ (1<<k)
jal ra, minimumOneBitOperations
sub a0, s0, a0
lw s0, 12(sp)
lw ra, 16(sp)
addi sp, sp, 20
ret
mbo_ret0:
li a0, 0
ret
clz:
li t0, 32 # n = 32
# 16-bit
srli t2, a0, 16 # y = x >> 16
snez t3, t2 # y != 0
slli t1, t3, 4 # t1 = 16 or 0
sub t0, t0, t1 # n -= t1
srl a0, a0, t1 # x >>= t1
# 8-bit
srli t2, a0, 8
snez t3, t2
slli t1, t3, 3 # t1 = 8 or 0
sub t0, t0, t1
srl a0, a0, t1
# 4-bit
srli t2, a0, 4
snez t3, t2
slli t1, t3, 2 # t1 = 4 or 0
sub t0, t0, t1
srl a0, a0, t1
# 2-bit
srli t2, a0, 2
snez t3, t2
slli t1, t3, 1 # t1 = 2 or 0
sub t0, t0, t1
srl a0, a0, t1
# 1-bit
srli t2, a0, 1
snez t3, t2
sub t0, t0, t3 # n -= 1
srl a0, a0, t3
sub a0, t0, a0 # return n - x
ret
```
:::
### Analysis
Test with 3 test case `1000000,2000000,3000000`
**Console Output**

**Comparision**
| | C Code | ASM | CLZ opt. |
|:----------------:|:---------:|:--------:|:--------:|
| **Cycles** | 12011 | 1870 | 1510 |
| **Instrs. Ret.** | 9003 | 1317 | 1273 |
| **CPI** | 1.33 | 1.42 | 1.19 |
| **IPC** | 0.75 | 0.704 | 0.843 |
| **Clock rate** | 22.68 KHz | 1.71 KHz | 2.90 KHz |
## 5 stage RISC-V Processor
We test our code using [Ripes](https://ripes.me/) simulator.
RISC-V processor have five stage : **IF, ID, EX, MEM, WB**

| Stage | Full Name |
|:-----:|:------------------:|
| IF | Instruction Fetch |
| ID | Instruction Decode |
| EX | Execution |
| MEM | Data Memory Access |
| WB | Write Back |
---
## Reference
[Quiz1 of Computer Architecture (2025 Fall)](https://hackmd.io/@sysprog/2025-arch-homework1)
[LeetCode 1611 - Minimum One Bit Operations to Make Integers Zero](https://leetcode.com/problems/minimum-one-bit-operations-to-make-integers-zero/description/)
[Markdown syntax](https://hackmd.io/@mrcoding/ryZE7k8cN)