This site is version 0.0.1. Most of the code on this site is drafted by an agent and needs review. I apologize for the AI-speak, we are working on rewriting it!
viv-lang.org
Python Viv

A C compiler, an assembler and a RISC-V processor, all in Viv

Compile recursive Fibonacci from C to RV32IM assembly with lib/cc, assemble it to machine code with lib/rv32/asm.viv, and run the machine code on the instruction-set simulator lib/rv32/sim.viv, counting every instruction it executes. The compiler follows the step order of Rui Ueyama's chibicc; the simulator passes the riscv-tests rv32ui and rv32um suites. gallery/hardware/cc_fib.viv ties the three together and is loaded with vp.load.

The program and its result

fib_on_rv32(n) compiles the C below with n filled in, runs it, and returns the exit code, the number of instructions executed and the count of each class of instruction. The exit code is main's return value, fib(n), compared here with Python's.

import numpy as np
import vipyr as vp
from vipyr.figure import figure

cc = vp.load("gallery/hardware/cc_fib.viv")
CLASSES = ["alu", "muldiv", "load", "store", "branch", "jump", "system"]
print(
    "int fib(int x) { if (x <= 1) return x; "
    "return fib(x - 1) + fib(x - 2); }"
)
print("int main() { return fib(n); }")
print("assembly lines for the program:", cc.asm_lines(10))


def fib(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a


ns = np.arange(0, 17)
runs = np.array([cc.fib_on_rv32(int(n)) for n in ns])
results, steps, mix = runs[:, 0], runs[:, 1], runs[:, 2:]
assert all(results[k] == fib(n) for k, n in enumerate(ns))
print("fib(n) from the simulator matches Python for n = 0 ..", ns[-1])
int fib(int x) { if (x <= 1) return x; return fib(x - 1) + fib(x - 2); }
int main() { return fib(n); }
assembly lines for the program: 84
fib(n) from the simulator matches Python for n = 0 .. 16

Instructions grow with the number of calls

fib(n) makes 2 fib(n+1) - 1 calls of fib: fib(n+1) of them are leaves (x <= 1) and the rest recurse. Per leaf and recursing call together the code runs 72 instructions, so the total is exactly 36 per call plus a constant 5 for the start-up code and main: the cost of one call in this compiler's stack-machine code.

calls = np.array([2 * fib(int(n) + 1) - 1 for n in ns])
assert np.array_equal(steps, 36 * calls + 5)
print("instructions = 36 * calls + 5 for every n")
print("  n   fib(n)   calls   instructions   per call")
for n, f, c, s in zip(ns, results, calls, steps):
    print("%3d %8d %7d %14d %10.2f" % (n, f, c, s, s / c))
fig = figure("Instructions executed for fib(n)", "n", "instructions")
fig.yscale("log")
fig.plot(ns, steps, "instructions")
fig.plot(ns, calls, "calls of fib")
fig.style("instructions", "s1 thick")
fig.style("calls of fib", "s2")
instructions = 36 * calls + 5 for every n
  n   fib(n)   calls   instructions   per call
  0        0       1             41      41.00
  1        1       1             41      41.00
  2        1       3            113      37.67
  3        2       5            185      37.00
  4        3       9            329      36.56
  5        5      15            545      36.33
  6        8      25            905      36.20
  7       13      41           1481      36.12
  8       21      67           2417      36.07
  9       34     109           3929      36.05
 10       55     177           6377      36.03
 11       89     287          10337      36.02
 12      144     465          16745      36.01
 13      233     753          27113      36.01
 14      377    1219          43889      36.00
 15      610    1973          71033      36.00
 16      987    3193         114953      36.00
Instructions executed for fib(n)instructionscalls of fib10⁰10²10⁴051015ninstructions

What the processor spends them on

The instruction mix of fib(16). The code generator keeps every intermediate value on the stack (push before a binary operator, pop after), so loads and stores are a large share; branches come from the if, jumps from calls and returns. There is no multiply or divide in this program, so the M-extension column is empty.

last = mix[-1]
for name, count in zip(CLASSES, last):
    print("%-7s %7d  %5.1f%%" % (name, count, 100.0 * count / steps[-1]))
fig = figure("Instruction mix of fib(16)", "", "instructions")
fig.bars(np.arange(len(CLASSES)), last, "count")
for k, name in enumerate(CLASSES):
    fig.xtick(name, k)
alu       55878   48.6%
muldiv        0    0.0%
load      25544   22.2%
store     20755   18.1%
branch     3193    2.8%
jump       9582    8.3%
system        1    0.0%
Instruction mix of fib(16)count02000040000alumuldivloadstorebranchjumpsysteminstructions

Credits

  • The C compiler follows chibicc by Rui Ueyama (MIT); the simulator is tested with the riscv-tests suites (BSD-3-Clause).

Source

gallery/hardware/plot_fib_on_rv32.py, run as written when these pages were built. The output and the figures above are from that run.

The Viv program it calls is gallery/hardware/cc_fib.viv: its atlas, the defs and the calls between them, or open it in the workbench.