用最大生成树减少 PGO 边计数插桩
LLVM 对控制流图按边权从高到低选生成树,用并查集排除环,把边计数器放在非树边上;这是 Kruskal 的最大生成树变体。
工程问题#
PGO 需要获得控制流边的执行次数,但每条边都增加计数器会扩大二进制并引入运行开销。基本块的流入与流出计数满足守恒关系,因此可以只测一部分边,再推导其余边的次数。
实际实现#
CFGMST 为控制流边赋权,按降序排序,每步优先取当前权重最高且不会成环的边,用带路径压缩和按秩合并的并查集检验连通性。高权边优先进入树,PGO 的 getInstrBB 跳过树边,只为可插桩的非树边安排计数器。目标是让更频繁执行的边尽量不直接计数;LLVM 还会调整关键边的权重,并优先纳入部分难以拆分的 landing pad 边。
处理流程#
- 构建包含入口、出口虚拟边的控制流图,并估计边的执行权重
- 按边权降序处理,用并查集判断两个端点是否已连通
- 将连接不同分量的边加入树,跳过会形成环的边
- 为非树边选取插桩位置,必要时拆分关键边,再利用流量守恒还原树边计数
源码片段#
固定版本 · 原文摘录9// This file implements PGO instrumentation using a minimum spanning tree based10// on the following paper:11// [1] Donald E. Knuth, Francis R. Stevenson. Optimal measurement of points12// for program frequency counts. BIT Numerical Mathematics 1973, Volume 13,13// Issue 3, pp 313-32214// The idea of the algorithm based on the fact that for each node (except for15// the entry and exit), the sum of incoming edge counts equals the sum of16// outgoing edge counts. The count of edge on spanning tree can be derived from17// those edges not on the spanning tree. Knuth proves this method instruments18// the minimum number of edges.19//20// The minimal spanning tree here is actually a maximum weight tree -- on-tree21// edges have higher frequencies (more likely to execute). The idea is to22// instrument those less frequently executed edges to reduce the runtime23// overhead of instrumented binaries.文件开头说明流量守恒和树边计数恢复,并明确这里实际选择最大权重树,使非树边的执行频率尽量较低。
59 // Find the root group of the G and compress the path from G to the root.60 BBInfo *findAndCompressGroup(BBInfo *G) {61 if (G->Group != G)62 G->Group = findAndCompressGroup(static_cast<BBInfo *>(G->Group));63 return static_cast<BBInfo *>(G->Group);64 }6566 // Union BB1 and BB2 into the same group and return true.67 // Returns false if BB1 and BB2 are already in the same group.68 bool unionGroups(const BasicBlock *BB1, const BasicBlock *BB2) {69 BBInfo *BB1G = findAndCompressGroup(&getBBInfo(BB1));70 BBInfo *BB2G = findAndCompressGroup(&getBBInfo(BB2));7172 if (BB1G == BB2G)73 return false;查找代表节点时压缩路径;若两个端点已有同一代表,加入该边会成环,合并返回 false。
75 // Make the smaller rank tree a direct child or the root of high rank tree.76 if (BB1G->Rank < BB2G->Rank)77 BB1G->Group = BB2G;78 else {79 BB2G->Group = BB1G;80 // If the ranks are the same, increment root of one tree by one.81 if (BB1G->Rank == BB2G->Rank)82 BB1G->Rank++;83 }84 return true;85 }将低秩树接到高秩树;两者同秩时增加新根的秩。这是该实现中并查集的维护规则。
218 // Sort CFG edges based on its weight.219 void sortEdgesByWeight() {220 llvm::stable_sort(AllEdges, [](const std::unique_ptr<Edge> &Edge1,221 const std::unique_ptr<Edge> &Edge2) {222 return Edge1->Weight > Edge2->Weight;223 });224 }225226 // Traverse all the edges and compute the Minimum Weight Spanning Tree227 // using union-find algorithm.228 void computeMinimumSpanningTree() {229 // First, put all the critical edge with landing-pad as the Dest to MST.230 // This works around the insufficient support of critical edges split231 // when destination BB is a landing pad.232 for (auto &Ei : AllEdges) {233 if (Ei->Removed)234 continue;235 if (Ei->IsCritical) {236 if (Ei->DestBB && Ei->DestBB->isLandingPad()) {237 if (unionGroups(Ei->SrcBB, Ei->DestBB))238 Ei->InMST = true;比较器按 Weight 降序排列。一般贪心选择前,先尽可能把目标为 landing pad 的关键边放入树,体现真实插桩约束。
243 for (auto &Ei : AllEdges) {244 if (Ei->Removed)245 continue;246 // If we detect infinite loops, force247 // instrumenting the entry edge:248 if (!ExitBlockFound && Ei->SrcBB == nullptr)249 continue;250 if (unionGroups(Ei->SrcBB, Ei->DestBB))251 Ei->InMST = true;252 }顺着已排序的边执行 unionGroups,合并成功才标记 InMST;无出口函数还会强制保留入口边用于插桩。
779// Given a CFG E to be instrumented, find which BB to place the instrumented780// code. The function will split the critical edge if necessary.781template <class Edge, class BBInfo>782BasicBlock *FuncPGOInstrumentation<Edge, BBInfo>::getInstrBB(Edge *E) {783 if (E->InMST || E->Removed)784 return nullptr;785786 BasicBlock *SrcBB = E->SrcBB;787 BasicBlock *DestBB = E->DestBB;788 // For a fake edge, instrument the real BB.789 if (SrcBB == nullptr)790 return DestBB;791 if (DestBB == nullptr)792 return SrcBB;793794 auto canInstrument = [](BasicBlock *BB) -> BasicBlock * {795 // There are basic blocks (such as catchswitch) cannot be instrumented.796 // If the returned first insertion point is the end of BB, skip this BB.797 if (BB->getFirstInsertionPt() == BB->end())798 return nullptr;799 return BB;实际 PGO 消费树标记的位置:树边和已删除的边不安排计数器。后续代码再选基本块或拆分关键边。
六段原文分别给出插桩目标、并查集两项优化、降序排序、选边与实际消费位置。权重估计、关键边拆分和 profile 使用阶段的计数恢复位于完整源文件中。
收益与取舍#
工程分析带来的收益
- 减少直接测量所需的边计数器,保留恢复完整边计数的依据
- 优先把较热的边留在树上,降低频繁执行路径的计数负担
付出的代价
- 排序 E 条边需要 O(E log E) 时间;路径压缩和按秩合并使并查集操作具有近常数的摊还成本,此外还有控制流改写和计数恢复成本
- 执行频率是权重估计,并非对每次实际运行开销的最优保证;关键边和异常处理约束还会影响选择
- 生成和使用 profile 的编译阶段必须对应同一控制流图,图变化需要校验处理
可比较的选择
每条边都插入计数器计数直接,但计数器数量与动态执行开销更大,关键边仍可能需要拆分。
任意生成树同样可以利用流量守恒减少计数器数量,但不优先避开高频边。
版本与适用范围#
对应 LLVM llvmorg-19.1.0 的 IR 级边计数 PGO 路径,未分析 basic-block coverage 的旁路模式。文件及函数名含 MinimumSpanningTree,但源码注释明确实际追求最大权重树;并查集合并按边端点连通性判断,不是有向最短路径。异常处理边、入口插桩和无出口循环另有约束,因此不能把此实现说成任意控制流下无附加条件的标准最小生成树。
参考来源#
源码、官方文档与原始论文llvm/lib/Transforms/Instrumentation/PGOInstrumentation.cpp · PGO 插桩依据及非树边选择
llvm/include/llvm/Transforms/Instrumentation/CFGMST.h · CFGMST · 权重排序与并查集合并
llvm/include/llvm/Transforms/Instrumentation/CFGMST.h · CFGMST · 最大权重选择及特殊边约束
相关实现与比较#
3 个相关案例scikit-learn 的单链聚类使用 Prim 派生的 MST-LINKAGE-CORE,维护每个未加入样本到当前集合的最小距离,随后用并查集生成聚类层次。
Nova 把必须满足的主机条件与偏好评分分开:过滤候选、归一化各项权重并排序,再尝试向 Placement 申领资源,失败则继续其他候选。
ONNX Runtime 记录一次执行中非字符串内部张量的分配与释放,用 best-fit 选择可用偏移,生成后续执行可以复用的内存布局。