Homework 6

Computer Architecture I @ ShanghaiTech University

Objective

In this assignment, you will implement a simplified RISC-V simulator and a single-level unified cache. Through this task, you will observe performance issues caused by using a shared cache for both instructions and data, which will help you understand why modern processors use separate L1 instruction and data caches.

Before starting, accept the assignment from GitHub Classroom.

Code Structure

Part 1: RISC-V Simulator

Implement a basic RISC-V simulator to execute assembly code. Your simulator must:

  1. Support a subset of RISC-V instructions:
    • Refer to enum Instruction_type in simulate.h for the list of instructions you need to support.
    • You do not have to support instructions not in the list.
  2. Maintain:
    • 32 integer registers (x0x31).
    • Program counter (PC).
  3. Parse and execute input assembly code sequentially.
    • Finish a simple parser. We have finished some helper macros for you. If you do not understand what they do, you can refer to the GCC manual.
    • Assume instructions are preloaded into memory starting at address 0x0.
    • For simplicity, treat all values as integers and ignore pipeline effects.

Input Assembly Pattern

All test cases are a simple for-loop and will follow this structure:

  1. The assembly will implement a loop similar to this C structure:
    for (i = 0; i < n; ++i) {
        // Operations on a[i] and/or b[i]
    }
  2. Initialization (first 4 instructions):
    li t0, n
    li t1, 0
    li a0, 0x100
    li a1, 0x200
    Where t0 is the loop counter limit (n iterations), t1 is the loop variable i, a0 and a1 are the base address of array a and b.
  3. Simplifications:
    • No label resolution needed − the assembly code contains no labels and all branch offsets are precomputed integers.
    • The assembly code contains no comments.
    • The assembly code contains no invalid instructions.
    • All load/store instructions are aligned to 4-byte boundaries. This guarantees that no memory access straddles two cache lines (Why?), simplifying your implementation.
    • When simulating lw/sw instructions, only calculate the accessed memory address. Do not store/load actual values to/from registers or memory.
      • Your cache simulator's results depend only on memory access patterns, not the actual data values.
      • This reduces implementation complexity and aligns with the assignment's focus on cache behavior.
    • When simulating the li instruction, you do not have to expand the pseudo-instruction, simply load the immediate into the register. The immediate is guaranteed to be in the range [-2048, 2047].

Part 2: Unified Cache Simulator

Implement a cache simulator to track:

Cache Specifications

Implementation Steps

  1. On each instruction fetch, check if the instruction address exists in the cache.
    • If yes: increment the counter for instruction cache hits.
    • If no: evict a cache line using the LRU algorithm, and load the block into the cache and count it as a miss.
  2. On each lw/sw operation, check if the data address exists in the cache.
    • If yes: increment the counter for data cache hits.
    • If no: evict a cache line using the LRU algorithm, and load the block into the cache and count it as a miss.

Output

After executing the input assembly, print:

Total memory accesses: [value].
Instruction cache hit: [value].
Data cache hit: [value].
Total cache misses: [value].

Sample & Testing

  1. Example Code

    We provide a sample assembly code sample/vec_add.S (vector addition). Reference output of the assembly code:

    Total memory accesses: 228.
    Instruction cache hit: 143.
    Data cache hit: 12.
    Total cache misses: 73.

    Debug logs are in sample/main.log.

  2. How to Test
    • Compile: Run make to build the executable main.
    • Execute ./main <assembly code to run> 2>main.log.
    • Program output (instruction/data hits and misses) will print to stdout.
    • Debug logs (e.g., cache access details) will be saved to main.log.
  3. Tips
    • While GDB is powerful, simple print statements are often faster for tracing cache behavior.
    • Use fprintf(stderr, ...) for debug prints to avoid mixing diagnostics with program output.
      • Example: fprintf(stderr, "Cache hit at address 0x%x\n", addr);
      • This keeps your final results (printf to stdout) clean and readable.

Submission Guidelines

This assignment will deepen your understanding of cache architecture trade-offs. Good luck!

Optional: Compare with Split Caches

You can use Venus' Cache Simulator to observe the performance benefits of separating instruction/data caches: