bpred 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bpred/__init__.py +42 -0
- bpred/bimodal.py +70 -0
- bpred/cli.py +167 -0
- bpred/counter.py +70 -0
- bpred/gshare.py +94 -0
- bpred/perceptron.py +183 -0
- bpred/py.typed +0 -0
- bpred/tournament.py +137 -0
- bpred/trace.py +95 -0
- bpred-0.2.0.dist-info/METADATA +172 -0
- bpred-0.2.0.dist-info/RECORD +14 -0
- bpred-0.2.0.dist-info/WHEEL +4 -0
- bpred-0.2.0.dist-info/entry_points.txt +2 -0
- bpred-0.2.0.dist-info/licenses/LICENSE +21 -0
bpred/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""bpred -- Pure-Python CPU branch predictor simulator.
|
|
2
|
+
|
|
3
|
+
Provides four classical branch predictors and trace-driven simulation
|
|
4
|
+
utilities for computer architecture education.
|
|
5
|
+
|
|
6
|
+
Predictors
|
|
7
|
+
----------
|
|
8
|
+
BimodalPredictor
|
|
9
|
+
Smith (1981) table of saturating counters indexed by PC.
|
|
10
|
+
GsharePredictor
|
|
11
|
+
McFarling (1993) global-history XOR predictor.
|
|
12
|
+
TournamentPredictor
|
|
13
|
+
McFarling (1993) / Alpha 21264-style meta-selecting predictor.
|
|
14
|
+
PerceptronPredictor
|
|
15
|
+
Jimenez and Lin (2001) table of integer-weight perceptrons.
|
|
16
|
+
|
|
17
|
+
Functions
|
|
18
|
+
---------
|
|
19
|
+
run_trace(predictor, *, trace)
|
|
20
|
+
Feed a list of (pc, taken) pairs through a predictor.
|
|
21
|
+
accuracy(*, trace_result)
|
|
22
|
+
Fraction of correctly predicted branches.
|
|
23
|
+
mispredictions(*, trace_result)
|
|
24
|
+
Count of incorrectly predicted branches.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from bpred.bimodal import BimodalPredictor
|
|
28
|
+
from bpred.gshare import GsharePredictor
|
|
29
|
+
from bpred.perceptron import PerceptronPredictor
|
|
30
|
+
from bpred.tournament import TournamentPredictor
|
|
31
|
+
from bpred.trace import TraceResult, accuracy, mispredictions, run_trace
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"BimodalPredictor",
|
|
35
|
+
"GsharePredictor",
|
|
36
|
+
"PerceptronPredictor",
|
|
37
|
+
"TournamentPredictor",
|
|
38
|
+
"TraceResult",
|
|
39
|
+
"accuracy",
|
|
40
|
+
"mispredictions",
|
|
41
|
+
"run_trace",
|
|
42
|
+
]
|
bpred/bimodal.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Bimodal (Smith) branch predictor.
|
|
2
|
+
|
|
3
|
+
Reference: J. E. Smith, "A study of branch prediction strategies," in
|
|
4
|
+
Proceedings of the 8th Annual Symposium on Computer Architecture (ISCA),
|
|
5
|
+
pp. 135-148, 1981.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from bpred.counter import SaturatingCounter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BimodalPredictor:
|
|
14
|
+
"""A table of saturating counters indexed by PC mod table_size.
|
|
15
|
+
|
|
16
|
+
Each counter independently tracks the taken/not-taken history for
|
|
17
|
+
branches that alias to the same table entry. With 2-bit counters
|
|
18
|
+
(counter_bits=2) this is the classic 2-bit predictor from Smith 1981.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_table: list[SaturatingCounter]
|
|
22
|
+
_table_size: int
|
|
23
|
+
_counter_bits: int
|
|
24
|
+
|
|
25
|
+
def __init__(self, *, counter_bits: int, table_size: int) -> None:
|
|
26
|
+
if counter_bits < 1:
|
|
27
|
+
raise ValueError(f"counter_bits must be >= 1, got {counter_bits}")
|
|
28
|
+
if table_size < 1:
|
|
29
|
+
raise ValueError(f"table_size must be >= 1, got {table_size}")
|
|
30
|
+
self._counter_bits = counter_bits
|
|
31
|
+
self._table_size = table_size
|
|
32
|
+
# Initialise all counters to weakly taken (threshold - 1 rounded up)
|
|
33
|
+
# so the predictor starts in a neutral "weakly taken" state.
|
|
34
|
+
initial = 1 << (counter_bits - 1) # weakly taken
|
|
35
|
+
self._table = [
|
|
36
|
+
SaturatingCounter(bits=counter_bits, initial=initial)
|
|
37
|
+
for _ in range(table_size)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# ------------------------------------------------------------------
|
|
41
|
+
# Properties
|
|
42
|
+
# ------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def table_size(self) -> int:
|
|
46
|
+
"""Number of entries in the prediction table."""
|
|
47
|
+
return self._table_size
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def counter_bits(self) -> int:
|
|
51
|
+
"""Bit-width of each saturating counter."""
|
|
52
|
+
return self._counter_bits
|
|
53
|
+
|
|
54
|
+
# ------------------------------------------------------------------
|
|
55
|
+
# Public interface
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def predict(self, *, pc: int) -> bool:
|
|
59
|
+
"""Return the taken prediction for the branch at *pc*."""
|
|
60
|
+
return self._table[pc % self._table_size].predict()
|
|
61
|
+
|
|
62
|
+
def update(self, *, pc: int, taken: bool) -> None:
|
|
63
|
+
"""Update the counter for the branch at *pc* with the actual outcome."""
|
|
64
|
+
self._table[pc % self._table_size].update(taken=taken)
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
return (
|
|
68
|
+
f"BimodalPredictor(counter_bits={self._counter_bits}, "
|
|
69
|
+
f"table_size={self._table_size})"
|
|
70
|
+
)
|
bpred/cli.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Command-line interface for bpred."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from bpred import (
|
|
10
|
+
BimodalPredictor,
|
|
11
|
+
GsharePredictor,
|
|
12
|
+
TournamentPredictor,
|
|
13
|
+
accuracy,
|
|
14
|
+
mispredictions,
|
|
15
|
+
run_trace,
|
|
16
|
+
)
|
|
17
|
+
from bpred.tournament import BranchPredictor
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _parse_taken(token: str) -> bool:
|
|
21
|
+
"""Convert a taken token to bool.
|
|
22
|
+
|
|
23
|
+
Accepts: 1/0, T/F, true/false (case-insensitive).
|
|
24
|
+
"""
|
|
25
|
+
normalised = token.strip().lower()
|
|
26
|
+
if normalised in {"1", "t", "true"}:
|
|
27
|
+
return True
|
|
28
|
+
if normalised in {"0", "f", "false"}:
|
|
29
|
+
return False
|
|
30
|
+
raise ValueError(f"Cannot parse taken value: {token!r}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _parse_pc(token: str) -> int:
|
|
34
|
+
"""Parse a PC value as decimal or hex (0x prefix)."""
|
|
35
|
+
token = token.strip()
|
|
36
|
+
if token.startswith("0x") or token.startswith("0X"):
|
|
37
|
+
return int(token, 16)
|
|
38
|
+
return int(token)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _load_trace(path: Path) -> list[tuple[int, bool]]:
|
|
42
|
+
"""Load a trace file with lines of the form ``<pc> <taken>``.
|
|
43
|
+
|
|
44
|
+
Blank lines and lines beginning with ``#`` are ignored.
|
|
45
|
+
"""
|
|
46
|
+
trace: list[tuple[int, bool]] = []
|
|
47
|
+
with path.open() as fh:
|
|
48
|
+
for lineno, raw in enumerate(fh, start=1):
|
|
49
|
+
line = raw.strip()
|
|
50
|
+
if not line or line.startswith("#"):
|
|
51
|
+
continue
|
|
52
|
+
parts = line.split()
|
|
53
|
+
if len(parts) != 2:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"Line {lineno}: expected '<pc> <taken>', got {line!r}"
|
|
56
|
+
)
|
|
57
|
+
pc = _parse_pc(parts[0])
|
|
58
|
+
taken = _parse_taken(parts[1])
|
|
59
|
+
trace.append((pc, taken))
|
|
60
|
+
return trace
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
64
|
+
parser = argparse.ArgumentParser(
|
|
65
|
+
prog="bpred",
|
|
66
|
+
description="Simulate classical CPU branch predictors on a branch trace.",
|
|
67
|
+
)
|
|
68
|
+
sub = parser.add_subparsers(dest="predictor", required=True)
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
# bimodal
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
bimodal = sub.add_parser("bimodal", help="Bimodal (Smith 1981) predictor")
|
|
74
|
+
bimodal.add_argument("--counter-bits", type=int, required=True)
|
|
75
|
+
bimodal.add_argument("--table-size", type=int, required=True)
|
|
76
|
+
bimodal.add_argument("tracefile", type=Path)
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
# gshare
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
gshare = sub.add_parser("gshare", help="Gshare (McFarling 1993) predictor")
|
|
82
|
+
gshare.add_argument("--history-bits", type=int, required=True)
|
|
83
|
+
gshare.add_argument("--table-size", type=int, required=True)
|
|
84
|
+
gshare.add_argument("tracefile", type=Path)
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
# tournament
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
tourn = sub.add_parser(
|
|
90
|
+
"tournament", help="Tournament (Alpha 21264-style) predictor"
|
|
91
|
+
)
|
|
92
|
+
tourn.add_argument(
|
|
93
|
+
"--local-predictor",
|
|
94
|
+
choices=["bimodal"],
|
|
95
|
+
required=True,
|
|
96
|
+
)
|
|
97
|
+
tourn.add_argument("--local-counter-bits", type=int, required=True)
|
|
98
|
+
tourn.add_argument("--local-table-size", type=int, required=True)
|
|
99
|
+
tourn.add_argument(
|
|
100
|
+
"--global-predictor",
|
|
101
|
+
choices=["gshare"],
|
|
102
|
+
required=True,
|
|
103
|
+
)
|
|
104
|
+
tourn.add_argument("--global-history-bits", type=int, required=True)
|
|
105
|
+
tourn.add_argument("--global-table-size", type=int, required=True)
|
|
106
|
+
tourn.add_argument("--meta-bits", type=int, required=True)
|
|
107
|
+
tourn.add_argument("tracefile", type=Path)
|
|
108
|
+
|
|
109
|
+
return parser
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _build_predictor(args: argparse.Namespace) -> BranchPredictor:
|
|
113
|
+
"""Construct the predictor requested by *args*."""
|
|
114
|
+
if args.predictor == "bimodal":
|
|
115
|
+
return BimodalPredictor(
|
|
116
|
+
counter_bits=args.counter_bits,
|
|
117
|
+
table_size=args.table_size,
|
|
118
|
+
)
|
|
119
|
+
if args.predictor == "gshare":
|
|
120
|
+
return GsharePredictor(
|
|
121
|
+
history_bits=args.history_bits,
|
|
122
|
+
table_size=args.table_size,
|
|
123
|
+
)
|
|
124
|
+
if args.predictor == "tournament":
|
|
125
|
+
local: BranchPredictor = BimodalPredictor(
|
|
126
|
+
counter_bits=args.local_counter_bits,
|
|
127
|
+
table_size=args.local_table_size,
|
|
128
|
+
)
|
|
129
|
+
global_: BranchPredictor = GsharePredictor(
|
|
130
|
+
history_bits=args.global_history_bits,
|
|
131
|
+
table_size=args.global_table_size,
|
|
132
|
+
)
|
|
133
|
+
return TournamentPredictor(
|
|
134
|
+
local=local,
|
|
135
|
+
global_=global_,
|
|
136
|
+
meta_bits=args.meta_bits,
|
|
137
|
+
)
|
|
138
|
+
raise ValueError(f"Unknown predictor: {args.predictor}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main() -> None:
|
|
142
|
+
"""Entry point for the ``bpred`` CLI."""
|
|
143
|
+
parser = _build_parser()
|
|
144
|
+
args = parser.parse_args()
|
|
145
|
+
|
|
146
|
+
tracefile: Path = args.tracefile
|
|
147
|
+
if not tracefile.exists():
|
|
148
|
+
print(f"error: trace file not found: {tracefile}", file=sys.stderr)
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
trace = _load_trace(tracefile)
|
|
153
|
+
except ValueError as exc:
|
|
154
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
predictor = _build_predictor(args)
|
|
158
|
+
result = run_trace(predictor, trace=trace)
|
|
159
|
+
|
|
160
|
+
acc = accuracy(trace_result=result)
|
|
161
|
+
misses = mispredictions(trace_result=result)
|
|
162
|
+
|
|
163
|
+
print(f"Predictor : {predictor!r}")
|
|
164
|
+
print(f"Branches : {result.total}")
|
|
165
|
+
print(f"Hits : {result.hits}")
|
|
166
|
+
print(f"Misses : {misses}")
|
|
167
|
+
print(f"Accuracy : {acc:.4%}")
|
bpred/counter.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Saturating counter used as the base building block for all branch predictors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SaturatingCounter:
|
|
7
|
+
"""An n-bit saturating counter in the range [0, 2^n - 1].
|
|
8
|
+
|
|
9
|
+
Prediction is *taken* when the value is >= 2^(n-1).
|
|
10
|
+
Incrementing toward 2^n - 1 models a taken branch; decrementing toward 0
|
|
11
|
+
models a not-taken branch. At the extremes the counter saturates rather
|
|
12
|
+
than wrapping.
|
|
13
|
+
|
|
14
|
+
The n=1 special case produces a 1-bit predictor with states {0, 1}.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
_value: int
|
|
18
|
+
_max: int
|
|
19
|
+
_threshold: int
|
|
20
|
+
|
|
21
|
+
def __init__(self, *, bits: int, initial: int) -> None:
|
|
22
|
+
if bits < 1:
|
|
23
|
+
raise ValueError(f"bits must be >= 1, got {bits}")
|
|
24
|
+
self._max = (1 << bits) - 1
|
|
25
|
+
self._threshold = 1 << (bits - 1)
|
|
26
|
+
if not (0 <= initial <= self._max):
|
|
27
|
+
raise ValueError(f"initial {initial} out of range [0, {self._max}]")
|
|
28
|
+
self._value = initial
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
# Properties
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def value(self) -> int:
|
|
36
|
+
"""Current counter value."""
|
|
37
|
+
return self._value
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def max_value(self) -> int:
|
|
41
|
+
"""Maximum value this counter can hold (2^bits - 1)."""
|
|
42
|
+
return self._max
|
|
43
|
+
|
|
44
|
+
# ------------------------------------------------------------------
|
|
45
|
+
# Core operations
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def predict(self) -> bool:
|
|
49
|
+
"""Return True (taken) when value >= threshold."""
|
|
50
|
+
return self._value >= self._threshold
|
|
51
|
+
|
|
52
|
+
def increment(self) -> None:
|
|
53
|
+
"""Increment toward max_value (saturating)."""
|
|
54
|
+
if self._value < self._max:
|
|
55
|
+
self._value += 1
|
|
56
|
+
|
|
57
|
+
def decrement(self) -> None:
|
|
58
|
+
"""Decrement toward 0 (saturating)."""
|
|
59
|
+
if self._value > 0:
|
|
60
|
+
self._value -= 1
|
|
61
|
+
|
|
62
|
+
def update(self, *, taken: bool) -> None:
|
|
63
|
+
"""Increment on taken, decrement on not-taken."""
|
|
64
|
+
if taken:
|
|
65
|
+
self.increment()
|
|
66
|
+
else:
|
|
67
|
+
self.decrement()
|
|
68
|
+
|
|
69
|
+
def __repr__(self) -> str:
|
|
70
|
+
return f"SaturatingCounter(value={self._value}, max={self._max})"
|
bpred/gshare.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Gshare branch predictor.
|
|
2
|
+
|
|
3
|
+
Reference: S. McFarling, "Combining Branch Predictors," WRL Technical Note
|
|
4
|
+
TN-36, Digital Equipment Corporation Western Research Laboratory, June 1993.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from bpred.counter import SaturatingCounter
|
|
10
|
+
|
|
11
|
+
_COUNTER_BITS = 2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GsharePredictor:
|
|
15
|
+
"""Global-history XOR-indexed branch predictor (gshare).
|
|
16
|
+
|
|
17
|
+
A global history register (GHR) of *history_bits* bits is XOR'd with the
|
|
18
|
+
lower *history_bits* bits of the PC to index into a prediction table of
|
|
19
|
+
2-bit saturating counters.
|
|
20
|
+
|
|
21
|
+
After each branch the actual outcome is shifted into the GHR (MSB first,
|
|
22
|
+
keeping only the most recent *history_bits* outcomes).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
_table: list[SaturatingCounter]
|
|
26
|
+
_table_size: int
|
|
27
|
+
_history_bits: int
|
|
28
|
+
_history_mask: int
|
|
29
|
+
_ghr: int # global history register
|
|
30
|
+
|
|
31
|
+
def __init__(self, *, history_bits: int, table_size: int) -> None:
|
|
32
|
+
if history_bits < 1:
|
|
33
|
+
raise ValueError(f"history_bits must be >= 1, got {history_bits}")
|
|
34
|
+
if table_size < 1:
|
|
35
|
+
raise ValueError(f"table_size must be >= 1, got {table_size}")
|
|
36
|
+
self._history_bits = history_bits
|
|
37
|
+
self._history_mask = (1 << history_bits) - 1
|
|
38
|
+
self._table_size = table_size
|
|
39
|
+
self._ghr = 0
|
|
40
|
+
initial = 1 << (_COUNTER_BITS - 1) # weakly taken
|
|
41
|
+
self._table = [
|
|
42
|
+
SaturatingCounter(bits=_COUNTER_BITS, initial=initial)
|
|
43
|
+
for _ in range(table_size)
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
# Properties
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def table_size(self) -> int:
|
|
52
|
+
"""Number of entries in the prediction table."""
|
|
53
|
+
return self._table_size
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def history_bits(self) -> int:
|
|
57
|
+
"""Width of the global history register."""
|
|
58
|
+
return self._history_bits
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def ghr(self) -> int:
|
|
62
|
+
"""Current value of the global history register."""
|
|
63
|
+
return self._ghr
|
|
64
|
+
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
# Internal helpers
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def _index(self, *, pc: int) -> int:
|
|
70
|
+
"""Compute table index: (pc XOR ghr) mod table_size."""
|
|
71
|
+
return (pc ^ self._ghr) % self._table_size
|
|
72
|
+
|
|
73
|
+
def _shift_history(self, *, taken: bool) -> None:
|
|
74
|
+
"""Shift *taken* into the MSB of the GHR, keeping history_bits."""
|
|
75
|
+
self._ghr = ((self._ghr << 1) | int(taken)) & self._history_mask
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
# Public interface
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def predict(self, *, pc: int) -> bool:
|
|
82
|
+
"""Return the taken prediction for the branch at *pc*."""
|
|
83
|
+
return self._table[self._index(pc=pc)].predict()
|
|
84
|
+
|
|
85
|
+
def update(self, *, pc: int, taken: bool) -> None:
|
|
86
|
+
"""Update the counter and GHR for the branch at *pc*."""
|
|
87
|
+
self._table[self._index(pc=pc)].update(taken=taken)
|
|
88
|
+
self._shift_history(taken=taken)
|
|
89
|
+
|
|
90
|
+
def __repr__(self) -> str:
|
|
91
|
+
return (
|
|
92
|
+
f"GsharePredictor(history_bits={self._history_bits}, "
|
|
93
|
+
f"table_size={self._table_size})"
|
|
94
|
+
)
|
bpred/perceptron.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Perceptron branch predictor.
|
|
2
|
+
|
|
3
|
+
Reference: D. A. Jimenez and C. Lin, "Dynamic Branch Prediction with
|
|
4
|
+
Perceptrons," in Proceedings of the 7th International Symposium on High-
|
|
5
|
+
Performance Computer Architecture (HPCA), pp. 197-206, January 2001.
|
|
6
|
+
|
|
7
|
+
Each perceptron is a vector of integer weights. The dot product of those
|
|
8
|
+
weights with the history vector (bias + H history bits) gives the confidence
|
|
9
|
+
and direction of the prediction. Weight updates use the perceptron learning
|
|
10
|
+
rule, gated by a threshold theta to prevent over-training on easy branches.
|
|
11
|
+
|
|
12
|
+
Training threshold (Jimenez & Lin, Section 3.3):
|
|
13
|
+
theta = floor(1.93 * H + 14)
|
|
14
|
+
where H is the history length. This integer form is the standard used in
|
|
15
|
+
hardware and simulation; it derives from the optimal threshold analysis in
|
|
16
|
+
the paper.
|
|
17
|
+
|
|
18
|
+
History encoding:
|
|
19
|
+
taken -> +1
|
|
20
|
+
not-taken -> -1
|
|
21
|
+
|
|
22
|
+
The bias input x_0 is always +1.
|
|
23
|
+
|
|
24
|
+
Weights are stored as plain Python ints and are left unclamped (unbounded).
|
|
25
|
+
Hardware implementations clamp to a signed fixed-point range for area
|
|
26
|
+
efficiency, but clamping is orthogonal to correctness and is omitted here
|
|
27
|
+
to keep the simulator transparent and exact.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import math
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PerceptronPredictor:
|
|
36
|
+
"""Table of perceptrons for branch prediction (Jimenez and Lin 2001).
|
|
37
|
+
|
|
38
|
+
Each perceptron is a list of (H + 1) integer weights: w[0] is the bias
|
|
39
|
+
weight (multiplied by x_0 = +1 always) and w[1..H] are multiplied by the
|
|
40
|
+
corresponding bits of the global history register (GHR).
|
|
41
|
+
|
|
42
|
+
The table is indexed by pc % table_size. The GHR is an integer where
|
|
43
|
+
bit position i (0 = most recent) stores the outcome of the i-th most
|
|
44
|
+
recent branch: 1 for taken, 0 for not-taken. When computing the dot
|
|
45
|
+
product the bits are converted to {+1, -1} on the fly.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
_table: list[list[int]]
|
|
49
|
+
_table_size: int
|
|
50
|
+
_history_length: int
|
|
51
|
+
_theta: int
|
|
52
|
+
_ghr: int # packed history; bit 0 = most recent outcome
|
|
53
|
+
_history_mask: int
|
|
54
|
+
|
|
55
|
+
def __init__(self, *, history_length: int, table_size: int) -> None:
|
|
56
|
+
"""Create a PerceptronPredictor.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
history_length: Number of history bits H. Must be >= 1.
|
|
60
|
+
table_size: Number of perceptrons in the table. Must be >= 1.
|
|
61
|
+
"""
|
|
62
|
+
if history_length < 1:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"history_length must be >= 1, got {history_length}"
|
|
65
|
+
)
|
|
66
|
+
if table_size < 1:
|
|
67
|
+
raise ValueError(f"table_size must be >= 1, got {table_size}")
|
|
68
|
+
|
|
69
|
+
self._history_length = history_length
|
|
70
|
+
self._table_size = table_size
|
|
71
|
+
# Standard threshold from Jimenez & Lin Section 3.3:
|
|
72
|
+
# theta = floor(1.93 * H + 14)
|
|
73
|
+
self._theta = math.floor(1.93 * history_length + 14)
|
|
74
|
+
self._history_mask = (1 << history_length) - 1
|
|
75
|
+
self._ghr = 0
|
|
76
|
+
# All weights initialised to 0: bias and history weights start neutral.
|
|
77
|
+
self._table = [
|
|
78
|
+
[0] * (history_length + 1) for _ in range(table_size)
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
# Properties
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def table_size(self) -> int:
|
|
87
|
+
"""Number of entries (perceptrons) in the table."""
|
|
88
|
+
return self._table_size
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def history_length(self) -> int:
|
|
92
|
+
"""Number of global history bits H."""
|
|
93
|
+
return self._history_length
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def theta(self) -> int:
|
|
97
|
+
"""Training threshold: floor(1.93 * H + 14)."""
|
|
98
|
+
return self._theta
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def ghr(self) -> int:
|
|
102
|
+
"""Current value of the global history register (raw packed int)."""
|
|
103
|
+
return self._ghr
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
# Internal helpers
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def _index(self, *, pc: int) -> int:
|
|
110
|
+
"""Table index: pc mod table_size."""
|
|
111
|
+
return pc % self._table_size
|
|
112
|
+
|
|
113
|
+
def _history_vector(self) -> list[int]:
|
|
114
|
+
"""Return the current history as a list of +1/-1 values.
|
|
115
|
+
|
|
116
|
+
x[0] is the bias (always +1).
|
|
117
|
+
x[i] for i in 1..H corresponds to the (i-1)-th most recent branch,
|
|
118
|
+
where bit (i-1) of _ghr is 1 (taken) or 0 (not-taken).
|
|
119
|
+
"""
|
|
120
|
+
x: list[int] = [1] # x[0] = bias = +1
|
|
121
|
+
for i in range(self._history_length):
|
|
122
|
+
bit = (self._ghr >> i) & 1
|
|
123
|
+
x.append(1 if bit else -1)
|
|
124
|
+
return x
|
|
125
|
+
|
|
126
|
+
def _dot(self, *, weights: list[int]) -> int:
|
|
127
|
+
"""Compute y = sum(w[i] * x[i]) using current history."""
|
|
128
|
+
x = self._history_vector()
|
|
129
|
+
total = 0
|
|
130
|
+
for w, xi in zip(weights, x):
|
|
131
|
+
total += w * xi
|
|
132
|
+
return total
|
|
133
|
+
|
|
134
|
+
def _shift_history(self, *, taken: bool) -> None:
|
|
135
|
+
"""Shift the new outcome into the GHR as the most recent bit."""
|
|
136
|
+
self._ghr = ((self._ghr << 1) | int(taken)) & self._history_mask
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
# Public interface
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def predict(self, *, pc: int) -> bool:
|
|
143
|
+
"""Return the taken prediction for the branch at *pc*.
|
|
144
|
+
|
|
145
|
+
Computes y = w dot x; predicts taken if y >= 0.
|
|
146
|
+
"""
|
|
147
|
+
idx = self._index(pc=pc)
|
|
148
|
+
y = self._dot(weights=self._table[idx])
|
|
149
|
+
return y >= 0
|
|
150
|
+
|
|
151
|
+
def update(self, *, pc: int, taken: bool) -> None:
|
|
152
|
+
"""Update the perceptron for *pc* with the actual outcome.
|
|
153
|
+
|
|
154
|
+
Training rule (Jimenez & Lin):
|
|
155
|
+
t = +1 if taken, -1 if not-taken.
|
|
156
|
+
Compute y = w dot x.
|
|
157
|
+
If prediction was wrong OR |y| <= theta:
|
|
158
|
+
for each i: w[i] = w[i] + t * x[i]
|
|
159
|
+
Shift taken into the GHR after training.
|
|
160
|
+
|
|
161
|
+
The GHR is updated AFTER training so that the same history vector
|
|
162
|
+
used during prediction is also used during training.
|
|
163
|
+
"""
|
|
164
|
+
idx = self._index(pc=pc)
|
|
165
|
+
weights = self._table[idx]
|
|
166
|
+
x = self._history_vector()
|
|
167
|
+
y = self._dot(weights=weights)
|
|
168
|
+
|
|
169
|
+
predicted_taken = y >= 0
|
|
170
|
+
t = 1 if taken else -1
|
|
171
|
+
|
|
172
|
+
# Train when prediction was wrong or confidence is below theta.
|
|
173
|
+
if predicted_taken != taken or abs(y) <= self._theta:
|
|
174
|
+
for i in range(len(weights)):
|
|
175
|
+
weights[i] += t * x[i]
|
|
176
|
+
|
|
177
|
+
self._shift_history(taken=taken)
|
|
178
|
+
|
|
179
|
+
def __repr__(self) -> str:
|
|
180
|
+
return (
|
|
181
|
+
f"PerceptronPredictor(history_length={self._history_length}, "
|
|
182
|
+
f"table_size={self._table_size})"
|
|
183
|
+
)
|
bpred/py.typed
ADDED
|
File without changes
|
bpred/tournament.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Tournament (hybrid) branch predictor.
|
|
2
|
+
|
|
3
|
+
Reference: S. McFarling, "Combining Branch Predictors," WRL Technical Note
|
|
4
|
+
TN-36, Digital Equipment Corporation Western Research Laboratory, June 1993.
|
|
5
|
+
|
|
6
|
+
The design follows the Alpha 21264 tournament predictor: a meta-selector table
|
|
7
|
+
of saturating counters chooses between a local and a global sub-predictor.
|
|
8
|
+
The chooser is updated only when the two sub-predictors disagree, biasing it
|
|
9
|
+
toward whichever was correct.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Protocol
|
|
15
|
+
|
|
16
|
+
from bpred.counter import SaturatingCounter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BranchPredictor(Protocol):
|
|
20
|
+
"""Structural protocol satisfied by BimodalPredictor and GsharePredictor."""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def table_size(self) -> int: ...
|
|
24
|
+
|
|
25
|
+
def predict(self, *, pc: int) -> bool: ...
|
|
26
|
+
|
|
27
|
+
def update(self, *, pc: int, taken: bool) -> None: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TournamentPredictor:
|
|
31
|
+
"""Meta-predictor that combines a local and a global sub-predictor.
|
|
32
|
+
|
|
33
|
+
The chooser table contains *meta_bits*-bit saturating counters. A low
|
|
34
|
+
chooser value (< threshold) means "trust the local predictor"; a high
|
|
35
|
+
value (>= threshold) means "trust the global predictor."
|
|
36
|
+
|
|
37
|
+
Chooser update rule (classic Alpha 21264):
|
|
38
|
+
- Both correct or both wrong: no update.
|
|
39
|
+
- Only local correct: decrement chooser toward 0 (favor local).
|
|
40
|
+
- Only global correct: increment chooser toward max (favor global).
|
|
41
|
+
|
|
42
|
+
Both sub-predictors are always updated with the actual outcome,
|
|
43
|
+
regardless of which one the meta-selector chose.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
_local: BranchPredictor
|
|
47
|
+
_global: BranchPredictor
|
|
48
|
+
_chooser: list[SaturatingCounter]
|
|
49
|
+
_chooser_size: int
|
|
50
|
+
_meta_bits: int
|
|
51
|
+
_threshold: int
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
local: BranchPredictor,
|
|
57
|
+
global_: BranchPredictor,
|
|
58
|
+
meta_bits: int,
|
|
59
|
+
) -> None:
|
|
60
|
+
if meta_bits < 1:
|
|
61
|
+
raise ValueError(f"meta_bits must be >= 1, got {meta_bits}")
|
|
62
|
+
self._local = local
|
|
63
|
+
self._global = global_
|
|
64
|
+
self._meta_bits = meta_bits
|
|
65
|
+
self._threshold = 1 << (meta_bits - 1)
|
|
66
|
+
# Chooser table sized to the larger sub-predictor table.
|
|
67
|
+
self._chooser_size = max(local.table_size, global_.table_size)
|
|
68
|
+
# Start neutral: weakly global (threshold value).
|
|
69
|
+
initial = self._threshold
|
|
70
|
+
self._chooser = [
|
|
71
|
+
SaturatingCounter(bits=meta_bits, initial=initial)
|
|
72
|
+
for _ in range(self._chooser_size)
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
# Properties
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def table_size(self) -> int:
|
|
81
|
+
"""Size of the meta-selector chooser table."""
|
|
82
|
+
return self._chooser_size
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def meta_bits(self) -> int:
|
|
86
|
+
"""Bit-width of each chooser counter."""
|
|
87
|
+
return self._meta_bits
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
# Internal helpers
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def _chooser_index(self, *, pc: int) -> int:
|
|
94
|
+
return pc % self._chooser_size
|
|
95
|
+
|
|
96
|
+
def _use_global(self, *, pc: int) -> bool:
|
|
97
|
+
"""Return True when the chooser favors the global sub-predictor."""
|
|
98
|
+
idx = self._chooser_index(pc=pc)
|
|
99
|
+
return self._chooser[idx].value >= self._threshold
|
|
100
|
+
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
# Public interface
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def predict(self, *, pc: int) -> bool:
|
|
106
|
+
"""Return the prediction chosen by the meta-selector."""
|
|
107
|
+
if self._use_global(pc=pc):
|
|
108
|
+
return self._global.predict(pc=pc)
|
|
109
|
+
return self._local.predict(pc=pc)
|
|
110
|
+
|
|
111
|
+
def update(self, *, pc: int, taken: bool) -> None:
|
|
112
|
+
"""Update both sub-predictors and the chooser.
|
|
113
|
+
|
|
114
|
+
The chooser is updated only when the two sub-predictors disagree.
|
|
115
|
+
"""
|
|
116
|
+
local_pred = self._local.predict(pc=pc)
|
|
117
|
+
global_pred = self._global.predict(pc=pc)
|
|
118
|
+
|
|
119
|
+
# Update chooser only on disagreement.
|
|
120
|
+
if local_pred != global_pred:
|
|
121
|
+
local_correct = local_pred == taken
|
|
122
|
+
global_correct = global_pred == taken
|
|
123
|
+
idx = self._chooser_index(pc=pc)
|
|
124
|
+
if local_correct and not global_correct:
|
|
125
|
+
self._chooser[idx].decrement()
|
|
126
|
+
elif global_correct and not local_correct:
|
|
127
|
+
self._chooser[idx].increment()
|
|
128
|
+
|
|
129
|
+
# Always update both sub-predictors.
|
|
130
|
+
self._local.update(pc=pc, taken=taken)
|
|
131
|
+
self._global.update(pc=pc, taken=taken)
|
|
132
|
+
|
|
133
|
+
def __repr__(self) -> str:
|
|
134
|
+
return (
|
|
135
|
+
f"TournamentPredictor(local={self._local!r}, "
|
|
136
|
+
f"global_={self._global!r}, meta_bits={self._meta_bits})"
|
|
137
|
+
)
|
bpred/trace.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Trace-driven simulation utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BranchPredictor(Protocol):
|
|
10
|
+
"""Structural protocol for any predictor accepted by run_trace."""
|
|
11
|
+
|
|
12
|
+
def predict(self, *, pc: int) -> bool: ...
|
|
13
|
+
|
|
14
|
+
def update(self, *, pc: int, taken: bool) -> None: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class TraceResult:
|
|
19
|
+
"""Result of running a branch trace through a predictor.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
predictions: Ordered list of predictions made before each update.
|
|
23
|
+
correct: Whether each prediction matched the actual outcome.
|
|
24
|
+
total: Total number of branches in the trace.
|
|
25
|
+
hits: Number of correctly predicted branches.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
predictions: list[bool]
|
|
29
|
+
correct: list[bool]
|
|
30
|
+
total: int
|
|
31
|
+
hits: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_trace(
|
|
35
|
+
predictor: BranchPredictor,
|
|
36
|
+
*,
|
|
37
|
+
trace: list[tuple[int, bool]],
|
|
38
|
+
) -> TraceResult:
|
|
39
|
+
"""Feed (pc, taken) pairs through *predictor* and collect statistics.
|
|
40
|
+
|
|
41
|
+
For each entry the prediction is recorded *before* the predictor is
|
|
42
|
+
updated, matching real hardware behaviour.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
predictor: Any object with ``predict`` and ``update`` methods.
|
|
46
|
+
trace: Ordered list of (pc, taken) pairs.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
A :class:`TraceResult` with per-branch predictions and aggregate counts.
|
|
50
|
+
"""
|
|
51
|
+
predictions: list[bool] = []
|
|
52
|
+
correct: list[bool] = []
|
|
53
|
+
hits = 0
|
|
54
|
+
|
|
55
|
+
for pc, taken in trace:
|
|
56
|
+
pred = predictor.predict(pc=pc)
|
|
57
|
+
hit = pred == taken
|
|
58
|
+
predictions.append(pred)
|
|
59
|
+
correct.append(hit)
|
|
60
|
+
if hit:
|
|
61
|
+
hits += 1
|
|
62
|
+
predictor.update(pc=pc, taken=taken)
|
|
63
|
+
|
|
64
|
+
return TraceResult(
|
|
65
|
+
predictions=predictions,
|
|
66
|
+
correct=correct,
|
|
67
|
+
total=len(trace),
|
|
68
|
+
hits=hits,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def accuracy(*, trace_result: TraceResult) -> float:
|
|
73
|
+
"""Return prediction accuracy as a fraction in [0.0, 1.0].
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
trace_result: Result from :func:`run_trace`.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
hits / total, or 0.0 for an empty trace.
|
|
80
|
+
"""
|
|
81
|
+
if trace_result.total == 0:
|
|
82
|
+
return 0.0
|
|
83
|
+
return trace_result.hits / trace_result.total
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def mispredictions(*, trace_result: TraceResult) -> int:
|
|
87
|
+
"""Return the number of mispredicted branches.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
trace_result: Result from :func:`run_trace`.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
total - hits
|
|
94
|
+
"""
|
|
95
|
+
return trace_result.total - trace_result.hits
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bpred
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Pure-Python simulator of classical CPU branch predictors: bimodal, gshare, and tournament
|
|
5
|
+
Project-URL: Homepage, https://github.com/amaar-mc/bpred
|
|
6
|
+
Project-URL: Repository, https://github.com/amaar-mc/bpred
|
|
7
|
+
Project-URL: Issues, https://github.com/amaar-mc/bpred/issues
|
|
8
|
+
Author-email: Amaar Chughtai <amaardevx@gmail.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Amaar Chughtai
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: bimodal,branch-prediction,computer-architecture,cpu-simulator,education,gshare
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Education
|
|
34
|
+
Classifier: Intended Audience :: Science/Research
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Topic :: Education
|
|
42
|
+
Classifier: Topic :: Scientific/Engineering
|
|
43
|
+
Classifier: Typing :: Typed
|
|
44
|
+
Requires-Python: >=3.10
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
47
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
48
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
|
|
51
|
+
# bpred
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<img src="assets/logo.png" alt="bpred logo" width="160">
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
Pure-Python simulator of classical CPU branch predictors for computer architecture education.
|
|
58
|
+
|
|
59
|
+
Implements four predictors from first principles with zero runtime dependencies:
|
|
60
|
+
|
|
61
|
+
- **Bimodal** (Smith 1981) -- a table of n-bit saturating counters indexed by PC.
|
|
62
|
+
- **Gshare** (McFarling 1993) -- PC XOR global-history register indexes 2-bit counters.
|
|
63
|
+
- **Tournament** (McFarling 1993 / Alpha 21264) -- a meta-selector combining local and global sub-predictors.
|
|
64
|
+
- **Perceptron** (Jimenez and Lin 2001) -- a table of integer-weight perceptrons that can learn linearly-separable history patterns bimodal and gshare cannot capture.
|
|
65
|
+
|
|
66
|
+
Part of the same open-source computer architecture education series as [tomasulo](https://github.com/amaar-mc/tomasulo) (out-of-order execution) and scoreboarding.
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install bpred
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
PyPI publication is pending; install from source in the meantime:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/amaar-mc/bpred
|
|
78
|
+
cd bpred
|
|
79
|
+
pip install -e ".[dev]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Python API
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from bpred import BimodalPredictor, GsharePredictor, PerceptronPredictor, TournamentPredictor
|
|
86
|
+
from bpred import run_trace, accuracy, mispredictions
|
|
87
|
+
|
|
88
|
+
# Bimodal: 2-bit counters, 1024-entry table
|
|
89
|
+
pred = BimodalPredictor(counter_bits=2, table_size=1024)
|
|
90
|
+
|
|
91
|
+
# Gshare: 10-bit history, 1024-entry table
|
|
92
|
+
pred = GsharePredictor(history_bits=10, table_size=1024)
|
|
93
|
+
|
|
94
|
+
# Tournament
|
|
95
|
+
from bpred import BimodalPredictor, GsharePredictor
|
|
96
|
+
local = BimodalPredictor(counter_bits=2, table_size=1024)
|
|
97
|
+
global_ = GsharePredictor(history_bits=10, table_size=1024)
|
|
98
|
+
pred = TournamentPredictor(local=local, global_=global_, meta_bits=2)
|
|
99
|
+
|
|
100
|
+
# Perceptron: 12-bit history, 1024-entry table
|
|
101
|
+
pred = PerceptronPredictor(history_length=12, table_size=1024)
|
|
102
|
+
|
|
103
|
+
# Feed a trace
|
|
104
|
+
trace = [(0x1000, True), (0x1004, False), (0x1008, True)]
|
|
105
|
+
result = run_trace(pred, trace=trace)
|
|
106
|
+
print(accuracy(trace_result=result)) # e.g. 0.6667
|
|
107
|
+
print(mispredictions(trace_result=result)) # e.g. 1
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Why use the perceptron predictor?
|
|
111
|
+
|
|
112
|
+
Bimodal and gshare each use a single scalar counter per table entry, so they
|
|
113
|
+
can only learn the *average* bias of a branch. When the taken/not-taken
|
|
114
|
+
outcome correlates with a specific combination of recent history bits (a
|
|
115
|
+
linearly-separable pattern), those predictors plateau.
|
|
116
|
+
|
|
117
|
+
The perceptron predictor maintains a weight vector per entry. The dot product
|
|
118
|
+
of those weights with the history vector expresses arbitrary linear functions
|
|
119
|
+
over H history bits. This lets it learn, for example, "taken when the last
|
|
120
|
+
4 branches were all taken" or "taken on every other iteration" -- patterns
|
|
121
|
+
that require tracking distinct history bits simultaneously. The trade-off is
|
|
122
|
+
that the predictor needs more warm-up branches to converge and the weights
|
|
123
|
+
grow without bound (in simulation; hardware clamps them to a fixed-point
|
|
124
|
+
range).
|
|
125
|
+
|
|
126
|
+
## CLI
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
bpred bimodal --counter-bits 2 --table-size 1024 path/to/trace.trace
|
|
130
|
+
bpred gshare --history-bits 10 --table-size 1024 path/to/trace.trace
|
|
131
|
+
bpred tournament \
|
|
132
|
+
--local-predictor bimodal --local-counter-bits 2 --local-table-size 1024 \
|
|
133
|
+
--global-predictor gshare --global-history-bits 10 --global-table-size 1024 \
|
|
134
|
+
--meta-bits 2 \
|
|
135
|
+
path/to/trace.trace
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Trace file format -- one branch per line:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
# pc taken
|
|
142
|
+
0x1000 1
|
|
143
|
+
0x1004 0
|
|
144
|
+
0x1008 T
|
|
145
|
+
0x100c false
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Accuracy example
|
|
149
|
+
|
|
150
|
+
Running the bundled sample trace with a gshare predictor:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
$ bpred gshare --history-bits 4 --table-size 16 examples/sample.trace
|
|
154
|
+
Predictor : GsharePredictor(history_bits=4, table_size=16)
|
|
155
|
+
Branches : 20
|
|
156
|
+
Hits : 18
|
|
157
|
+
Misses : 2
|
|
158
|
+
Accuracy : 90.0000%
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Development
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pip install -e ".[dev]"
|
|
165
|
+
pytest -q
|
|
166
|
+
ruff check .
|
|
167
|
+
mypy src
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
bpred/__init__.py,sha256=ElpUYpNLQehyeVkLYEUGIu3raUigNZZiUlP7PRAMmyI,1222
|
|
2
|
+
bpred/bimodal.py,sha256=rNN8Kpr_bcyWXFWkRaIj7BBexhy1U1V_vmbpT9DZNqo,2538
|
|
3
|
+
bpred/cli.py,sha256=i-QdTse5wAv-ocgMKyknxPG6XFJpYAOiUtW-zYFEDbQ,5475
|
|
4
|
+
bpred/counter.py,sha256=7J0VdMzxJ6nFsqJyOCpjAdu1r0GwHPa-2quy9G2KShM,2255
|
|
5
|
+
bpred/gshare.py,sha256=hxVuWkZ8A7xe1SqV3AtBeHf0VjytvBVJuZtZjo-dt68,3306
|
|
6
|
+
bpred/perceptron.py,sha256=MWrajdEnAEpVxRvYhgIxZHAKOWhxZd1mXqZIgs-isY8,6609
|
|
7
|
+
bpred/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
bpred/tournament.py,sha256=AoixJLPSoBzSaIVQC2ugWBqQO2w4sZlGP9FAgYayXVc,4815
|
|
9
|
+
bpred/trace.py,sha256=f6JtONZZ48m6uKfQTPKig3WCZZcw8gwsBlCWn9oBQmc,2448
|
|
10
|
+
bpred-0.2.0.dist-info/METADATA,sha256=1SUBzl07nULlpobSovRYgqOdlEJRqsNiOTdozJ6a21Y,6274
|
|
11
|
+
bpred-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
12
|
+
bpred-0.2.0.dist-info/entry_points.txt,sha256=ZBCc52QaGDNOg73I2UjdBCFiPvPlssxZ9gNAqqfeymc,41
|
|
13
|
+
bpred-0.2.0.dist-info/licenses/LICENSE,sha256=NORTVJb4x206RXQkpZt_vpj8I7aZL6Vn26BvTOq6J40,1071
|
|
14
|
+
bpred-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amaar Chughtai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|