Skip to content

Add enhanced branch predictor phase-1 integration and tests - #301

Open
kumavatkomal wants to merge 4 commits into
riscv-software-src:masterfrom
kumavatkomal:komal/branch-predictor-phase1
Open

Add enhanced branch predictor phase-1 integration and tests#301
kumavatkomal wants to merge 4 commits into
riscv-software-src:masterfrom
kumavatkomal:komal/branch-predictor-phase1

Conversation

@kumavatkomal

@kumavatkomal kumavatkomal commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

This is a PR for Phase-1 branch predictor integration work.
The goal is to land implementation and test wiring first, while keeping EDM comparison and final performance claims for follow-up work.

Included in this PR

  • Enhanced branch predictor integration in fetch flow
  • Predictor selection and config wiring
  • Branch prediction evaluation and update path integration
  • Branch predictor unit and integration test updates
  • Required architecture parameter updates for predictor selection

Not included yet

  • Final EDM vs trace comparison results
  • Final measured claim locking based on EDM runs
  • Final report artifacts and final comparison tables

Validation completed

  • Build completed for updated targets
  • Branch predictor tests passed after integration updates

Commands used for validation

  • cmake --build build --target fetch BranchPred_test -j4
  • ./build/test/core/branch_pred/BranchPred_test

issue tracking

Related to #1

@kumavatkomal
kumavatkomal marked this pull request as ready for review May 4, 2026 13:35
Comment thread core/fetch/Fetch.cpp Outdated
Comment on lines +64 to +66
std::transform(branch_predictor_name_.begin(), branch_predictor_name_.end(),
branch_predictor_name_.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::transform shouldn't be used for in-place modification of a container:

std::transform does not guarantee in-order application of unary_op or binary_op. To apply a function to a sequence in-order or to apply a function that modifies the elements of a sequence, use std::for_each.

Comment thread core/fetch/Fetch.cpp Outdated
Comment on lines +68 to +70
const auto is_power_of_two = [](const uint32_t value) {
return value && ((value & (value - 1)) == 0);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use sparta::utils::is_power_of_2

Comment thread core/fetch/EnhancedBranchPred.cpp Outdated
Comment on lines +16 to +19
bool isPowerOfTwo(const uint32_t value)
{
return value && ((value & (value - 1)) == 0);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use sparta::utils::is_power_of_2

uint64_t EnhancedBranchPredictor::btbCacheAddress_(uint64_t fetch_pc) const {
// Convert instruction-addressed fetch PC into the cache-line address
// expected by SimpleCache2 while preserving intended BTB indexing.
return fetch_pc << (kBTBCacheLineShift - kCompressedIndexShift);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you want this to be a right-shift?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still an issue?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good call. @kumavatkomal this should be a right shift.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right shift causes 6 test failures in BranchPred_test. Left shift passes all tests. Should the tests be updated, or am I missing something else that needs to change?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I understand what's happening now. The Sparta cache for the BTB is configured with 1KB lines, so the cache address decoder does >> 10 to get the index bits. We actually want it to use PC[8:1] for the lookup, so left shifting it by 9 bits first takes care of that.

A different way to handle this would be to set kBTBCacheLineBytes = 2 and configure btb_cache_ like so:

    btb_cache_ = std::make_unique<sparta::cache::SimpleCache2<BTBCacheLine>>(
            btb_entries_ * kBTBCacheLineBytes,
            kBTBCacheLineBytes,
            kBTBCacheLineBytes,
            BTBCacheLine(),
            *btb_replacement_policy_,
            false);
    }

This would configure the address decoder in the cache to just >> 1, and then this function can be removed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented your suggestion:

  • kBTBCacheLineBytes = 2 with btb_entries_ * kBTBCacheLineBytes and false parameter

  • Removed btbCacheAddress_() function, now using fetch_pc directly

  • Cache decoder now does >> 1 for PC[8:1] indexing

All tests pass. Is this correct?

Comment thread core/fetch/EnhancedBranchPred.cpp Outdated
Comment on lines +66 to +77
bool EnhancedBranchPredictor::btbLookup_(uint64_t fetch_pc, BTBEntry * entry) {
auto cache_line = btb_cache_->peekLine(btbCacheAddress_(fetch_pc));
if ((cache_line != nullptr) && cache_line->isValid()) {
if (entry != nullptr) {
*entry = cache_line->btb_entry;
}
// Keep replacement state hot for recently used BTB lines.
btb_cache_->touchMRU(*cache_line);
return true;
}
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could just have this return a pointer to cache_line->btb_entry on a hit and a nullptr on a miss. This would also avoid the overhead of copying around a BTBEntry on every hit.

Comment thread core/fetch/EnhancedBranchPred.cpp Outdated
// One path for both invalid-line fill and victim replacement.
auto & replacement_line = btb_cache_->getLineForReplacementWithInvalidCheck(btb_addr);
btb_cache_->allocateWithMRUUpdate(replacement_line, btb_addr);
replacement_line.btb_entry = entry;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider altering replacement_line.btb_entry directly instead of copying over it.

Comment thread core/fetch/EnhancedBranchPred.cpp Outdated
Comment on lines +152 to +157
} else {
const uint64_t default_target = update.actually_taken ?
update.corrected_PC :
(update.fetch_PC + update.branch_idx + bytes_per_inst);
entry = BTBEntry(update.branch_idx, default_target);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One potential optimization here is to only allocate new BTB entries after a taken branch is mispredicted.

Comment thread core/fetch/Fetch.cpp Outdated
Comment on lines +252 to +268
for (auto it = insts_to_send->begin(); it != insts_to_send->end(); ++it, ++i)
{
const auto & inst = *it;
if (!inst->isBranch()) {
continue;
}

found_branch = true;
actual_branch_idx = i;
actual_branch_inst = inst;
if (inst->isTakenBranch()) {
actual_next_pc = inst->getTargetVAddr();
} else {
actual_next_pc = inst->getPC() + BranchPredictorIFType::bytes_per_inst;
}
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you could do something like this:

auto it = std::find_if(insts_to_send->begin(), insts_to_send->end(), [](const InstPtr& inst) { return inst->isBranch(); });

if(it == insts_to_send->end())
{
    return;
}

// Now process the branch update

Comment thread core/fetch/Fetch.cpp
InstPtr actual_branch_inst;

// Predictor contract: one prediction per fetch packet, keyed by fetch PC.
// We therefore train/evaluate against the first branch in this packet.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there are multiple branches in a fetch packet? If there is a never taken conditional branch followed by an always taken conditional branch, the second branch will never be inserted into the BTB and we will always mispredict.

if (bhtPredict_(input.fetch_PC)) {
prediction.predicted_PC = btb_entry.predicted_PC;
} else {
prediction.predicted_PC = input.fetch_PC + prediction.branch_idx + bytes_per_inst;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't the correct fallthrough PC. The way branch_idx is calculated in Fetch::evaluateBranchPrediction_ doesn't account for non-branch instructions in a fetch bundle.

@klingaard klingaard left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the testing!

Comment thread arches/medium_core.yaml Outdated

top.cpu.core0:
fetch.params.num_to_fetch: 3
fetch.params.branch_predictor: simple

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since medium core inherits from small core, it automatically picks up the simple branch predictor.

Comment thread arches/big_core.yaml Outdated

top.cpu.core0:
fetch.params.num_to_fetch: 8
fetch.params.branch_predictor: simple

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since big core inherits from medium/small core, it automatically picks up the simple branch predictor.

Comment thread core/fetch/EnhancedBranchPred.cpp Outdated
}

uint32_t EnhancedBranchPredictor::bhtIndex_(uint64_t fetch_pc) const {
return static_cast<uint32_t>((fetch_pc >> 1) & (bht_entries_ - 1));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this lose bit precision?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cast happens after masking, so precision is preserved. The mask (bht_entries_ - 1) extracts only the lower 9 bits (for 512 entries), which easily fits in uint32_t. No bits are lost.

Is there a specific precision concern I should address?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this goes directly into a vector lookup, you could return a size_t instead and avoid a conversion.

Comment thread core/fetch/EnhancedBranchPred.cpp Outdated
}

bool EnhancedBranchPredictor::bhtPredict_(uint64_t fetch_pc) const {
return branch_history_table_[bhtIndex_(fetch_pc)] > 1;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest you use at to avoid overflow: branch_history_table_.at()

Comment thread core/fetch/EnhancedBranchPred.cpp Outdated
}

void EnhancedBranchPredictor::bhtUpdate_(uint64_t fetch_pc, bool actually_taken) {
auto & counter = branch_history_table_[bhtIndex_(fetch_pc)];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use at()

Comment thread core/fetch/Fetch.cpp Outdated
Comment on lines +28 to +31
branch_predictor_name_(p->branch_predictor),
enhanced_btb_entries_(p->enhanced_btb_entries),
enhanced_btb_ways_(p->enhanced_btb_ways),
enhanced_bht_entries_(p->enhanced_bht_entries),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these members of Fetch? Does Fetch need to know this information? Looks like it's passed to the predictor instantiations below.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right - Fetch doesn't need to store these after construction. They're only used to pass to the predictor. Should I update this to read from ParameterSet and pass directly without storing as member variables?

Comment thread core/fetch/Fetch.cpp Outdated
Comment on lines +71 to +79
sparta_assert(enhanced_btb_entries_ > 0, "enhanced_btb_entries must be > 0");
sparta_assert(enhanced_btb_ways_ > 0, "enhanced_btb_ways must be > 0");
sparta_assert(enhanced_bht_entries_ > 0, "enhanced_bht_entries must be > 0");
sparta_assert((enhanced_btb_entries_ % enhanced_btb_ways_) == 0,
"enhanced_btb_entries must be divisible by enhanced_btb_ways");
sparta_assert(sparta::utils::is_power_of_2(enhanced_btb_entries_ / enhanced_btb_ways_),
"enhanced BTB set count must be power-of-two");
sparta_assert(sparta::utils::is_power_of_2(enhanced_bht_entries_),
"enhanced_bht_entries must be power-of-two");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these asserts be in EnhancedBranchPredictor?

Comment thread core/fetch/Fetch.cpp
Comment on lines +256 to +261
BranchPredictor::DefaultUpdate update;
update.fetch_PC = input.fetch_PC;
update.branch_idx = actual_branch_idx;
update.corrected_PC = actual_next_pc;
update.actually_taken = actual_branch_inst->isTakenBranch();
branch_predictor_->updatePredictor(update);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the updatePredictor method should populate predictor object. Fetch should know nothing about how the predictor works.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fetch builds DefaultUpdate because it has the instruction data. This is the same pattern used by SimpleBranchPredictor - both receive DefaultUpdate objects. Should I change both predictors to use a different approach, or is the current design acceptable?

@bdutro bdutro Jun 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, let's keep the current interface for now. In the future we may want the update object to contain the instruction data.

Comment thread core/fetch/Fetch.cpp

// Track end-to-end prediction quality at fetch integration level.
++branch_predictions_;
if (mispredicted) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bdutro question for you: should we count the mispredict here or when the branch is resolved?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should count them when the branch is resolved to avoid overcounting mispredicts that happen in the shadow of another mispredict/flush/etc.

Comment thread core/fetch/Fetch.hpp Outdated
Comment on lines +132 to +137
std::string branch_predictor_name_;

// Geometry knobs consumed when enhanced predictor is selected.
const uint32_t enhanced_btb_entries_;
const uint32_t enhanced_btb_ways_;
const uint32_t enhanced_bht_entries_;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't see a need to keep these around

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed - removed unused counter variables.

For the config variables (enhanced_btb_entries_, enhanced_btb_ways_, enhanced_bht_entries_): these are currently stored because they're read from ParameterSet and passed to predictor constructor. Should I update this to read and pass directly without storing? Let me know and I'll make the change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I update this to read and pass directly without storing?

Yes, that's the preferred way to handle this.

Comment thread core/fetch/Fetch.cpp Outdated
Comment on lines +70 to +72
p->enhanced_btb_entries,
p->enhanced_btb_ways,
p->enhanced_bht_entries));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kumavatkomal, you need to call ignore() on these parameters when branch_predictor_name_ != "enhanced" to fix the regression failures.

Comment thread core/fetch/Fetch.cpp

std::for_each(branch_predictor_name_.begin(), branch_predictor_name_.end(),
[](char& ch) { ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch))); });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you can do this here and then use the local variables when you instantiate the EnhancedBranchPredictor:

Suggested change
const auto enhanced_btb_entries = p->enhanced_btb_entries;
const auto enhanced_btb_ways = p->enhanced_btb_ways;
const auto enhanced_bht_entries = p->enhanced_bht_entries;

@oluwatimilehin

Copy link
Copy Markdown

Hi all, is there any more work planned for this PR, or is it just waiting to be merged?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants