---
# System prepended metadata

title: arch2025-homework1

---

# Assignment 1 - RISC-V
## SUMMARY
- **Opening**
- **UTF8 Encoding/Decoding**
- **Study of the Processor**
- **BF16 All Operations (add/sub/mul/div/sqrt)**

---
# Opening

Before reading this work, I want to clarify a few things:

- First, I'm not entirely certain what the professor expects, which is why this report explains each part of the code in detail, even if it becomes borring. (I will adjust the approach for the next homework if needed)
- The more I worked on this assignment, the more I wanted to refactor everything. Unfortunately, there are constraints:
  - **Better naming conventions than "skip" and "loop" (currently quite unreadable)**
  - **More comprehensive explanations throughout**
- **This document reads more like a development journal than a formal report , I may make statements and revise them shortly after. I apologize for any inconsistencies.**

# UTF8 Encoding/Decoding

## Overview & Summary
Implementation of an encoder/decoder for the UTF8 format in RISC-V assembly.
- **Part 1: Pure C to RISC-V Translation** (encode & decode functions)
- **Part 2: Validation & Testing** (test suite and error handling)
- **Part 3: Performance Analysis** (instruction count and optimization)
---

# Part 1: Pure C to RISC-V Translation

**Main registers convention:**
- `x1`: input value / addresses
- `x2`: return result
- `x3-x19`: working registers
- `x20`: saved `ra` (return address)
- `x21`: stack pointer (`sp`)

---

## encode_uf8 Function
This section shows the step-by-step translation from C to RISC-V assembly.

### Save and Initialize
```riscv
encode_uf8:
    addi x21, x21, -4
    sw x20, 0(x21)
    jal x20, init_registers_encode
```
- Saves `ra` on stack for function call preservation

```riscv
init_registers_encode:
    li x2, 0          # return value
    li x3, 16         # comparison threshold
    li x4, 32         # CLZ counter
    li x5, 16         # CLZ step
    jr x20
```
- Initializes working registers with constants
```riscv
blt x1, x3, skip_encode
```
If value < 16, no encoding is needed; the value is returned directly.

### CLZ (Count Leading Zeros)
```riscv
add x6, x1, x0
loop:
    beq x5, x0, end_loop
    srl x7, x6, x5
    beq x7, x0, skip2
    sub x4, x4, x5
    addi x6, x7, 0
skip2:
    li x10, 1
    srl x5, x5, x10
    j loop
```
**Binary search algorithm** to count leading zeros:
- `x4`: counter 
- `x5`: shift step 
- `x6`: working value
- Shifts and tests to find the first 1 bit

### MSB Calculation (Most Significant Bit)
```riscv
sub x3, x4, x6
li x4, 31
sub x4, x4, x3
```
- `x3` = lz (leading zeros)
- `x4` = MSB position (31 - LZ)

### Exponent Calculation
```riscv
li x5, 0
blt x4, x11, skip3
    sub x5, x4, x12
    blt x5, x13, skip4
        li x5, 15
```
- If MSB < 5: exponent = 0
- else: exponent = MSB - 4
- maximum value: 15

### Overflow Construction
```riscv
loop3:
    bge x7, x5, loop3_end
    sll x6, x6, x10
    addi x6, x6, 16
    addi x7, x7, 1
    j loop3
```
overflow = (16 << exp) - 16

### Overflow Adjustment
```riscv
loop4:
    blt x5, x10, loop4_end
    blt x6, x1, loop4_end
    beq x6, x1, loop4_end
    addi x6, x6, -16
    srl x6, x6, x10
    addi x5, x5, -1
    j loop4
```
Reduces exponent if overflow is too large compared to the input value.

### Overflow Expansion
```riscv
loop5:
    bge x5, x15, loop5_end
    sll x7, x6, x10
    addi x7, x7, 16
    bge x7, x1, loop5_end
    addi x6, x7, 0
    addi x5, x5, 1
    j loop5
```
Increases exponent if possible to maximize precision.

### Mantissa Calculation
```riscv
sub x7, x1, x6
srl x7, x7, x5
sll x2, x5, x14
or x2, x2, x7
```
- Mantissa = (value - overflow) >> exponent
- result = (exponent << 4) | mantissa

### Return
```riscv
skip_encode:
    add x2, x0, x1    # if value < 16, just return value

encode_return:
    lw x20, 0(x21)
    addi x21, x21, 4
    jr x20
```
Uses the stack to handle multiple values (for testing purposes).

---

## decode_uf8 Function

### Register Initialization (decode)
```riscv
init_decode:
    li x2, 0          # return value
    li x3, 0          # mantissa
    li x4, 0          # exponent
    li x6, 0x0F       # mantissa mask
    li x7, 4          # shift amount
    li x8, 15         # max exponent
    li x9, 0x7FFF     # offset mask
    jr x20
```

### Field Extraction
```riscv
and x3, x1, x6        # mantissa = value & 0x0F
srl x4, x1, x7        # exponent = value >> 4
```
Separates 4 bits of mantissa and 4 bits of exponent.

### Offset Calculation
```riscv
sub x5, x8, x4        # 15 - exponent
srl x5, x9, x5        # 0x7FFF >> (15 - exponent)
sll x5, x5, x7        # << 4
```
Calculates offset = `((0x7FFF >> (15 - exp)) << 4)`.

### Final Decoding
```riscv
sll x2, x3, x4        # mantissa << exponent
add x2, x2, x5        # + offset
```
Result = `(mantissa << exponent) + offset`.

---

# Part 2: Validation & Testing

## Test Data Structure

### Test Values
```riscv
initial_value1: .word 976
initial_value2: .word 15
initial_value3: .word 0
```
**Test values**: 976, 15, and 0 to verify functionality.

These values test three different code paths:
- **0 and 15**: pass through without calculation (value < 16, direct return via `skip_encode`)
- **976**: goes through the full encode/decode process and should return exactly the same value

**Note**: Different numbers can be used, but keep in mind there will be an error rate for certain values due to the lossy nature of UF8 encoding.

### Result Management
```riscv
test1_pass:
    la a0, msg_pass
    li a7, 4
    ecall
    la x10, test_results
    li x11, 1
    sw x11, 0(x10)
```
Prints "PASS" and stores 1 in `test_results[0]`.

If test fails:
```riscv
la a0, msg_fail
li a7, 4
ecall
la x10, test_results
sw x0, 0(x10)
```
Prints "FAIL" and stores 0 in `test_results[0]`.

### Final Summary
```riscv
final_summary:
    la x10, test_results
    lw x11, 0(x10)
    lw x12, 4(x10)
    lw x13, 8(x10)
    and x14, x11, x12
    and x14, x14, x13
    beqz x14, failed
```
- Loads all 3 test results
- Performs logical AND: all must be 1 to pass
- If x14 = 0, at least one test failed

```riscv
la a0, msg_all_pass
li a7, 4
ecall
```
If all tests passed, prints "All tests PASSED!".

```riscv
failed:
    la a0, msg_final_fail
    li a7, 4
    ecall
```
If any test failed, prints "Some tests FAILED!".

---

# Part 3: Performance Analysis

## Instruction Count Analysis

To measure performance, **976** was chosen as a representative value that exercises the full encoding path (976 > 16).

### Methodology:
1. Count instructions in the RISC-V implementation using Ripes step-by-step execution
2. Estimate instructions in the C implementation 
3. Identify optimization opportunities

### encode_uf8 function (value = 976):

**Total: 102 instructions**

- **Initialization**: 10 instructions
- **CLZ**: 35 instructions (binary search loop, 5 iterations)
- **MSB calculation**: 3 instructions
- **Exponent and overflow setup**: 11 instructions
- **Overflow construction loops**: 26 instructions (loop3 + loop4 + loop5)
- **Mantissa calculation and combine**: 6 instructions
- **Return**: 3 instructions
- **Other**: 8 instructions (branches, misc, etc.)

The analysis reveals that CLZ is expensive, as are the various loops.

### decode_uf8 function (value = 976 encoded):

**Total: 23 instructions**

- **Initialization**: 15 instructions 
- **Decode formula**: 8 instructions 

Initialization is the most costly component.

## C vs RISC-V Comparison

| Operation | RISC-V | C language | 
|-----------|--------|---------------|
| Encode    | 102    | 77-90         | 
| Decode    | 23     | 12            | 
 
Note: These are estimated values for **ONE NUMBER ONLY**.

## Optimization Opportunities

### 1. CLZ Optimization
**Current**: Simple translation from the C program.

**Potential improvements**: 
- Use built-in RISC-V modules (not allowed in this assignment)
- Use a lookup table stored in memory

### 2. Loop Fusion
**Current**: Three separate loops (loop3, loop4, loop5)

**Attempted optimization**: Merge into a single loop with combined conditions

Multiple fusion attempts were made, but no significant improvement was achieved, so the original version was retained.

One attempt to merge loop4 and loop5 saved only 3 instructions:

```riscv
merge_loop:
        # Loop4
        blt x6, x1, skip00      # if overflow < value, try expand
        beq x6, x1, skip3       # if overflow == value, done
        
        # Overflow too big, decrease
        addi x6, x6, -16        # overflow -= 16
        srl x6, x6, x10         # overflow >>= 1
        addi x5, x5, -1         # exponent--
        j merge_loop
    
    skip00:
        # Loop5
        bge x5, x15, skip3      # if exponent >= 15, done
        sll x7, x6, x10         # next_overflow = overflow << 1
        addi x7, x7, 16         # next_overflow += 16
        bge x7, x1, skip3       # if next_overflow >= value, done
        addi x6, x7, 0          # overflow = next_overflow
        addi x5, x5, 1          # exponent++
        j merge_loop            # continue adjusting
```

## Optimized Performance Estimate

| Optimization | Encode (before) | Encode (after) | Improvement |
|--------------|-----------------|----------------|-------------|
| Loop merging | 102             | 98             | Minimal     |

No improvements were found for decode; initialization remains the most expensive component and cannot be easily optimized.

## Conclusion

After these unsuccessful optimization attempts, research was conducted on C compilation and efficiency. The key finding is that **cycle count is more important than instruction count for efficiency**. The next section compares C assembly with RISC-V assembly to better understand this principle.

---

# Processor Study with Ripes

Using the Ripes simulator with RISC-V 32-bit 5-stage processor, the flow of instructions through the pipeline and control signals at the hardware level are analyzed (based on the understanding of question 3 in homework 1).

### Pipeline Configuration:
- **Processor**: RISC-V 32-bit Single-cycle / 5-stage pipeline
- **ISA**: RV32I
- **Stages**: IF → ID → EX → MEM → WB

### Pipeline Stages Overview:
1. **IF** (Instruction Fetch): Fetch instruction from instruction memory using PC
2. **ID** (Instruction Decode): Decode instruction, read source registers from register file
3. **EX** (Execute): Perform ALU operation or calculate memory address
4. **MEM** (Memory Access): Access data memory for load/store instructions
5. **WB** (Write Back): Write result back to destination register

---

## Selected Instructions for Analysis

Two representative instructions covering big part of instruction are analyzed: `lw`, `sw`.

---

## Instruction 1: Load Word - `lw x1, 0(x2)`

**Context**: Loading test value 976 from memory at the beginning of main program

**Purpose**: Read the test value from data memory into register x1 for encoding

### Initial State
![image](https://hackmd.io/_uploads/SyLo6zLTxl.png)

![image](https://hackmd.io/_uploads/BJ2fTMIpeg.png)

- Memory address `0x10000000`: contains **976** (0x000003D0)
- Register x1: contains **0x00000000**
- Register x2: contains address **0x10000000**

---

### Stage 1: Instruction Fetch (IF)

![image](https://hackmd.io/_uploads/r1eXJXUage.png)

**What happens**:
- PC outputs current instruction address: 
![image](https://hackmd.io/_uploads/rymrkmUaxx.png)

- Instruction Memory fetches the encoded `lw` instruction: 
![image](https://hackmd.io/_uploads/HJbyZ7Lale.png)

- 0x00012083 = 0000000000000001001000001**0000011**
![image](https://hackmd.io/_uploads/BJLnxQ8plx.png)

---

### Stage 2: Instruction Decode (ID)
![image](https://hackmd.io/_uploads/HJ_MGQL6gg.png)

**What happens**:

Control Unit decodes instruction ![image](https://hackmd.io/_uploads/BJP14786gl.png)

- `0x02` source address (2)
- `0x00` add offset (nothing to add)
- `LW` instruction
- `0x01` destination register (1)

---

### Stage 3: Execute (EX)
![image](https://hackmd.io/_uploads/H1IvSm86gl.png)

**What happens**:
- ALU performs address calculation: base_address + offset
![image](https://hackmd.io/_uploads/BytmUQ86gg.png)

Calculation: 0x10000000 + 0 = 0x10000000

Result is the memory address to access.

---

### Stage 4: Memory Access (MEM)
![image](https://hackmd.io/_uploads/BywdS7U6lg.png)

**What happens**:
- Data Memory receives address from ALU (0x10000000)
![image](https://hackmd.io/_uploads/r1GewmI6lx.png)

- Signal Write red → Data memory reading only
- Data at address 0x10000000 is read: **976** (0x000003D0)
![image](https://hackmd.io/_uploads/B1c3DQUpgg.png)

**Control Signals Active**:
- **WR en = 0** (inactive), indicating read operation

---

### Stage 5: Write Back (WB)
![image](https://hackmd.io/_uploads/rJQYHmUall.png)

**What happens**:

![image](https://hackmd.io/_uploads/rJrWYQLpel.png)
- Wr data: 976 (0x000003d0)
- Wr idx: 0x01 (register x1)
- Wr En: 1 (enable) will write to the register

![image](https://hackmd.io/_uploads/Hy8BF78Tlx.png)

### Result
- **Register x1 now contains the test value for use throughout the program**
![image](https://hackmd.io/_uploads/HkOusQ86gx.png)

---

## Instruction 2: Store Word - `sw x20, 0(x21)`
Now that the process is understood, this analysis will proceed more quickly.

**Context**: Storing encoded result back to memory after encoding

**Purpose**: Store the return address (RA) in the stack (this technique is used when there are nested function calls)

### Initial State
![image](https://hackmd.io/_uploads/SyY2wBUTel.png)

Expected state:
- Register x20: contains the return address 
- Register x21: contains the current stack address

---

### Stage 1: IF
![image](https://hackmd.io/_uploads/ByCx4BL6le.png)

---
![image](https://hackmd.io/_uploads/BJqKVSIalg.png)

- PC: line number 0x00000200 ![image](https://hackmd.io/_uploads/SJ56EB8Tgl.png)

- Instructions: 0x014aa023=0000000101001010101000000**0100011**

![image](https://hackmd.io/_uploads/HyglrSU6xe.png)

![image](https://hackmd.io/_uploads/HJUAmHU6ee.png)

---

### Stage 2: ID
![image](https://hackmd.io/_uploads/rJC-4B86eg.png)

![image](https://hackmd.io/_uploads/rkSO8rUpgg.png)

**What happens**:
- `0x15` Reg1 destination address (x21)
- `0x14` Reg2 source address (x20)
- `SW` instruction
- `0x00` offset (0)

![image](https://hackmd.io/_uploads/HkazDHUTgl.png)

- Reading register contents: 
- Reg 1: 0x00010000
- Reg 2: 0x00000028

![image](https://hackmd.io/_uploads/Sy61dHITge.png)

---

### Stage 3: EX

![image](https://hackmd.io/_uploads/rJIfEHLpge.png)

**What happens**:

![image](https://hackmd.io/_uploads/SkVTOB8pgl.png)

- ALU calculates memory address: x21 + 0

---

### Stage 4: MEM
![image](https://hackmd.io/_uploads/BkRMESITeg.png)

**What happens**:

![image](https://hackmd.io/_uploads/HknEKrLplx.png)

- Addr: x21 (location to write)
- Data in: x20 (data to write)

**Control Signals Active**:
- **Wr en = 1 (green) = x20 → x21** 

![image](https://hackmd.io/_uploads/SJVN9rLalg.png)

---

### Stage 5: WB
![image](https://hackmd.io/_uploads/B13XNS8agx.png)

**What happens**:
- Data is written to the correct location!
![image](https://hackmd.io/_uploads/r1isqB86lx.png)

- No register will be updated 
- Wr en = 0 (inactive)
![image](https://hackmd.io/_uploads/BkAq9S8axe.png)

---

## Signal Summary Table

To avoid repetition, here is a summary table of all instruction types:

| Instruction Type | Reg Wr | Mem Wr | Mem to Reg | ALU | Branch |  
|-----------------|----------|----------|----------|--------|--------|
| `lw` (Load) | 1 | 0 | 1 | 1 | 0 |  
| `sw` (Store) | 0 | 1 | X | 1 | 0 | 
| `sub` (R-type) | 1 | 0 | 0 | 1 | 0 |  
| `beq` (B-type) | 0 | 0 | X | 0 | 1 |  
| `jal` (Jump) | 1 | 0 | 1 | X | 0 | 

**Legend**: 1 = active, 0 = inactive, X = don't care

---

## Processor Hazard Handling

### Data Hazard Resolution
This section was not originally planned, but an important observation was made during the SW instruction presentation. This differs from the processor configuration used at my home institution.

The code was initially modified to present SW in an ideal situation:

```riscv
encode_uf8:
    addi x21, x21, 0
    sw x20, 0(x21)
```
But the actual code is:
```riscv
encode_uf8:
    addi x21, x21, -4
    sw x20, 0(x21)
```
This modifies the stack pointer to avoid overwriting data (standard stack behavior).

**Problem**: The instruction needs the updated value of x21 before it has been written back.

**Solution in Ripes**: 
![image](https://hackmd.io/_uploads/Hkek5ZU8aeg.png)

- The `addi x21 x21 -4` instruction is in the MEM stage, so the new value of x21 is already computed
- The ALU accepts the forwarding value from the next pipeline stage
- The old value from the ID/EX pipeline is bypassed

---

### Branch Hazard Resolution

Further investigation revealed another type of hazard handling in the processor.

**Code sequence**:
```riscv
beq x5, x0, end_loop    
srl x7, x6, x5           
```

**Problem**: Branch decision is not made until the EX stage, but the processor has already fetched the next instruction.

**Solution**: 
- The next instruction is fetched to prepare for both cases:

![image](https://hackmd.io/_uploads/B1M7EULTxg.png)

- If the branch is taken, the entire pipeline is flushed:

![image](https://hackmd.io/_uploads/H1AmEULpgx.png)

---
# BF16 Operations
## SUMMARY

- **BF16 to F32 Conversion**
- **F32 to BF16 Conversion**
- **BF16 Addition**
- **BF16 Subtraction**
- **BF16 Multiplication**
- **BF16 Division**
- **BF16 Square Root**

---

# Part 1: Pure C to RISC-V Translation

## BF16 Format Overview
- **16 bits total: 1 sign bit + 8 exponent bits + 7 mantissa bits**
- Special values: Inf (exp=0xFF, mant=0), NaN (exp=0xFF, mant≠0), Zero (exp=0, mant=0) (Unlike UF8, these special values exist in BF16)

**Register Convention:**
- `x1`: result value
- `x2`: return address (ra)
- `x3`: stack pointer (sp)
- `x4-x9`: extracted information (sign, exp, mantissa)
- `x10-x15`: temporaries
- `x16-x22`: working registers

*The register convention has been updated from the UF8 implementation for improved clarity.*

---

## 1. BF16 to F32 Conversion

### Code
```riscv
main: 
    la x1, bfloat16
    lh x1, 0(x1)
    slli x4, x1, 16
    la x7, float32
    sw x4, 0(x7)
    j end
```

**Explanation:** FP32 = BF16 << 16.

---

## 2. F32 to BF16 Conversion

### Handle Special Cases
```riscv
f32_to_bf16:
    la x1, bfloat32
    lw x1, 0(x1)            
    srli x4, x1, 23
    andi x4, x4, 0x7FF      
    li x5, 0xFF
    bne x4, x5, normal_case
        srli x6, x1, 16          
        andi x6, x6, 0xFFFF      
        j return_bf16
```
### Code
```riscv    
    normal_case:
        srli x6, x1, 16          
        andi x6, x6, 1     
        addi x6, x6, 0x7FFF         
        add x6, x1, x6
        srli x6, x6, 16         
```


---

## 3. BF16 Addition

### Initialize
```riscv
init:
    srli x4, x14, 15          # sign_a
    srli x5, x15, 15          # sign_b
    srli x6, x14, 7           # exp_a
    andi x6, x6, 0xFF
    srli x7, x15, 7           # exp_b
    andi x7, x7, 0xFF
    andi x8, x14, 0x7F        # mant_a
    andi x9, x15, 0x7F        # mant_b
    jr x2
```
*This initialization pattern is reused for most operations.*

### Handle Special Cases
```riscv
add:
    jal x2, init
    li x16, 0xFF
    bne x6, x16, skip1
        bne x8, x0, return_a     # a is NaN
        bne x7, x16, skip2
            bne x9, x0, return_b  # b is NaN
            beq x4, x5, return_b  # Inf + Inf (same sign)
            li x1, 0xFFC0         # Inf + (-Inf) = NaN
            j return
        skip2:
        return_a:
            add x1, x14, x0
            j return
    skip1:
    
    li x16, 0xFF
    beq x7, x16, return_b         # b is Inf/NaN
    
    or x16, x6, x8
    beq x16, x0, return_b         # a is zero
    
    or x16, x7, x9
    beq x16, x0, return_a         # b is zero
```

### Add Implicit Bit
```riscv
    beq x6, x0, skip3
        ori x8, x8, 0x80          # add implicit 1
    skip3:
    
    beq x7, x0, skip4
        ori x9, x9, 0x80          # add implicit 1
    skip4:
```

### Align Exponents
```riscv
    sub x10, x6, x7               # exp_diff
    
    blez x10, skip5
        add x12, x6, x0           # result_exp = exp_a
        li x16, 8
        bge x10, x16, return_a    # diff too large
        srl x9, x9, x10           # align mant_b
        j skip7
    
    skip5:
    bgez x10, skip6
        add x12, x7, x0           # result_exp = exp_b
        li x16, -8
        blt x10, x16, return_b    # diff too large
        sub x16, x0, x10
        srl x8, x8, x16           # align mant_a
        j skip7
    skip6:
        add x12, x6, x0           # exponents equal
    skip7:
```

### Add/Sub Mantissas
```riscv
    bne x4, x5, skip8             # different signs
    
    add x11, x4, x0               # result_sign
    add x13, x8, x9               # add mantissas
    
    andi x16, x13, 0x100
    beq x16, x0, skip9
        srli x13, x13, 1          # normalize overflow
        addi x12, x12, 1
        li x16, 0xFF
        blt x12, x16, skip9
            slli x1, x11, 15      # overflow to Inf
            li x16, 0x7F80
            or x1, x1, x16
            j return
    skip9:
    j combine_result
    
skip8:
    blt x8, x9, skip10
        add x11, x4, x0
        sub x13, x8, x9
        j skip11
    skip10:
        add x11, x5, x0
        sub x13, x9, x8
    skip11:
    
    beq x13, x0, return_zero
    
skip12:
    andi x16, x13, 0x80
    bne x16, x0, skip13
        slli x13, x13, 1          # normalize
        addi x12, x12, -1
        blez x12, return_zero
        j skip12
skip13:
```

### Combine Result
```riscv
combine_result:
    slli x1, x11, 15              # sign << 15
    andi x16, x12, 0xFF
    slli x16, x16, 7              # exp << 7
    or x1, x1, x16
    andi x16, x13, 0x7F           # mantissa
    or x1, x1, x16
    j return
```

---

## 4. BF16 Subtraction

### Code
```riscv
sub:
    li x16, 0x8000
    xor x15, x15, x16             # flip sign of b
    j add                         # reuse addition
```
**Context:** Subtraction is in the same program as addition (selected during test execution).

**Explanation:** Subtraction is implemented as addition with the sign of the second operand flipped.

---

## 5. BF16 Multiplication

### Initialize
```riscv
init:
    srli x4, x10, 15              # sign_a
    srli x5, x11, 15              # sign_b
    srli x6, x10, 7
    andi x6, x6, 0xFF             # exp_a
    srli x7, x11, 7
    andi x7, x7, 0xFF             # exp_b
    andi x8, x10, 0x7F            # mant_a
    andi x9, x11, 0x7F            # mant_b
    xor x12, x4, x5               # result_sign
    jr x2
```

### Handle Special Cases
```riscv
mul:
    jal x2, init
    
    li x16, 0xFF
    bne x6, x16, skip1
        bnez x8, return_a         # a is NaN
        or x17, x7, x9
        beqz x17, return_NAN      # Inf * 0 = NaN
        slli x1, x12, 15
        li x16, 0x7F80
        or x1, x1, x16            # return Inf
        j return
    skip1:
    
    li x16, 0xFF
    bne x7, x16, skip2
        bnez x9, return_b         # b is NaN
        or x17, x6, x8
        beqz x17, return_NAN      # 0 * Inf = NaN
        slli x1, x12, 15
        li x16, 0x7F80
        or x1, x1, x16            # return Inf
        j return
    skip2:
    
    or x17, x6, x8
    or x16, x7, x9
    bnez x17, skip3
        slli x1, x12, 15          # return signed zero
        j return
    skip3:
    bnez x16, skip4
        slli x1, x12, 15
        j return
    skip4:
```

### Normalize 
```riscv
    li x13, 0                     # exponent adjustment
    
    bnez x6, skip5
        normalize_a:
            andi x16, x8, 0x80
            bnez x16, skip6
                slli x8, x8, 1
                addi x13, x13, -1
                j normalize_a
        skip6:
        li x6, 1
        j skip7
    skip5:
        ori x8, x8, 0x80          # add implicit bit
    skip7:
    
    bnez x7, skip8
        normalize_b:
            andi x16, x9, 0x80
            bnez x16, skip9
                slli x9, x9, 1
                addi x13, x13, -1
                j normalize_b
        skip9:
        li x7, 1
        j skip10
    skip8:
        ori x9, x9, 0x80
    skip10:
```

### Multiply Mantissas
```riscv
    li x14, 0
    li x17, 0
    mul_loop:                     # result_mant = mant_a * mant_b
        li x18, 8
        beq x17, x18, mul_end
        andi x18, x9, 1
        beqz x18, mul_skip
            add x14, x14, x8
        mul_skip:
        slli x8, x8, 1
        srli x9, x9, 1
        addi x17, x17, 1
        j mul_loop
    mul_end:               
    
    add x15, x6, x7
    li x16, 127
    sub x15, x15, x16             # result_exp = exp_a + exp_b - 127
    add x15, x15, x13
```

### Normalize Result
```riscv
    li x16, 0x8000
    and x17, x14, x16
    beqz x17, skip11
        srli x14, x14, 8
        andi x14, x14, 0x7F
        addi x15, x15, 1          # bit 15 set, shift right
        j skip12
    skip11:
        srli x14, x14, 7          # bit 15 not set
        andi x14, x14, 0x7F
    skip12:
```

### Handle Overflow/Underflow
```riscv
    li x16, 0xFF
    blt x15, x16, skip13
        slli x1, x12, 15
        li x16, 0x7F80
        or x1, x1, x16            # overflow to Inf
        j return
    skip13:
    
    bgtz x15, skip14
        li x17, -6
        blt x15, x17, skip15
            li x16, 1
            sub x16, x16, x15
            srl x14, x14, x16     # denormalize
            li x15, 0
            j skip14
        skip15:
            slli x1, x12, 15      # underflow to zero
            j return
    skip14:
```

### Combine Result
```riscv
    slli x1, x12, 15
    andi x16, x15, 0xFF
    slli x16, x16, 7
    or x1, x1, x16
    andi x16, x14, 0x7F
    or x1, x1, x16
    j return
```

---

## 6. BF16 Division

### Initialize
```riscv
init:
    srli x4, x10, 15
    srli x5, x11, 15
    srli x6, x10, 7
    andi x6, x6, 0xFF
    srli x7, x11, 7
    andi x7, x7, 0xFF
    andi x8, x10, 0x7F
    andi x9, x11, 0x7F
    xor x12, x4, x5               # result_sign
    jr x2
```

### Handle Special Cases
```riscv
div:
    jal x2, init
    
    li x16, 0xFF
    bne x7, x16, skip1
        bnez x9, return_b         # b is NaN
        li x16, 0xFF
        bne x6, x16, skip1_1
            bnez x8, skip1_1
                li x1, 0x7FC0     # Inf / Inf = NaN
                j return
        skip1_1:
        slli x1, x12, 15          # x / Inf = 0
        j return
    skip1:
    
    or x17, x7, x9
    bnez x17, skip2
        or x17, x6, x8
        bnez x17, skip2_1
            li x1, 0x7FC0         # 0 / 0 = NaN
            j return
        skip2_1:
        slli x1, x12, 15
        li x16, 0x7F80
        or x1, x1, x16            # x / 0 = Inf
        j return
    skip2:
    
    li x16, 0xFF
    bne x6, x16, skip3
        bnez x8, return_a         # a is NaN
        slli x1, x12, 15
        li x16, 0x7F80
        or x1, x1, x16            # Inf / x = Inf
        j return
    skip3:
    
    or x17, x6, x8
    bnez x17, skip4
        slli x1, x12, 15          # 0 / x = 0
        j return
    skip4:
```

### Add Implicit Bit
```riscv
    bnez x6, skip5
        ori x8, x8, 0x80
        j skip6
    skip5:
        ori x8, x8, 0x80
    skip6:
    
    bnez x7, skip7
        ori x9, x9, 0x80
        j skip8
    skip7:
        ori x9, x9, 0x80
    skip8:
```

### Integer Division 
```riscv
    slli x13, x8, 15              # dividend = mant_a << 15
    mv x14, x9                    # divisor = mant_b
    li x15, 0                     # quotient = 0
    li x18, 0                     # loop counter
    
    loop:
        li x19, 16
        beq x18, x19, end_loop
        slli x15, x15, 1
        li x20, 15
        sub x20, x20, x18
        sll x21, x14, x20
        bltu x13, x21, skip9
            sub x13, x13, x21
            ori x15, x15, 1
        skip9:
        addi x18, x18, 1
        j loop
    end_loop:
```

### Calculate Exponent
```riscv
    sub x22, x6, x7
    addi x22, x22, 127            # result_exp = exp_a - exp_b + 127
    bnez x6, skip10
        addi x22, x22, -1         # adjust for denormal a
    skip10:
    bnez x7, skip11
        addi x22, x22, 1          # adjust for denormal b
    skip11:
```

### Normalize Result
```riscv
    li x16, 0x8000
    and x17, x15, x16
    beqz x17, normalize_loop_start
        srli x15, x15, 8          # already normalized
        j skip12
    
    normalize_loop_start:
    loop_2:
        li x16, 0x8000
        and x17, x15, x16
        bnez x17, end_loop_2
            li x19, 1
            ble x22, x19, end_loop_2
            slli x15, x15, 1
            addi x22, x22, -1
            j loop_2
    end_loop_2:
        srli x15, x15, 8
    skip12:
    
    andi x15, x15, 0x7F
```

### Handle Overflow/Underflow
```riscv
    li x16, 0xFF
    blt x22, x16, skip13
        slli x1, x12, 15
        li x16, 0x7F80
        or x1, x1, x16            # overflow to Inf
        j return
    skip13:
    
    bgtz x22, skip14
        slli x1, x12, 15          # underflow to zero
        j return
    skip14:
```

### Combine Result
```riscv
    slli x1, x12, 15
    andi x22, x22, 0xFF
    slli x22, x22, 7
    or x1, x1, x22
    andi x15, x15, 0x7F
    or x1, x1, x15
    j return
```

---

## 7. BF16 Square Root 
**Disclaimer:**
AI assistance was used for this implementation due to difficulties encountered. However, even with AI support, challenges remained because both the AI and I struggled with certain RISC-V assembly aspects.

A working version using the RV32M extension (multiplication/division instructions) is presented here, as it may be useful for future assignments.

**Note: If you wish to review the non-working pure RV32I version with infinite loops, please check the GitHub repository.**

### Initialize
```riscv
init:
    srli x4, x10, 15              # sign
    srli x5, x10, 7
    andi x5, x5, 0xFF             # exponent
    andi x6, x10, 0x7F            # mantissa
    jr x2
```

### Handle Special Cases
```riscv
sqrt:
    jal x2, init
    
    li x7, 0xFF
    bne x5, x7, skip1
        bnez x6, return_a         # NaN
        bnez x4, return_nan       # sqrt(-Inf)
        j return_a                # sqrt(+Inf)
    skip1:
    
    or x7, x5, x6
    bnez x7, skip2
        li x1, 0                  # sqrt(0) = 0
        j return
    skip2:
    
    bnez x4, return_nan           # sqrt(negative)
    
    bnez x5, skip3
        li x1, 0                  # sqrt(denormal) ≈ 0
        j return
    skip3:
```

### Calculate Square Root
```riscv
    li x7, 127
    sub x8, x5, x7                # e = exp - 127
    
    ori x11, x6, 0x80             # m = mantissa with implicit bit
    
    andi x7, x8, 1
    beqz x7, skip4
        slli x11, x11, 1          # if e is odd, m *= 2
        addi x8, x8, -1
        srai x9, x8, 1
        addi x9, x9, 127          # new_exp = (e-1)/2 + 127
        j skip5
    skip4:
        srai x9, x8, 1
        addi x9, x9, 127          # new_exp = e/2 + 127
    skip5:
```

### Binary Search for Square Root
```riscv
    li x12, 90                    # low = 90
    li x13, 256                   # high = 256
    li x14, 128                   # result_sqrt = 128
    
    while_loop:
        bltu x13, x12, end_loop
        add x15, x12, x13
        srli x15, x15, 1          # mid = (low + high) / 2
        mul x18, x15, x15
        li x7, 128
        divu x18, x18, x7         # sq = mid^2 / 128
        bltu x11, x18, skip6
            mv x14, x15
            addi x12, x15, 1      # low = mid + 1
            j skip7
        skip6:
            addi x13, x15, -1     # high = mid - 1
        skip7:
        j while_loop
    end_loop:
```

### Normalize Result
```riscv
    li x7, 256
    blt x14, x7, skip8
        srli x14, x14, 1
        addi x9, x9, 1
        j skip9
    skip8:
        li x7, 128
        bge x14, x7, skip9
        loop_2:
            li x7, 128
            bge x14, x7, skip10
                li x16, 1
                ble x9, x16, skip10
                slli x14, x14, 1
                addi x9, x9, -1
                j loop_2
        skip10:
    skip9:
    
    andi x14, x14, 0x7F
```

### Handle Overflow/Underflow
```riscv
    li x7, 0xFF
    bge x9, x7, return_inf
    
    blez x9, return_zero
```

### Combine Result
```riscv
    andi x9, x9, 0xFF
    slli x9, x9, 7
    or x1, x9, x14
    j return
```

---

# Part 2: Validation & Testing

## Test Program

```riscv
.data
test_a: .word 0x3F80    # 1.0
test_b: .word 0x4000    # 2.0

result_add1: .word 0

msg_test1: .string "Test Add 1.0+2.0: "
msg_pass: .string "PASS\n"
msg_fail: .string "FAIL\n"

.text
.global main

main:
    la a0, msg_test1
    li a7, 4
    ecall
    la x14, test_a
    lw x14, 0(x14)
    la x15, test_b
    lw x15, 0(x15)
    jal x2, add
    la x7, result_add1
    sw x1, 0(x7)
    li x16, 0x4040
    beq x1, x16, test1_pass
    la a0, msg_fail
    li a7, 4
    ecall
    j test2
test1_pass:
    la a0, msg_pass
    li a7, 4
    ecall

test2: ...
```
**Note: Eight different tests can be executed in the q1-bf16-test.s file (excluding sqrt).**

---

# Part 3: Performance Analysis

**Initially, this analysis was intended to simply compare instruction counts. However, the goal evolved to surpass C compiler performance by leveraging RISC-V architecture-specific optimizations.** This addresses the assignment's directive: *"Leverage the strengths of the RISC-V architecture for efficiency."*

## BF16 Addition Performance Comparison

The following comparison uses a simple test case:
- **Operation: result = a + b**
- **a = 1.0 (BF16: 0x3F80)**
- **b = 2.0 (BF16: 0x4000)**

### Initial Attempt: C with -O2 (Inline Disabled)

The first approach was to examine the assembly generated with -O2 optimization. However, the compiler inlined the function by default. To obtain a fair comparison, inlining was disabled using compiler directives.

The resulting x86-64 assembly (truncated for brevity):

```assembly
bf16_add:
.LFB19:
	.cfi_startproc
	movl	%edi, %ecx
	movl	%esi, %edx
	movl	%edi, %r11d
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	shrw	$7, %cx
	pushq	%rbx
	.cfi_def_cfa_offset 24
	.cfi_offset 3, -24
	shrw	$7, %dx
	movl	%esi, %ebx
	movl	%esi, %r10d
	movzbl	%cl, %ecx
	movl	%edi, %r8d
	movl	%edi, %eax
	shrw	$15, %r11w
	shrw	$15, %bx
	movzbl	%dl, %edx
	andl	$127, %edi
	andl	$127, %r10d
	cmpw	$255, %cx
	je	.L55
	...
	.cfi_endproc
```

### Performance Results

After counting instructions and estimating cycle counts with AI assistance and manual verification:

| Implementation | Instructions | Cycles |
|----------------|--------------|--------|
| **RISC-V (manual)** 👑 | **21** | **37** |
| C (-O2, no inline) | 51 | 81 |

**The RISC-V implementation wins!** However, this comparison has significant caveats:

1. **Non-inline compilation** was forced for analysis purposes
2. **-O2 optimization** was used instead of maximum -O3
3. **x86-64 architecture** uses many registers and is designed for 64-bit operations with additional security features compared to the minimal RISC-V implementation

### Adding Inline Optimization

| Implementation | Instructions | Cycles |
|----------------|--------------|--------|
| **RISC-V (manual)** 👑 | **21** | **37** |
| C (-O2, no inline) | 51 | 81 |
| C (-O2, inline) | 58 | 72 |

**Interesting observation:** More instructions but fewer cycles due to better instruction scheduling and elimination of function call overhead.

### Maximum Optimization: -O3

| Implementation | Instructions | Cycles |
|----------------|--------------|--------|
| **RISC-V (manual)** 👑 | **21** | **37** |
| C (-O2, no inline) | 51 | 81 |
| C (-O2, inline) | 58 | 72 |
| C (-O3) | 42 | 48 |

**Why does -O3 perform better?**

1. **The compiler integrates the function directly into main**, eliminating all function call overhead
2. **Common case optimization**: The compiler reorders code to place the most frequent execution paths first, minimizing branch mispredictions

### Lessons Learned

The performance advantage of the manual RISC-V implementation comes from:

1. **Minimal overhead**: No function call prologue/epilogue when integrated properly
2. **RISC-V simplicity**: The reduced instruction set means each operation is optimized for the architecture
3. **Hand-tuned paths**: Critical paths (common cases) can be manually optimized to avoid unnecessary branches

However, to achieve truly superior performance, the implementation should:
- **Place rare cases (like infinity/NaN handling) at the end** to avoid polluting the instruction cache
- **Optimize for the common case first** (normal additions) with minimal branching
- **Use pipeline-friendly instruction sequences** that avoid data hazards

---

# Conclusion

## Summary of Achievements

This assignment successfully implemented:
1. **UTF8 encoding/decoding** with complete test coverage
2. **Comprehensive processor pipeline analysis** demonstrating understanding of hazards and forwarding
3. **Complete BF16 floating-point operations** (conversion, arithmetic, and square root)

## Insights

### 1. Assembly Programming Complexity
Writing assembly code revealed the complexity. Even simple operations require extensive error handling, normalization, and edge case management.

### 2. Performance Characteristics
The analysis demonstrated that:
- **Instruction count alone is misleading** - cycle count matter more
- **Compiler optimizations are sophisticated** - achieving better performance than -O3 requires deep architectural knowledge (but not in tose simple case)
- **Hand-optimization has limits** - while the RISC-V implementation was competitive, the margin diminishes with higher compiler optimization levels

### 3. Architecture-Specific Design
The RISC-V architecture's strength lies in:
- **Simple, uniform instruction format** enabling fast decode
- **Large register file** reducing memory traffic
- **Explicit pipeline visibility** allowing manual optimization of data hazards


## Future Improvements

For future assignments:

1. **Code Organization**: Implement better naming conventions and modular structure
2. **Optimization Strategy**: Focus on common-case performance rather than exhaustive edge case 
3. **Documentation**: Maintain clearer separation between implementation notes and formal analysis

## Final Reflection

This assignment was enlightening.
While initially frustrating (Due to simple translation of C to risc ,especially the sqrt implementation), it provided invaluable insight into low-level computation, compiler optimization, and computer architecture. The iterative process of writing, testing, and optimizing assembly code, though time-consuming, has deepened my understanding of low levels languages.

The realization that manual RISC-V code could compete with (and occasionally exceed) compiled C code was unexpected, even if this advantage diminishes at higher optimization levels. 

I don't consider this homerwork as good (I mean that i didn't provided a pretty good homework) but this exercise has prepared me well for future work i think.

---


**End of Report**