scoreboarding
Pure-Python cycle-exact simulator of Thornton's Scoreboarding algorithm, as implemented in the CDC 6600 (1964). Designed for computer architecture education: readable source, zero runtime dependencies, and per-instruction cycle-number traces.
i built this because the algorithm only really clicked once i could watch each hazard clear one cycle at a time.
What is Scoreboarding?
Scoreboarding is an in-order issue, out-of-order execution dynamic scheduling technique. The processor issues instructions one at a time (program order), but lets them read operands and execute independently once their hazards clear. A central scoreboard -- three tables -- tracks every in-flight instruction and enforces the classic CDC 6600 hazard rules without any register renaming.
The Four Stages
| Stage | What happens | Hazard checked |
|---|---|---|
| Issue | Assign instruction to a free FU | Structural (no free FU of the kind) + WAW (another active insn writes same dest) |
| Read Operands | Read both source registers | RAW (stall until producing FU has written result) |
| Execute | Occupy the FU for its full latency | -- |
| Write Result | Commit result to register file, free FU | WAR (stall until every earlier reader has read its operand) |
Three Tracking Tables
- Instruction Status -- per-instruction cycle stamps (Issue / ReadOperands / ExecuteComplete / WriteResult).
- Functional Unit Status -- per-FU: busy flag, op, destination (Fi), sources (Fj, Fk), producing FUs (Qj, Qk), ready flags (Rj, Rk).
- Register Result Status -- which FU will next write each register (None once written).
Pipelined vs unpipelined functional units
Each FunctionalUnit declares whether its execute stage is pipelined.
- Unpipelined (
pipelined=False, the classic CDC 6600 default): the unit is a structural hazard for its entire lifetime. It stays busy from Issue through Write Result, so a same-kind successor cannot issue to it until the occupying instruction has written its result. This is the original scoreboard behaviour. - Pipelined (
pipelined=True): the unit frees its issue slot as soon as the occupying instruction has read its operands and entered the execute pipeline. A same-kind successor can then issue the next cycle while the deep execute pipeline still carries the earlier result. The structural stall is shorter; RAW, WAR, and WAW hazards are unaffected.
For example, two independent back-to-back multiplies on a single latency=4
multiply unit:
| Unpipelined | Pipelined | |
|---|---|---|
| MULT #1 issue / write | 1 / 7 | 1 / 7 |
| MULT #2 issue / write | 7 / 13 | 2 / 8 |
| Total cycles | 13 | 8 |
The pipelined unit issues the second multiply at cycle 2 (right after the first reads operands) instead of waiting until cycle 7 for the first to write back.
How it differs from Tomasulo
| Scoreboarding | Tomasulo | |
|---|---|---|
| Issue order | In order | In order |
| Execution order | Out of order | Out of order |
| WAW handling | Stall Issue | Register renaming (RS tags) |
| WAR handling | Stall Write Result | Eliminated by renaming |
| RAW handling | Stall Read Operands | Stall in RS until CDB broadcast |
| Register renaming | No | Yes (via reservation stations) |
| Broadcast mechanism | Central scoreboard | Common Data Bus |
See the sibling package tomasulo for the Tomasulo out-of-order scheduler with register renaming.
Install
# From source (until PyPI release):
git clone https://github.com/amaar-mc/scoreboarding
cd scoreboarding
uv pip install -e ".[dev]"
Usage
Python API
from scoreboarding import FunctionalUnit, Instruction, run, render_trace
fus = [
FunctionalUnit(name="Load1", kind="load", latency=2, pipelined=False),
FunctionalUnit(name="Mult1", kind="mult", latency=10, pipelined=False),
FunctionalUnit(name="Add1", kind="add", latency=2, pipelined=False),
FunctionalUnit(name="Div1", kind="div", latency=40, pipelined=False),
]
program = [
Instruction(op="LD", dest="F6", src1="R2", src2=""),
Instruction(op="LD", dest="F2", src1="R3", src2=""),
Instruction(op="MULT", dest="F0", src1="F2", src2="F4"),
Instruction(op="SUB", dest="F8", src1="F6", src2="F2"),
Instruction(op="DIV", dest="F10", src1="F0", src2="F6"),
Instruction(op="ADD", dest="F6", src1="F8", src2="F2"),
]
trace = run(program, functional_units=fus)
print(render_trace(trace))
Example timing table
+---------------------+-------+---------+----------+-------------+
| Instruction | Issue | ReadOps | ExecComp | WriteResult |
+---------------------+-------+---------+----------+-------------+
| LD F6, R2 | 1 | 1 | 2 | 3 |
| LD F2, R3 | 3 | 3 | 4 | 5 |
| MULT F0, F2, F4 | 4 | 5 | 14 | 15 |
| SUB F8, F6, F2 | 4 | 5 | 6 | 7 |
| DIV F10, F0, F6 | 5 | 15 | 54 | 55 |
| ADD F6, F8, F2 | 8 | 8 | 9 | 16 |
+---------------------+-------+---------+----------+-------------+
Total cycles: 16
Per-cycle snapshots
Pass capture_snapshots=True to run() to capture scoreboard state after every
cycle, then render any CycleSnapshot with render_snapshot():
from scoreboarding import render_snapshot
trace = run(program, functional_units=fus, capture_snapshots=True)
print(render_snapshot(trace.snapshots[0]))
-- Cycle 1 --
Instruction status
# Issue ReadOps ExecComp WriteResult
- ----- ------- -------- -----------
0 1 - - -
1 - - - -
2 - - - -
3 - - - -
4 - - - -
5 - - - -
Functional unit status
FU Busy Op Fi Fj Fk Qj Qk Rj Rk ExecStart
----- ---- -- -- -- -- -- -- --- --- ---------
Add1 no - - - - - - no no -
Div1 no - - - - - - no no -
Load1 yes LD F6 R2 - - - yes yes -
Mult1 no - - - - - - no no -
Register result status
Register Producer
-------- --------
F6 Load1
render_trace() renders the summary timing table; render_snapshot() renders
the full three-table scoreboard state for a single cycle.
CLI
# Use the bundled example:
scoreboarding examples/classic.txt
# Enable per-cycle snapshots:
scoreboarding --snapshots examples/classic.txt
# Read from stdin:
cat examples/classic.txt | scoreboarding -
--snapshots prints the instruction-status, functional-unit-status, and
register-result-status tables after every cycle, so a stall can be traced back
to the exact scoreboard entry holding it up.
Program file format:
# Comments start with #
# FU <name> <kind> <latency> [pipelined|unpipelined] (default: unpipelined)
FU Load1 load 2
FU Mult1 mult 10 pipelined
FU Add1 add 2
FU Div1 div 40 unpipelined
LD F6, R2
LD F2, R3
MULT F0, F2, F4
SUB F8, F6, F2
DIV F10, F0, F6
ADD F6, F8, F2
Development
uv run pytest -q
uv run ruff check .
uv run mypy src
uv build
CI runs on Python 3.10, 3.11, 3.12, 3.13 via GitHub Actions.
License
MIT -- see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file scoreboarding-0.3.0.tar.gz.
File metadata
- Download URL: scoreboarding-0.3.0.tar.gz
- Upload date:
- Size: 967.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25f075432749dd4aca29e4562583e9ec7274044750456c8343ef80d1f9dabca8
|
|
| MD5 |
2e8f5e2d6f3d547914bed39bf695437f
|
|
| BLAKE2b-256 |
d99bb87c178f8f763061d5ab25101134a0228f1de967d580598d70a4017d1c90
|
File details
Details for the file scoreboarding-0.3.0-py3-none-any.whl.
File metadata
- Download URL: scoreboarding-0.3.0-py3-none-any.whl
- Upload date:
- Size: 17.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5558ddea5d067250da21a48e365c390a6c20e6193baa513d8894320027754eba
|
|
| MD5 |
21b13afecb7e18779566be91b6f3ba0c
|
|
| BLAKE2b-256 |
b6bf639fd3f312859ada0c2e6f649365723e0efd12e4fd5162d02f72bbea60c1
|