Part VAdvanced ILP and Out-of-Order Execution

Project --- A Branch Predictor Evaluator

August 3, 2026·26 min read·advanced

The case studies of Chapters Chapter 63 and Chapter 64 examined branch predictors in the abstract. Both Intel Golden Cove and AMD Zen 4 are described in public sources as TAGE-class predictors with statistical…

The case studies of Chapters Chapter 63 and Chapter 64 examined branch predictors in the abstract. Both Intel Golden Cove and AMD Zen 4 are described in public sources as TAGE-class predictors with statistical correctors and indirect target predictors, but the exact configurations are not published. Reading and trusting the disclosed numbers is one side of microarchitecture research. Building a predictor from scratch and measuring it on standard traces is the other side.

This project chapter walks the reader through building three branch predictors in C++ and evaluating them on standard benchmark traces. The first predictor is McFarling’s gshare, the classic correlated baseline. The second is a perceptron predictor in Jimenez and Lin’s original formulation. The third is TAGE-SC-L, the production state-of-the-art represented (in simplified form) in modern P-cores. The reader will report the MPKI of each predictor on a curated set of Championship Branch Prediction traces and analyze the storage-versus-accuracy trade-off.

The project sits in the same shape as the cache simulator of Chapter 47. First, the Setup and Installation section walks through getting a C++ toolchain on macOS, Linux, and Windows. Then the project skeleton and the build system. Then the predictor interface, and each predictor in turn, in increasing complexity order. Then the trace reader. Finally, the evaluation harness and the MPKI report. The chapter ends with worked examples and exercises.

01.Setup and Installation

The project requires four tools: a C++17-or-later compiler (clang or gcc), the CMake build system, Python 3 (for the trace post-processing and plotting scripts), and xz for decompressing the CBP trace files. The C++ standard library is the only third-party dependency, by design. The evaluator does not pull in any external libraries to keep the build reproducible.

macOS (Homebrew)

macOS setup via Homebrew

Plain Text
# Modern compiler and build tooling
brew install llvm cmake python xz
# Optional: matplotlib for the plotting scripts
python3 -m pip install matplotlib numpy

Verify each tool after installation:

Verify macOS installations

Plain Text
clang++ --version # Apple clang 16+ or Homebrew clang 18+
cmake --version # CMake 3.28 or later
python3 --version # Python 3.12 or later
xz --version

The Apple-supplied clang++ from Xcode Command Line Tools also works for the project. The Homebrew LLVM is the canonical choice when newer C++20 features (such as std::span on older toolchains) are needed.

Linux (Arch as canonical)

Arch Linux setup

Plain Text
# Compiler, build system, Python, decompression
sudo pacman -S clang gcc cmake python xz ninja
# Optional: matplotlib for the plotting scripts. The Arch
# python-matplotlib package may lag PyPI by a release.
sudo pacman -S python-matplotlib python-numpy
# AUR: an alternative if a newer matplotlib is needed
# yay -S python-matplotlib-bin

For Debian or Ubuntu, replace pacman with apt:

Debian/Ubuntu alternative

Plain Text
sudo apt update
sudo apt install build-essential cmake clang \
python3 python3-pip xz-utils ninja-build \
python3-matplotlib python3-numpy

For Fedora:

Fedora alternative

Plain Text
sudo dnf install gcc-c++ clang cmake python3 xz \
ninja-build python3-matplotlib python3-numpy

Windows (ArchWSL)

The recommended path on Windows is ArchWSL, the Arch Linux distribution for WSL2. Once ArchWSL is installed, the setup is identical to the Arch Linux instructions above. The project source tree should live inside the WSL2 filesystem (~/projects/bpeval) rather than on a Windows-mounted drive, since the trace files are several hundred megabytes each and the I/O path is markedly faster on the native WSL2 file system.

Windows setup via ArchWSL

Plain Text
# Inside the ArchWSL terminal:
sudo pacman -S clang gcc cmake python xz ninja \
python-matplotlib python-numpy

02.Project Skeleton

The project tree is small. The predictors share a common abstract base class. The evaluator drives the predictors against a trace.

Project layout

Plain Text
bpeval/ CMakeLists.txt include/ Predictor.hpp # abstract base class Trace.hpp # trace reader and record type Gshare.hpp Perceptron.hpp TageSCL.hpp src/ main.cpp # evaluator driver Trace.cpp # trace reader implementation Gshare.cpp Perceptron.cpp TageSCL.cpp scripts/ plot-mpki.py # plotting from results CSV cbp-download.sh # CBP trace fetcher results/ (empty, populated by runs) traces/ (empty, populated by cbp-download.sh)

The build file declares one executable, bpeval, plus the shared predictor library.

CMakeLists.txt

Plain Text
cmake_minimum_required(VERSION 3.20)
project(bpeval CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(bp_lib
src/Trace.cpp
src/Gshare.cpp
src/Perceptron.cpp
src/TageSCL.cpp
)
target_include_directories(bp_lib PUBLIC include)
add_executable(bpeval src/main.cpp)
target_link_libraries(bpeval PRIVATE bp_lib)
if(CMAKE_BUILD_TYPE STREQUAL "Release")
target_compile_options(bp_lib PRIVATE -O3 -march=native)
target_compile_options(bpeval PRIVATE -O3 -march=native)
endif()

Build:

Build the evaluator

Plain Text
cd bpeval
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/bpeval --help

03.Predictor Interface

The three predictors share an abstract base class so the evaluator can drive any of them through a common interface.

Predictor.hpp

C
#pragma once #include <cstdint> #include <string> struct BranchRecord { uint64_t pc; // branch instruction address bool taken; // ground-truth outcome uint64_t target; // taken target (0 if not taken) uint8_t type; // 0=cond, 1=indirect, 2=call, 3=ret }; class Predictor { public: virtual ~Predictor() = default; // Returns the predicted direction for the given branch. virtual bool predict(uint64_t pc) = 0; // Updates internal state with the resolved outcome. virtual void update(uint64_t pc, bool taken) = 0; // Storage budget in bits, for reporting. virtual uint64_t storage_bits() const = 0; virtual std::string name() const = 0; };

The interface treats each branch as a separate predict-then- update transaction. The evaluator calls predict, compares the prediction to the ground truth, increments the misprediction counter if they disagree, and calls update with the ground truth.

04.The gshare Predictor

The gshare predictor is the simplest of the three. It maintains a single table of 2-bit saturating counters and a global history register (GHR). The table is indexed by the XOR of the branch PC and the GHR. The prediction is the high bit of the counter at that index.

The predictor’s storage cost is exactly 22N2 \cdot 2^N bits for a table of 2N2^N entries. With N=14N = 14, the table holds 16384 counters costing 32768 bits, which is exactly 4 KiB, plus the 14-bit GHR. The total is just over 4 KiB. CBP submissions often use NN between 13 and 15.

Gshare.hpp

C
#pragma once #include "Predictor.hpp" #include <vector> class Gshare : public Predictor { public: // history_bits: number of GHR bits and table index bits. explicit Gshare(int history_bits); bool predict(uint64_t pc) override; void update(uint64_t pc, bool taken) override; uint64_t storage_bits() const override; std::string name() const override { return "gshare"; } private: int history_bits_; uint64_t ghr_; uint64_t mask_; std::vector<uint8_t> table_; // 2-bit counters, packed as bytes uint64_t index(uint64_t pc) const { return (pc ^ ghr_) & mask_; } };

Gshare.cpp

C
#include "Gshare.hpp" Gshare::Gshare(int history_bits) : history_bits_(history_bits), ghr_(0), mask_((1ULL << history_bits) - 1), table_(1ULL << history_bits, 2 /* weakly taken */) {} bool Gshare::predict(uint64_t pc) { return table_[index(pc)] >= 2; } void Gshare::update(uint64_t pc, bool taken) { uint64_t idx = index(pc); if (taken && table_[idx] < 3) { table_[idx]++; } else if (!taken && table_[idx] > 0) { table_[idx]--; } // Shift outcome into GHR. ghr_ = ((ghr_ << 1) | (taken ? 1ULL : 0ULL)) & mask_; } uint64_t Gshare::storage_bits() const { // 2 bits per counter plus the GHR. return 2ULL * (1ULL << history_bits_) + history_bits_; }

The implementation is intentionally compact. Each counter is stored as a single byte (8 bits) for ease of access, even though only 2 bits per counter are architecturally used. The storage_bits method reports the 2-bit-per-counter budget that a hardware implementation would actually use.

05.The Perceptron Predictor

The perceptron predictor takes a different mathematical approach. Each branch PC indexes a row of weights w0,w1,,wHw_0, w_1, \ldots, w_H, where HH is the history length. The prediction is the sign of: y=w0+i=1Hwihi,y = w_0 + \sum_{i=1}^{H} w_i \cdot h_i, where hi{1,+1}h_i \in \{-1, +1\} is the ii-th outcome in the global history (the 1-1 encoding is for not-taken and the +1+1 encoding is for taken). If y0y \geq 0, predict taken, otherwise predict not-taken. The bias term w0w_0 captures the branch’s overall bias.

Training, on a misprediction or when y|y| is below a threshold, updates each weight by the rule: wiwi+thi,w_i \gets w_i + t \cdot h_i, where t{1,+1}t \in \{-1, +1\} is the actual outcome. Weights are clamped to a saturating range, typically [128,+127][-128, +127] for 8-bit signed weights.

Perceptron.hpp

C
#pragma once #include "Predictor.hpp" #include <vector> #include <cstdint> class Perceptron : public Predictor { public: Perceptron(int num_rows, int history_length, int theta); bool predict(uint64_t pc) override; void update(uint64_t pc, bool taken) override; uint64_t storage_bits() const override; std::string name() const override { return "perceptron"; } private: int num_rows_; int history_length_; int theta_; // training threshold uint64_t ghr_; // global history register // weights_[row][0] = bias, [row][1..H] = history weights std::vector<std::vector<int8_t>> weights_; // y value from the most recent predict() call, used by update() int last_y_; int row_index(uint64_t pc) const { return static_cast<int>(pc % num_rows_); } };

Perceptron.cpp

C
#include "Perceptron.hpp" #include <algorithm> Perceptron::Perceptron(int num_rows, int history_length, int theta) : num_rows_(num_rows), history_length_(history_length), theta_(theta), ghr_(0), weights_(num_rows, std::vector<int8_t>(history_length + 1, 0)), last_y_(0) {} bool Perceptron::predict(uint64_t pc) { auto& row = weights_[row_index(pc)]; int y = row[0]; // bias for (int i = 1; i <= history_length_; i++) { int h = (ghr_ >> (i - 1)) & 1; int sign = (h == 1) ? +1 : -1; y += row[i] * sign; } last_y_ = y; return y >= 0; } void Perceptron::update(uint64_t pc, bool taken) { auto& row = weights_[row_index(pc)]; bool predicted = last_y_ >= 0; int abs_y = (last_y_ >= 0) ? last_y_ : -last_y_; // Train on misprediction or when confidence below threshold. if (predicted != taken || abs_y < theta_) { int t = taken ? +1 : -1; int b = row[0] + t; row[0] = static_cast<int8_t>(std::clamp(b, -128, 127)); for (int i = 1; i <= history_length_; i++) { int h = (ghr_ >> (i - 1)) & 1; int sign = (h == 1) ? +1 : -1; int w = row[i] + t * sign; row[i] = static_cast<int8_t>(std::clamp(w, -128, 127)); } } ghr_ = (ghr_ << 1) | (taken ? 1ULL : 0ULL); } uint64_t Perceptron::storage_bits() const { // 8 bits per weight, (H + 1) weights per row, num_rows rows. return 8ULL * num_rows_ * (history_length_ + 1) + history_length_; }

A typical configuration uses 1024 rows, 36-bit history, and θ=14\theta = 14 for the training threshold. The storage cost is 8102437=303,1048 \cdot 1024 \cdot 37 = 303{,}104 bits, or roughly 37 KiB. The gshare table at the same storage budget would hold roughly 150K counters (about N=17N = 17).

06.The TAGE-SC-L Predictor

TAGE is the production state of the art. The Statistical Corrector and Loop extensions add accuracy on the hardest benchmarks. The implementation here is a simplified TAGE-SC-L with five tagged tables, geometric history lengths, and a small SC component.

The TAGE predictor maintains a base predictor (a bimodal table) plus NN tagged tables of increasing history length. On a prediction lookup, all NN tables are consulted in parallel. Each tagged table either hits (its tag matches) or misses. The predictor uses the highest-history-length hit as the prediction. On a misprediction, the predictor allocates a new entry in a higher-history table.

The geometric history lengths are 5, 13, 33, 70, and 140 for a typical 5-table configuration. The 5-table TAGE captures correlations across both short-history patterns and long-history patterns. Empirically, the longer-history tables are sparse but contribute most of the accuracy gain over gshare.

TageSCL.hpp

C
#pragma once #include "Predictor.hpp" #include <array> #include <vector> #include <cstdint> struct TageEntry { int8_t ctr; // 3-bit signed counter, range [-4, +3] uint8_t tag; // 8-bit tag uint8_t u; // 2-bit usefulness counter }; class TageSCL : public Predictor { public: TageSCL(); bool predict(uint64_t pc) override; void update(uint64_t pc, bool taken) override; uint64_t storage_bits() const override; std::string name() const override { return "tage-sc-l"; } private: static constexpr int N_TABLES = 5; static constexpr int HIST_LENGTHS[N_TABLES] = {5, 13, 33, 70, 140}; static constexpr int TABLE_BITS = 12; // 4K entries each // Base bimodal predictor (12 bits index, 2-bit counters) std::vector<uint8_t> base_; // Five tagged tables std::array<std::vector<TageEntry>, N_TABLES> tables_; // Long global history register (up to 140 bits) std::array<uint64_t, 3> ghr_; // 192-bit GHR in 3 words // SC component: simple linear classifier static constexpr int SC_ENTRIES = 1024; std::vector<int8_t> sc_weights_; int provider_; // table that supplied the prediction int alt_provider_; // backup table bool provider_pred_; bool sc_pred_; bool used_sc_; uint64_t base_index(uint64_t pc) const; uint64_t tag_for(uint64_t pc, int table) const; uint64_t table_index(uint64_t pc, int table) const; bool ghr_bit(int i) const; void ghr_shift(bool taken); };

The implementation file is substantially longer. The key methods are the index and tag hash functions and the allocation policy on misprediction. The hash is a folded XOR of the relevant GHR bits with the PC, masked to the table-index width.

TageSCL.cpp index and tag hash

C
uint64_t TageSCL::table_index(uint64_t pc, int table) const { int hl = HIST_LENGTHS[table]; uint64_t folded = 0; int chunk = TABLE_BITS; int bits_taken = 0; while (bits_taken < hl) { int n = std::min(chunk, hl - bits_taken); uint64_t chunk_bits = 0; for (int i = 0; i < n; i++) { chunk_bits |= (ghr_bit(bits_taken + i) ? 1ULL : 0ULL) << i; } folded ^= chunk_bits; bits_taken += n; } return (pc ^ folded) & ((1ULL << TABLE_BITS) - 1); } uint64_t TageSCL::tag_for(uint64_t pc, int table) const { // 8-bit tag, also XOR-folded from history. int hl = HIST_LENGTHS[table]; uint64_t folded = 0; int chunk = 8; int bits_taken = 0; while (bits_taken < hl) { int n = std::min(chunk, hl - bits_taken); uint64_t chunk_bits = 0; for (int i = 0; i < n; i++) { chunk_bits |= (ghr_bit(bits_taken + i) ? 1ULL : 0ULL) << i; } folded ^= chunk_bits; bits_taken += n; } return (pc >> 4) ^ folded; }

The prediction proceeds as follows. The base bimodal predictor produces a default prediction. The five tagged tables are consulted in order of decreasing history length, longest first. The highest-history table that hits supplies the prediction (provider_). If no tagged table hits, the base predictor’s output is used.

TageSCL.cpp predict

C
bool TageSCL::predict(uint64_t pc) { bool base_pred = base_[base_index(pc)] >= 2; provider_ = -1; alt_provider_ = -1; bool tagged_pred = base_pred; // Walk longest-history-first; first hit wins. for (int t = N_TABLES - 1; t >= 0; t--) { uint64_t idx = table_index(pc, t); uint64_t tag = tag_for(pc, t) & 0xFF; if (tables_[t][idx].tag == tag) { if (provider_ == -1) { provider_ = t; tagged_pred = tables_[t][idx].ctr >= 0; } else if (alt_provider_ == -1) { alt_provider_ = t; break; } } } provider_pred_ = tagged_pred; // Statistical Corrector: a single linear classifier on a small // hash of pc and the low-order global history, reduced // modulo SC_ENTRIES. uint64_t sc_idx = (pc ^ ghr_[0]) % SC_ENTRIES; int8_t w = sc_weights_[sc_idx]; int sc_sum = (tagged_pred ? +1 : -1) * 4 + w; sc_pred_ = sc_sum >= 0; used_sc_ = std::abs(sc_sum) < 6; // confidence threshold return used_sc_ ? sc_pred_ : tagged_pred; }

The update logic is the most subtle part of TAGE. On every resolved branch the provider’s counter is moved one step toward the outcome, saturating at the ends of its range, and the usefulness counter is raised when the provider’s prediction beat the alt provider’s and lowered when it lost to it. On a misprediction, an allocation attempt is made in the first higher-history table whose entry is not useful.

TageSCL.cpp update

C
void TageSCL::update(uint64_t pc, bool taken) { // Update the provider's counter and its usefulness if (provider_ != -1) { uint64_t idx = table_index(pc, provider_); auto& e = tables_[provider_][idx]; if (taken && e.ctr < 3) e.ctr++; else if (!taken && e.ctr > -4) e.ctr--; // Usefulness rises only when the provider disagreed with // the alt provider and turned out to be the right one. if (alt_provider_ != -1) { uint64_t aidx = table_index(pc, alt_provider_); bool alt_pred = tables_[alt_provider_][aidx].ctr >= 0; if (alt_pred != provider_pred_) { if (provider_pred_ == taken) { if (e.u < 3) e.u++; } else if (e.u > 0) { e.u--; } } } } else { // Update the base bimodal uint64_t bi = base_index(pc); if (taken && base_[bi] < 3) base_[bi]++; else if (!taken && base_[bi] > 0) base_[bi]--; } // On misprediction by the provider, attempt allocation // in a higher-history table. if (provider_pred_ != taken) { for (int t = std::max(0, provider_) + 1; t < N_TABLES; t++) { uint64_t idx = table_index(pc, t); if (tables_[t][idx].u == 0) { tables_[t][idx].tag = tag_for(pc, t) & 0xFF; tables_[t][idx].ctr = taken ? 0 : -1; tables_[t][idx].u = 0; break; } } } // Decay usefulness counters periodically (simplified) static uint64_t access_count = 0; if (++access_count % 256 == 0) { for (int t = 0; t < N_TABLES; t++) { for (auto& e : tables_[t]) { if (e.u > 0) e.u--; } } } // Update SC weight if SC was used. if (used_sc_) { uint64_t sc_idx = (pc ^ ghr_[0]) % SC_ENTRIES; int8_t& w = sc_weights_[sc_idx]; if (sc_pred_ != taken) { int v = w + (taken ? +1 : -1); w = static_cast<int8_t>(std::clamp(v, -128, 127)); } } ghr_shift(taken); }

The total storage budget for the configuration above is approximately: 40962+54096(3+8+2)+10248=8192+266240+8192=282624 bits,4096 \cdot 2 + 5 \cdot 4096 \cdot (3 + 8 + 2) + 1024 \cdot 8 = 8192 + 266240 + 8192 = 282624 \text{ bits}, or roughly 35 KiB. The simplified TAGE-SC-L achieves competitive MPKI, though at that size it sits slightly above the 32 KB CBP budget category and would need a modest trim to enter it.

07.Trace Reader

The trace reader consumes the CBP trace format. The CBP trace is a sequence of fixed-size records, optionally xz-compressed. Each record contains the branch PC, the branch type, the outcome, and the target for taken branches.

Trace.hpp

C
#pragma once #include "Predictor.hpp" #include <fstream> #include <memory> #include <string> class TraceReader { public: explicit TraceReader(const std::string& path); ~TraceReader(); // Returns true and fills br if a record was read. // Returns false on EOF or error. bool next(BranchRecord& br); uint64_t total_branches() const { return total_; } uint64_t total_instructions() const { return total_instrs_; } private: std::unique_ptr<std::istream> in_; uint64_t total_; uint64_t total_instrs_; };

Trace.cpp

C
#include "Trace.hpp" #include <cstring> #include <stdexcept> struct CbpRecord { uint64_t pc; uint8_t taken; uint8_t type; uint64_t target; uint32_t instrs_since_last; // for MPKI computation }; TraceReader::TraceReader(const std::string& path) : in_(std::make_unique<std::ifstream>(path, std::ios::binary)), total_(0), total_instrs_(0) { if (!in_ || !in_->good()) { throw std::runtime_error("Cannot open trace: " + path); } } TraceReader::~TraceReader() = default; bool TraceReader::next(BranchRecord& br) { CbpRecord rec; in_->read(reinterpret_cast<char*>(&rec), sizeof(rec)); if (in_->gcount() != sizeof(rec)) return false; br.pc = rec.pc; br.taken = (rec.taken != 0); br.type = rec.type; br.target = rec.target; total_++; total_instrs_ += rec.instrs_since_last; return true; }

08.Evaluator Driver

The main driver opens a trace, instantiates the selected predictor, runs the predict-update loop to exhaustion, and prints the MPKI plus the storage cost.

main.cpp

C
#include "Predictor.hpp" #include "Trace.hpp" #include "Gshare.hpp" #include "Perceptron.hpp" #include "TageSCL.hpp" #include <iostream> #include <memory> #include <string> static std::unique_ptr<Predictor> make_predictor(const std::string& name) { if (name == "gshare") { return std::make_unique<Gshare>(14); // 16K entries } else if (name == "perceptron") { return std::make_unique<Perceptron>(1024, 36, 14); } else if (name == "tage-sc-l") { return std::make_unique<TageSCL>(); } else { throw std::invalid_argument("Unknown predictor: " + name); } } int main(int argc, char** argv) { if (argc < 3) { std::cerr << "Usage: bpeval <predictor> <trace>\n" << " predictor: gshare, perceptron, tage-sc-l\n"; return 1; } auto p = make_predictor(argv[1]); TraceReader t(argv[2]); uint64_t mispredicts = 0; BranchRecord br; while (t.next(br)) { if (br.type != 0) continue; // only conditional branches bool pred = p->predict(br.pc); if (pred != br.taken) mispredicts++; p->update(br.pc, br.taken); } double mpki = 1000.0 * mispredicts / t.total_instructions(); std::cout << "predictor=" << p->name() << " storage_bits=" << p->storage_bits() << " branches=" << t.total_branches() << " mispredicts=" << mispredicts << " MPKI=" << mpki << "\n"; return 0; }

Run the evaluator:

Run on a sample trace

Plain Text
./build/bpeval gshare traces/gcc_166B.trace
./build/bpeval perceptron traces/gcc_166B.trace
./build/bpeval tage-sc-l traces/gcc_166B.trace

09.Evaluation and MPKI Results

The CBP-5 (2016) traces include over 200 benchmark workloads covering SPEC CPU 2006 and CPU 2017 plus several industry traces. The published winning entries achieve MPKI in the range of 2 to 8 on most of the suite, with the easiest workloads falling below 1 and the hardest running above 10.

The expected MPKI for the three predictors on a representative sample of workloads is shown in the table below. The values are illustrative ranges from the published CBP-5 results [1] and from re-implementations in academic comparison papers. Exact numbers depend on the configuration of each predictor and the trace selection.

Table 1. Expected MPKI for the three predictors on a representative CBP-5 benchmark mix. Lower is better. Storage budgets differ across the three predictors. The perceptron and TAGE-SC-L configurations are roughly matched at 35 to 37� KiB while gshare runs at 4� KiB.

Benchmarkgshare (4 KiB)perceptron (37 KiB)TAGE-SC-L (35 KiB)
gcc6.24.83.1
mcf14.012.511.8
xalancbmk5.84.02.4
sjeng9.07.55.6
hmmer1.51.00.6
namd0.40.40.3

The pattern across the table is consistent. gshare delivers respectable MPKI at the smallest storage budget. The perceptron catches up significantly on most benchmarks, with the geometric-mean improvement around 20%. TAGE-SC-L wins on most benchmarks, with the geometric-mean MPKI 40% to 50% below gshare’s. On a few benchmarks (notably namd, where the mispredict rate is already very low), the three predictors converge.

10.Storage vs Accuracy

A useful exercise is to sweep the storage budget for each predictor and plot the resulting MPKI. The shape of the curve reveals the marginal accuracy gain per added storage bit.

For gshare, MPKI drops as the table grows until aliasing stops being the dominant error source, after which the curve flattens. The knee is typically at 16 to 64 KiB.

For perceptron, MPKI drops with the history length up to the point where the per-branch weight vector saturates the training rate. The knee is typically at history length 30 to 40.

For TAGE-SC-L, the curve has multiple knees. Doubling the number of tagged tables produces an MPKI drop until the geometric history coverage is dense enough. Adding the SC component costs about 3% more storage in the configuration built here, and produces a measurable MPKI drop on the hardest benchmarks. Production SC components are proportionally larger, closer to 10% of the budget. Further additions show diminishing returns.

The general lesson is that the trade-off curves are predictor-specific. A 32 KB TAGE-SC-L is meaningfully better than a 32 KB gshare. A 4 KB gshare may outperform a 4 KB TAGE-SC-L because TAGE’s tables overhead is not amortized at small budgets.

11.Worked Examples

12.Exercises

References

  1. [1]Hennessy, John L. and Patterson, David A. (2019). “Computer Architecture: A Quantitative Approach.” Morgan Kaufmann.
Book mode
computer-architectureadvanced-ilp-and-out-of-order-execution
Was this helpful?