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.
Metadata
Release files for scoreboarding 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| scoreboarding-0.3.0.tar.gz | 967.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| scoreboarding-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 984.5 kB
Release files / scoreboarding-0.3.0.tar.gz
| Download URL | scoreboarding-0.3.0.tar.gz |
|---|---|
| Size | 967.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
25f075432749dd4aca29e4562583e9ec7274044750456c8343ef80d1f9dabca8
|
|
BLAKE2b-256 checksum How to use checksums |
d99bb87c178f8f763061d5ab25101134a0228f1de967d580598d70a4017d1c90
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / scoreboarding-0.3.0-py3-none-any.whl
| Download URL | scoreboarding-0.3.0-py3-none-any.whl |
|---|---|
| Size | 17.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5558ddea5d067250da21a48e365c390a6c20e6193baa513d8894320027754eba
|
|
BLAKE2b-256 checksum How to use checksums |
b6bf639fd3f312859ada0c2e6f649365723e0efd12e4fd5162d02f72bbea60c1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|