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 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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bpred = bpred.cli:main
@@ -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.