mirror of
https://github.com/apple/swift.git
synced 2025-12-21 12:14:44 +01:00
The old analysis pass doesn't take into account profile data, nor does it consider post-dominance. It primarily dealt with _fastPath/_slowPath. A block that is dominated by a cold block is itself cold. That's true whether it's forwards or backwards dominance. We can also consider a call to any `Never` returning function as a cold-exit, though the block(s) leading up to that call may be executed frequently because of concurrency. For now, I'm ignoring the concurrency case and assuming it's cold. To make use of this "no return" prediction, use the `-enable-noreturn-prediction` flag, which is currently off by default.
464 lines
15 KiB
C++
464 lines
15 KiB
C++
//===--- ColdBlockInfo.cpp - Hot/cold block analysis for the SIL CFG ------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
|
|
// Licensed under Apache License v2.0 with Runtime Library Exception
|
|
//
|
|
// See https://swift.org/LICENSE.txt for license information
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "swift/AST/SemanticAttrs.h"
|
|
#include "swift/SIL/BasicBlockBits.h"
|
|
#include "swift/SIL/SILArgument.h"
|
|
#include "swift/SIL/SILModule.h"
|
|
#include "swift/SILOptimizer/Analysis/ColdBlockInfo.h"
|
|
#include "swift/SILOptimizer/Analysis/DominanceAnalysis.h"
|
|
|
|
#define DEBUG_TYPE "cold-block-info"
|
|
|
|
using namespace swift;
|
|
|
|
bool isColdEnergy(ColdBlockInfo::Energy e);
|
|
|
|
ColdBlockInfo::ColdBlockInfo(DominanceAnalysis *DA,
|
|
PostDominanceAnalysis *PDA) : DA(DA), PDA(PDA) {
|
|
LLVM_DEBUG(llvm::dbgs() << "ColdBlockInfo: constructed\n");
|
|
}
|
|
|
|
static std::string toString(SILBasicBlock const *bb) {
|
|
std::string str = bb->getParent()->getName().str();
|
|
str += "::bb" + std::to_string(bb->getDebugID());
|
|
return str;
|
|
}
|
|
|
|
static StringRef toString(ColdBlockInfo::Energy e) {
|
|
if (e == ColdBlockInfo::Energy::full())
|
|
return "<<ALL>>";
|
|
else if (e.empty())
|
|
return "<<NONE>>";
|
|
else if (e.contains(ColdBlockInfo::State::Warm))
|
|
return "warm";
|
|
else if (e.contains(ColdBlockInfo::State::Cold))
|
|
return "cold";
|
|
else
|
|
llvm_unreachable("unhandled energy state");
|
|
}
|
|
|
|
static StringRef toString(ColdBlockInfo::State::Temperature t) {
|
|
ColdBlockInfo::Energy e;
|
|
e.insert(t);
|
|
return toString(e);
|
|
}
|
|
|
|
void ColdBlockInfo::dump() const {
|
|
unsigned warm = 0, cold = 0;
|
|
llvm::dbgs() << "ColdBlockInfo {\n";
|
|
for (auto pair : EnergyMap) {
|
|
auto energy = pair.getSecond();
|
|
isColdEnergy(energy) ? cold++ : warm++;
|
|
|
|
llvm::dbgs() << toString(pair.getFirst())
|
|
<< " -> " << toString(energy) << "\n";
|
|
}
|
|
llvm::dbgs() << "STATISTICS: warm " << warm << " | cold " << cold << "\n}\n";
|
|
}
|
|
|
|
inline bool isCriticalEdge(SILBasicBlock *predBB, SILBasicBlock *succBB) {
|
|
return !(predBB->getSingleSuccessorBlock() == succBB
|
|
|| succBB->getSinglePredecessorBlock() == predBB);
|
|
}
|
|
static bool hasCriticalEdge(SILBasicBlock *BB) {
|
|
return llvm::any_of(BB->getSuccessorBlocks(), [&](auto *succBB) {
|
|
if (!isCriticalEdge(BB, succBB))
|
|
return false;
|
|
|
|
LLVM_DEBUG(llvm::dbgs() << "ColdBlockInfo: "
|
|
<< toString(BB) << " -> " << toString(succBB)
|
|
<< " is a critical edge!\n");
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/// A cold terminator is one where it's unlikely to be reached, which are
|
|
/// function exits that are less-common. A cold terminator implies a cold block.
|
|
/// - 'unreachable', as it's never executed.
|
|
/// - 'throw', if throws prediction is enabled.
|
|
static bool isColdTerminator(const TermInst *term) {
|
|
switch (term->getTermKind()) {
|
|
case TermKind::AwaitAsyncContinuationInst:
|
|
case TermKind::BranchInst:
|
|
case TermKind::CondBranchInst:
|
|
case TermKind::SwitchValueInst:
|
|
case TermKind::SwitchEnumInst:
|
|
case TermKind::SwitchEnumAddrInst:
|
|
case TermKind::DynamicMethodBranchInst:
|
|
case TermKind::CheckedCastBranchInst:
|
|
case TermKind::CheckedCastAddrBranchInst:
|
|
case TermKind::TryApplyInst:
|
|
case TermKind::YieldInst:
|
|
case TermKind::ReturnInst:
|
|
return false;
|
|
case TermKind::ThrowInst:
|
|
case TermKind::ThrowAddrInst:
|
|
case TermKind::UnwindInst:
|
|
return term->getModule().getOptions().EnableThrowsPrediction;
|
|
case TermKind::UnreachableInst:
|
|
// For now, assume it's always cold, since it's executed once.
|
|
// Not all functions in the stdlib are properly annotated as a
|
|
// "known program termination point", so we don't use
|
|
// ApplySite::isCalleeKnownProgramTerminationPoint.
|
|
return term->getModule().getOptions().EnableNoReturnCold;
|
|
}
|
|
}
|
|
|
|
/// Peek through an extract of Bool.value.
|
|
static SILValue getCondition(SILValue C) {
|
|
if (auto *SEI = dyn_cast<StructExtractInst>(C)) {
|
|
if (auto *Struct = dyn_cast<StructInst>(SEI->getOperand()))
|
|
return Struct->getFieldValue(SEI->getField());
|
|
return SEI->getOperand();
|
|
}
|
|
return C;
|
|
}
|
|
|
|
constexpr unsigned RecursionDepthLimit = 3;
|
|
std::optional<bool>
|
|
ColdBlockInfo::searchForExpectedValue(SILValue Cond,
|
|
unsigned recursionDepth) {
|
|
if (recursionDepth > RecursionDepthLimit)
|
|
return std::nullopt;
|
|
|
|
if (auto *Arg = dyn_cast<SILArgument>(Cond)) {
|
|
llvm::SmallVector<std::pair<SILBasicBlock *, SILValue>, 4> InValues;
|
|
if (!Arg->getIncomingPhiValues(InValues))
|
|
return std::nullopt;
|
|
|
|
std::optional<bool> expectedValue;
|
|
|
|
// Check all predecessor values which come from non-cold blocks.
|
|
for (auto Pair : InValues) {
|
|
auto *predBB = Pair.first;
|
|
auto predArg = Pair.second;
|
|
|
|
if (isCold(predBB))
|
|
continue;
|
|
|
|
std::optional<bool> predecessorValue;
|
|
|
|
if (auto *IL = dyn_cast<IntegerLiteralInst>(predArg)) {
|
|
predecessorValue = IL->getValue().getBoolValue();
|
|
} else {
|
|
predecessorValue = searchForExpectedValue(predArg, recursionDepth+1);
|
|
}
|
|
|
|
// There's at least one non-cold predecessor with an unknown value.
|
|
if (!predecessorValue)
|
|
return std::nullopt;
|
|
|
|
// If this is the first non-cold predecessor, save the value.
|
|
if (!expectedValue) {
|
|
expectedValue = *predecessorValue;
|
|
continue;
|
|
}
|
|
|
|
// Check if we have a consistent value across all non-cold predecessors.
|
|
if (*expectedValue != *predecessorValue)
|
|
return std::nullopt;
|
|
}
|
|
|
|
return expectedValue;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
static std::optional<bool> getExpectedValue(SILValue Cond) {
|
|
// Handle the fully inlined Builtin.
|
|
if (auto *BI = dyn_cast<BuiltinInst>(Cond)) {
|
|
if (BI->getIntrinsicInfo().ID == llvm::Intrinsic::expect) {
|
|
// peek through an extract of Bool.value.
|
|
SILValue ExpectedValue = getCondition(BI->getArguments()[1]);
|
|
if (auto *Literal = dyn_cast<IntegerLiteralInst>(ExpectedValue)) {
|
|
return (Literal->getValue() == 0) ? false : true;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Handle the @semantic functions used for branch hints.
|
|
auto AI = dyn_cast<ApplyInst>(Cond);
|
|
if (!AI)
|
|
return std::nullopt;
|
|
|
|
if (auto *F = AI->getReferencedFunctionOrNull()) {
|
|
if (F->hasSemanticsAttrs()) {
|
|
// fastpath/slowpath attrs are untested because the inliner luckily
|
|
// inlines them before the downstream calls.
|
|
if (F->hasSemanticsAttr(semantics::SLOWPATH))
|
|
return false;
|
|
else if (F->hasSemanticsAttr(semantics::FASTPATH))
|
|
return true;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
/// The minimum probability that an edge is taken to be considered "warm".
|
|
constexpr double WARM_EDGE_MINIMUM = 3.0 / 100.0;
|
|
|
|
// Using the profile data on the terminator of this block, annotate successors
|
|
// with cold/warm information.
|
|
//
|
|
// \returns true if an inference was made
|
|
bool ColdBlockInfo::inferFromEdgeProfile(SILBasicBlock *BB) {
|
|
ProfileCounter totalCount{0};
|
|
SmallVector<ProfileCounter, 2> succCount;
|
|
|
|
// Current analysis only accurately handles blocks with 2 successors,
|
|
// especially since we only have two temperatures.
|
|
if (BB->getNumSuccessors() != 2)
|
|
return false;
|
|
|
|
// Check the successor edges for profile data.
|
|
for (auto const &succ : BB->getSuccessors()) {
|
|
auto counter = succ.getCount();
|
|
|
|
// Can't make an inference if there's profile data missing for a successor.
|
|
// FIXME: there are techniques to determine a missing count;
|
|
// see the SamplePGO paper by Diego Novillo.
|
|
if (!counter)
|
|
return false;
|
|
|
|
succCount.push_back(counter);
|
|
|
|
auto didSaturate = totalCount.add_saturating(counter);
|
|
|
|
ASSERT(!didSaturate && "should rescale the profile data first");
|
|
(void)didSaturate;
|
|
}
|
|
|
|
TermInst::ConstSuccessorListTy succs = BB->getSuccessors();
|
|
ASSERT(succCount.size() == succs.size());
|
|
|
|
// Record temperatures.
|
|
for (size_t i = 0; i < succs.size(); i++) {
|
|
double takenProbability =
|
|
succCount[i].getValue() / (double)totalCount.getValue();
|
|
|
|
// It's a cold edge if the profiling-based probability is below the threshold.
|
|
auto state =
|
|
takenProbability < WARM_EDGE_MINIMUM ? ColdBlockInfo::State::Cold
|
|
: ColdBlockInfo::State::Warm;
|
|
|
|
set(succs[i], state);
|
|
|
|
LLVM_DEBUG(llvm::dbgs()
|
|
<< "ColdBlockInfo: setting to " << toString(state)
|
|
<< " (inferFromEdgeProfile): " << toString(succs[i])
|
|
<< " has taken probability " << takenProbability << "\n");
|
|
ASSERT(takenProbability >= 0);
|
|
ASSERT(takenProbability <= 1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void ColdBlockInfo::analyze(SILFunction *fn) {
|
|
LLVM_DEBUG(llvm::dbgs() << "--> Before Stage 1\n");
|
|
LLVM_DEBUG(dump());
|
|
|
|
BasicBlockSet foundExpectedCond(fn);
|
|
|
|
// Stage 1: Seed the graph with warm/cold blocks.
|
|
for (auto &BB : *fn) {
|
|
auto *term = BB.getTerminator();
|
|
|
|
// Check for a cold exit.
|
|
if (isColdTerminator(term)) {
|
|
assert(term->getNumSuccessors() == 0);
|
|
LLVM_DEBUG(llvm::dbgs()
|
|
<< "ColdBlockInfo: resetting to cold (isColdTerminator): "
|
|
<< toString(&BB) << "\n");
|
|
|
|
// Overwrite any existing temperatures.
|
|
resetToCold(&BB);
|
|
continue;
|
|
}
|
|
|
|
// Check profile data for successors, choosing it first over branch hints.
|
|
if (inferFromEdgeProfile(&BB)) {
|
|
foundExpectedCond.insert(&BB);
|
|
continue;
|
|
}
|
|
|
|
// Check for an obvious _fastPath / _slowPath condition for successors.
|
|
if (auto *CBI = dyn_cast<CondBranchInst>(term)) {
|
|
if (auto val = getExpectedValue(getCondition(CBI->getCondition()))) {
|
|
setExpectedCondition(CBI, val);
|
|
foundExpectedCond.insert(&BB);
|
|
}
|
|
}
|
|
}
|
|
|
|
LLVM_DEBUG(llvm::dbgs() << "--> After Stage 1; changedMap = "
|
|
<< changedMap << "\n");
|
|
LLVM_DEBUG(dump(); changedMap = false);
|
|
|
|
// Stage 2: Propagate via dominators
|
|
SmallVector<SILBasicBlock *, 8> scratch;
|
|
for (auto &BB : *fn) {
|
|
scratch.clear();
|
|
|
|
if (isCold(&BB)) {
|
|
// Mark all blocks I dominate as cold.
|
|
auto *domInfo = DA->get(fn);
|
|
domInfo->getDescendants(&BB, scratch);
|
|
for (auto *dominatedBB : scratch) {
|
|
if (dominatedBB == &BB)
|
|
continue;
|
|
LLVM_DEBUG(llvm::dbgs() << "ColdBlockInfo: resetting to cold (dominatedBB): "
|
|
<< toString(dominatedBB) << "\n");
|
|
resetToCold(dominatedBB);
|
|
}
|
|
} else {
|
|
// Mark myself cold if I'm post-dominated by a cold block.
|
|
auto *pdInfo = PDA->get(fn);
|
|
scratch.push_back(&BB);
|
|
auto *node = pdInfo->getNode(&BB);
|
|
bool foundCold = false;
|
|
|
|
while (node && !foundCold) {
|
|
node = node->getIDom();
|
|
if (!node || pdInfo->isVirtualRoot(node))
|
|
break;
|
|
|
|
auto *postBB = node->getBlock();
|
|
if (isCold(postBB)) {
|
|
foundCold = true;
|
|
} else {
|
|
scratch.push_back(postBB);
|
|
}
|
|
}
|
|
|
|
if (foundCold) {
|
|
for (auto *chainBB : scratch) {
|
|
LLVM_DEBUG(llvm::dbgs() << "ColdBlockInfo: resetting to cold (chainBB): "
|
|
<< toString(chainBB) << "\n");
|
|
resetToCold(chainBB);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
LLVM_DEBUG(llvm::dbgs() << "--> After Stage 2; changedMap = "
|
|
<< changedMap << "\n");
|
|
LLVM_DEBUG(dump(); changedMap = false);
|
|
|
|
/// Stage 3: Backwards propagate coldness from successors.
|
|
auto isColdBlock = [&](auto *bb) { return isCold(bb); };
|
|
|
|
unsigned completedIters = 0;
|
|
bool changed;
|
|
do {
|
|
changed = false;
|
|
// TODO: We could use a worklist so we progressively visit fewer blocks
|
|
// on each iteration, as we only need to visit non-cold/non-leaf blocks.
|
|
for (auto &BB : llvm::reverse(*fn)) {
|
|
// Only on the first pass, search recursively for an expected value,
|
|
// now that more temperature data has been seeded.
|
|
if (!completedIters && !foundExpectedCond.contains(&BB)) {
|
|
if (auto *CBI = dyn_cast<CondBranchInst>(BB.getTerminator())) {
|
|
auto cond = getCondition(CBI->getCondition());
|
|
if (auto val = searchForExpectedValue(cond)) {
|
|
setExpectedCondition(CBI, val);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Nothing to propagate from.
|
|
if (BB.getNumSuccessors() == 0)
|
|
continue;
|
|
|
|
// Coldness already exists here.
|
|
if (isCold(&BB))
|
|
continue;
|
|
|
|
if (llvm::all_of(BB.getSuccessorBlocks(), isColdBlock)) {
|
|
resetToCold(&BB);
|
|
changed = true;
|
|
}
|
|
}
|
|
completedIters++;
|
|
} while (changed);
|
|
|
|
LLVM_DEBUG(llvm::dbgs() << "--> Final for " << fn->getName() <<
|
|
" | converged after " << completedIters << " iters"
|
|
<< " over " << fn->size() << " blocks; "
|
|
<< " changedMap = " << changedMap << "\n");
|
|
LLVM_DEBUG(dump(); changedMap = false);
|
|
}
|
|
|
|
inline bool isColdEnergy(ColdBlockInfo::Energy e) {
|
|
return e.contains(ColdBlockInfo::State::Cold)
|
|
&& !e.contains(ColdBlockInfo::State::Warm);
|
|
}
|
|
|
|
bool ColdBlockInfo::isCold(const SILBasicBlock *BB) const {
|
|
auto result = EnergyMap.find(BB);
|
|
if (result == EnergyMap.end())
|
|
return false;
|
|
|
|
return isColdEnergy(result->getSecond());
|
|
}
|
|
|
|
void ColdBlockInfo::resetToCold(const SILBasicBlock *BB) {
|
|
auto &entry = EnergyMap.getOrInsertDefault(BB);
|
|
LLVM_DEBUG(isColdEnergy(entry) ? false : changedMap = true);
|
|
entry.removeAll();
|
|
entry.insert(State::Cold);
|
|
}
|
|
|
|
void ColdBlockInfo::set(const SILBasicBlock *BB, State::Temperature temp) {
|
|
auto &entry = EnergyMap.getOrInsertDefault(BB);
|
|
LLVM_DEBUG(entry.contains(temp) ? false : changedMap = true);
|
|
entry.insert(temp);
|
|
}
|
|
|
|
void ColdBlockInfo::setExpectedCondition(CondBranchInst *CBI, ExpectedValue value) {
|
|
if (!value)
|
|
return;
|
|
|
|
// This function marks both sides of the conditional-branch, assuming
|
|
// critical edges are split. If they're NOT, then unexpected things happen.
|
|
// For example, we'd mark bb2 below as cold, which post-dominates the warm
|
|
// block bb1, and thus wipes out the warm annotation on bb1!
|
|
// bb0: [ _fastPath(trueSide) ]
|
|
// │ │
|
|
// │ bb1: [ warm ]
|
|
// │ │
|
|
// │─────────────┘
|
|
// ▼
|
|
// bb2: [ cold ]
|
|
if (hasCriticalEdge(CBI->getParent()))
|
|
return;
|
|
|
|
if (*value) {
|
|
set(CBI->getTrueBB(), State::Warm);
|
|
set(CBI->getFalseBB(), State::Cold);
|
|
LLVM_DEBUG(llvm::dbgs() << "ColdBlockInfo: "
|
|
<< "_fastPath = " << toString(CBI->getTrueBB())
|
|
<< " | _slowPath = " << toString(CBI->getFalseBB())
|
|
<< "\n");
|
|
} else {
|
|
set(CBI->getTrueBB(), State::Cold);
|
|
set(CBI->getFalseBB(), State::Warm);
|
|
LLVM_DEBUG(llvm::dbgs() << "ColdBlockInfo: "
|
|
<< "_fastPath = " << toString(CBI->getFalseBB())
|
|
<< " | _slowPath = " << toString(CBI->getTrueBB())
|
|
<< "\n");
|
|
}
|
|
}
|