首页
/ BOLT 代码热力图实战指南:用 llvm-bolt-heatmap 可视化 perf 采样画像

BOLT 代码热力图实战指南:用 llvm-bolt-heatmap 可视化 perf 采样画像

2026-09-05 22:31:58作者:侯霆垣

BOLT(Binary Optimization and Layout Tool)提供了一类直观的剖析能力:基于 perf 采样画像生成代码热力图(Code Heatmap)。它以彩色 ASCII 网格形式渲染二进制中每个地址区间被采样的次数,可直接在终端中查看,也可以转换为 HTML 在浏览器中分享。读完后,你将掌握完整的画像采集到热力图渲染的操作流程,理解热力图每个字符、行列坐标与颜色档位的含义,并能结合 llvm-bolt-heatmap 的源码实现读懂 -block-size-line-size 等参数背后的机制,用于对比 BOLT 优化前后二进制的代码布局。

BOLT 代码热力图在终端中的彩色 ASCII 渲染效果

工作流程:从 perf 采样到热力图

热力图的输入是两类:一个可执行文件(支持已被 BOLT 优化过的或未优化的二进制),以及一份采样画像。官方文档(Heatmaps.md)给出的标准流程是两步:

第一步:用 perf 采集采样画像

先让目标程序在 perf 下运行:

$ perf record -e cycles:u -j any,u -- <executable with args>

如果需要监控已经存在的进程,则使用:

$ perf record -e cycles:u -j any,u [-p PID|-a] -- sleep <interval>

其中 -j any,u(等价于 -b)开启**分支栈(brstack)**采集。文档明确建议运行 brstack 模式,因为它能提供比基本事件(basic events)更细的覆盖率;若只想用基本事件,可以在生成热力图时加 llvm-bolt-heatmap -ba(basic events)选项,但这类热力图没有 brstack 的覆盖精度,通常只适合在更大代码块粒度上定位热点。

第二步:运行 llvm-bolt-heatmap

perf.data 生成后,直接对可执行文件生成热力图:

$ llvm-bolt-heatmap -p perf.data <executable>

默认情况下热力图输出到 stdout,可以用 -o <heatmapfile> 选项改为写入文件。从工具自身的帮助文本(heatmap.cpp)可以看到,它同时会产出三种信息:多粒度的热力图(由 -block-size 控制)、每个 section 的热点度(samples% 与 utilization%)、以及累积分布(对应给定采样百分位的工作集大小)。

工具还接受预聚合画像(pre-aggregated profile),而不是只限 perf.data——测试用例 heatmap-preagg.test 演示了完整的用法:用 yaml2obj 生成测试二进制,然后:

$ llvm-bolt-heatmap %t.exe -o %t --pa -p %p/Inputs/blarge_new.preagg.txt \
    --block-size=64,128,1K --line-size 64

这里 --pa 表示使用预聚合画像文件,--block-size=64,128,1K 一次性输出 64B、128B、1K 三种粒度的热力图,--line-size 64 控制每行字符数。该测试还验证了对 BOLT 过的二进制(带 --enable-bat 重排)生成热力图,并检查 4K/16K/1M 各档热力图的最热区间一致。

若习惯在浏览器中查看(或需要分享给他人),可以用 ASCII 转 HTML 的工具转换:

$ aha -b -f <heatmapfile> > <heatmapfile>.html

读懂热力图:字符、坐标与颜色

热力图本质上是一个渲染成网格的直方图(histogram)。从 Heatmap.cpp 的实现看,每个字符对应一段连续代码(默认 64 字节)内累积的采样数,粒度由 -block-size 控制——例如设为 4096 即可按 4K 页观察代码使用情况。

字符含义

  • 显示为圆点 .:该地址区间没有找到任何采样;
  • 显示为字母:该二进制 text section 中捕获到了采样。每个 text section 按序分配一个字母(见 Heatmap.cpp 中的 'a' + ((Section - TextSections.begin()) % 26));
  • 显示为 oO:被采样地址不属于任何 text section;
  • -print-mappings 可在图例中打印字母与 text section 的对应关系(CommandLineOpts.cpp 中该选项默认关闭)。

热力图头部:X 轴十六进制刻度与图例说明

颜色与图例:图例按每个 block 的采样数划分区间并逐区间赋色。源码 Heatmap.cpp 中定义了 6 个颜色档位(白、白、青、绿、黄、红),前两个档位用小写字母区分,之后用大写字母表示。分档阈值不是均匀划分的,而是对最大值取根号幂次(std::pow(MaxValue, (I+1)/NumRanges)),这样在采样数跨度极大时,低值区与高值区都能保留可读的区分度。

Y 轴(行):每行行首是该行的真实地址;连续行之间按相同步长推进,一行覆盖的二进制大小由 block 大小与行长度共同决定;行与行之间出现大段空白时,源码会插入一个空行做视觉分隔(Heatmap.cpp 中,当空行数超过 32 时直接只打印一个空行)。

X 轴(列):横向打印的十六进制数只能帮助估算采样在行内的位置——它们是相对 bucket 大小与行大小的偏移,无法直接拼出完整地址。文档特别指出:例如图中高亮的 0x100 列并不是相对每行行首地址的偏移,而是指向该行中部(示例使用默认 bucket 大小与 128 的行长度生成)。

关键命令行参数与源码实现

文档列出的核心选项(-line-size-block-size-max-address-print-mappings)全部在 CommandLineOpts.cpp 中定义,默认值以当前仓库源码为准:

选项 说明 默认值(当前源码)
-line-size=<uint> 每行条目数;屏幕横向放不下时可用更小值(如 128) 256(BucketsPerLine
-block-size=<initial>[,<zoom-out>,...] 热力图 bucket 大小,可跟多级"缩出"粒度生成粗粒度热力图;支持 [kKmMgG][i][B] 后缀 64, 4K, 16K, 64K, 2M(HeatmapBlock,即缓存行与 x86-64/AArch64 实际使用的页大小、以及 4K 基础页之上的 2M 大页)
-max-address=<uint> 热力图认为有效的最大地址 0xffffffff(4GB)
-min-address=<uint> 热力图认为有效的最小地址 0
-print-mappings 在图例中打印字母/block 与 text section 的映射 false
-heatmap=<file> 热力图输出文件 stdout
-heatmap-cdf-pct=<n> 报告工作集时使用的采样 CDF 截止百分位(以百万分之一为单位) 990000(即 99%)

注意:原文档中 -block-size 的默认值写作 "64B, 4K, 256K",而当前仓库源码中默认已扩展为 64B、4K、16K、64K、2M 共五档,且测试 heatmap-preagg.test 中出现的 4KB16kb1MiB 写法表明后缀解析同时接受大小写。

-block-size 的解析逻辑(HeatmapBlockSpecParser)值得注意两点:

  1. 后缀换算k/K 左移 10 位、m/M 左移 20 位、g/G 左移 30 位(iBB 等后缀均被正则 ^[kKmMgG]i?[bB]?$ 接受),因此 4KB16kb1M 等价于十进制字节数 4096、16384、1048576;
  2. 倍数约束:每个"缩出"粒度必须是前一级的整数倍(SizeVal % PreviousSize == 0),否则报错 "must be a multiple of previous value"。

多粒度输出通过 Heatmap::resizeBucket 实现(Heatmap.cpp):从最小粒度开始,把桶计数按比例归并到更大的桶(NewMap[Bucket * BucketSize / NewSize] += Count),因此一次运行即可同时得到 64B 精细视图和 2M 页级宏观视图,而不需重复采样。

工具内部:从画像到网格的完整链路

理解 heatmap.cpp 的 main 函数,可以看清 llvm-bolt-heatmap 复用了 BOLT 主流程的哪些组件:

  1. 强制设置 opts::HeatmapMode = opts::HM_Exclusiveopts::AggregateOnly = trueheatmap.cpp),即只运行聚合阶段、不做重写;
  2. 通过 createBinary 读取输入文件,仅支持 ELF 对象文件(ELFObjectFileBase),随后创建 RewriteInstance
  3. 调用 RI.setProfile(Filename) 装入画像(perf.data 或预聚合画像),再 RI.run() 驱动 BOLT 的画像聚合与 section 重映射逻辑——这也是工具描述中"支持 BOLT-optimized binaries"的原因:它能解析 BOLT 重排后二进制里的新 section 布局;
  4. 采样落入 Heatmap::registerAddressHeatmap.h):先按 -min-address/-max-address 过滤,再按 Map[Address / BucketSize] += Count 累加计数;对区间的采样则走 registerAddressRange,超过 64KB 的异常区间会被记为无效并跳过。

聚合完成后,print() 负责渲染,printSectionHotness() 输出每个 section 的热点度 CSV(包含 Percentage Hotness、Utilization Pct 与 Partition Score,测试用例中通过 -section-hotness.csv 文件检查),printCDF() 则输出"Bucket counts, Size (KB), CDF (%)"的累积分布表,并在 stdout 打印形如 HEATMAP: working set @ bucket size ... p99/total: N/M 的工作集摘要(见 Heatmap.cpp)。

典型用法与适用场景

综合文档与测试用例,几个可直接复用的组合:

  • 优化前后布局对比:对未优化与 BOLT 优化后的同一二进制分别生成热力图(llvm-bolt-heatmap -p perf.data before... -p perf.data after),用不同粒度的视图对比热代码是否更集中;测试 heatmap-preagg.test 正是对 llvm-bolt --reorder-blocks=ext-tsp --split-functions --reorder-functions=cdsort --enable-bat 重排后的二进制生成热力图并校验热区布局;
  • 粗粒度定位热点区--block-size=4K 或更大的页级视图,用于快速回答"哪些内存区间贡献了绝大部分采样";
  • 工作集分析-heatmap-cdf-pct 控制的工作集报告配合 CDF 输出,可评估热代码的紧凑程度;
  • 终端放不下的宽图:用 -line-size 128(或更小)压缩每行字符数,配合 -o file 落盘后再用 aha 转 HTML。

需要留意的前提:该流程依赖 Linux 下的 perf(相关测试标注了 REQUIRES: system-linux),输入必须是 ELF 可执行文件;-ba 基本事件模式下的热力图精度有限;X 轴十六进制刻度只是行内估算,不可当作完整地址使用。完整的文档与示例图见 bolt/docs/Heatmaps.md

登录后查看全文
热门项目推荐
相关项目推荐