用最小费用流修复采样执行计数
LLVM 的 profi 将采样执行计数转成最小费用流问题,用 Moore–Bellman–Ford 的队列松弛变体寻找增广路,再还原基本块与分支的执行次数。
工程问题#
采样可能遗漏基本块,也可能使一个块的执行次数与流入、流出分支的计数不一致。编译器需要符合控制流约束的计数,用于后续优化,同时尽量减少对已采样数值的调整。
实际实现#
profi 为增加或减少采样计数设置费用,建立带容量的流网络。最小费用流求解器在残量网络中反复寻找增广路:距离变小时更新前驱,并把目标节点加入待处理队列。反向边的费用与正向边相反,允许撤回先前分配的流量,因此路径搜索必须处理负费用边。这里的最短路径用于求解计数修正问题,并不是寻找程序执行时经过的最短分支。
处理流程#
- 采样 profile 加载器在启用 profi 时收集基本块计数,调用推断器
- 把控制流与计数修正代价编码为带容量、费用及反向边的网络
- 通过队列松弛寻找最小费用增广路,沿路径或候选 DAG 推送流量
- 从流网络提取基本块和分支计数,随后进行孤立分量连接等后处理
源码片段#
固定版本 · 原文摘录924template <typename BT>925void SampleProfileLoaderBaseImpl<BT>::propagateWeights(FunctionT &F) {926 // Flow-based profile inference is only usable with BasicBlock instantiation927 // of SampleProfileLoaderBaseImpl.928 if (SampleProfileUseProfi) {929 // Prepare block sample counts for inference.930 BlockWeightMap SampleBlockWeights;931 for (const auto &BI : F) {932 ErrorOr<uint64_t> Weight = getBlockWeight(&BI);933 if (Weight)934 SampleBlockWeights[&BI] = Weight.get();935 }936 // Fill in BlockWeights and EdgeWeights using an inference algorithm.937 applyProfi(F, Successors, SampleBlockWeights, BlockWeights, EdgeWeights);加载器在启用 profi 时收集采样计数,并把基本块和边权交给推断器;这是算法进入编译优化流程的实际调用点。
1346 // Create and apply an inference network model1347 auto InferenceNetwork = MinCostMaxFlow(Params);1348 initializeNetwork(Params, InferenceNetwork, Func);1349 InferenceNetwork.run();13501351 // Extract flow values for every block and every edge1352 extractWeights(Params, InferenceNetwork, Func);13531354 // Post-processing adjustments to the flow1355 auto Adjuster = FlowAdjuster(Params, Func);1356 Adjuster.run();推断器建立最小费用流网络、运行求解、提取计数,然后调用后处理;最短路径是这条计数修正流程中的求解步骤。
137 Edge SrcEdge;138 SrcEdge.Dst = Dst;139 SrcEdge.Cost = Cost;140 SrcEdge.Capacity = Capacity;141 SrcEdge.Flow = 0;142 SrcEdge.RevEdgeIndex = Edges[Dst].size();143144 Edge DstEdge;145 DstEdge.Dst = Src;146 DstEdge.Cost = -Cost;147 DstEdge.Capacity = 0;148 DstEdge.Flow = 0;149 DstEdge.RevEdgeIndex = Edges[Src].size();150151 Edges[Src].push_back(SrcEdge);152 Edges[Dst].push_back(DstEdge);每条正向边都配有费用取反的反向边。增广后反向边可以获得残量容量,因此后续路径搜索不能假定所有边权非负。
270 for (uint64_t EdgeIdx = 0; EdgeIdx < Edges[Src].size(); EdgeIdx++) {271 auto &Edge = Edges[Src][EdgeIdx];272 if (Edge.Flow < Edge.Capacity) {273 uint64_t Dst = Edge.Dst;274 int64_t NewDistance = Nodes[Src].Distance + Edge.Cost;275 if (Nodes[Dst].Distance > NewDistance) {276 // Update the distance and the parent node/edge277 Nodes[Dst].Distance = NewDistance;278 Nodes[Dst].ParentNode = Src;279 Nodes[Dst].ParentEdgeIndex = EdgeIdx;280 // Add the node to the queue, if it is not there yet281 if (!Nodes[Dst].Taken) {282 Queue.push(Dst);283 Nodes[Dst].Taken = true;284 }285 }286 }287 }288 }289290 return Nodes[Target].Distance != INF;291 }只处理仍有残量容量的边;发现更小费用时更新距离和前驱,并将节点加入队列继续传播。该函数通过这种队列松弛执行 Moore–Bellman–Ford 变体。
四段原文依次给出工程入口、求解与后处理、反向边以及松弛操作。队列初始化和网络特定剪枝位于同一函数 233–268 行,增广迭代及流量更新在相邻函数中;摘录没有包含完整求解器。
收益与取舍#
工程分析带来的收益
- 以流量守恒约束统一修正基本块与分支计数,并用费用表达哪些调整应尽量避免
- 负费用反向边允许撤回先前的流量分配,支持最小费用流的迭代求解
付出的代价
- 需要额外构建残量网络并多次搜索增广路;原文件给出的最坏界为 O(FVE),F 为最大流值,V、E 为网络顶点和边数,而非原始代码行数
- 剪枝依赖该网络没有负权环等不变量;这个函数不是通用图的负权环检测接口
- 得到的是遵循模型与费用参数的估计计数,不能补回未采集的真实执行轨迹
可比较的选择
基于已知计数逐轮传播同一加载器保留了传播分支,可根据已知块和边推断未知计数;它不使用这里的最小费用流模型来统一选择修正代价。
Dijkstra 配合势函数重标权最小费用流也可先维护势函数,使可用残量边的重标权费用非负后采用 Dijkstra;不能把含负费用反向边的当前网络直接交给普通 Dijkstra。
版本与适用范围#
对应 LLVM llvmorg-19.1.0 的采样 profile 推断路径,需启用 SampleProfileUseProfi。算法属于用于最小费用流的 Moore–Bellman–Ford 变体,采用队列、网络特定剪枝和可选的 DAG 增广;不等同于教科书中固定 V−1 轮扫描所有边的实现。流求解后还有调整步骤,不能把整个 profi 的最终结果简单称作未改动的最小费用流解。
参考来源#
源码、官方文档与原始论文llvm/include/llvm/Transforms/Utils/SampleProfileLoaderBaseImpl.h · SampleProfileLoaderBaseImpl::propagateWeights
llvm/include/llvm/Transforms/Utils/SampleProfileInference.h · SampleProfileInference::apply
llvm/lib/Transforms/Utils/SampleProfileInference.cpp · MinCostMaxFlow
llvm/lib/Transforms/Utils/SampleProfileInference.cpp · MinCostMaxFlow::findAugmentingPath
llvm/lib/Transforms/Utils/SampleProfileInference.cpp · llvm::applyFlowInference
相关实现与比较#
4 个相关案例HNSW 从稀疏上层快速定位区域,再在底层扩大候选搜索。
借助计算依赖图与使用者计数,清理既不影响输出也无副作用的节点。
Linux IIO 的 kfifo 缓冲在采样进入内核与用户态读取之间暂存数据,按先进先出顺序交付采样记录。
Linux BPF 验证器的 check_cfg 使用显式指令栈执行非递归 DFS,检查跳转目标、可达性及控制流结构。