BOLT 代码热力图实战指南:用 llvm-bolt-heatmap 可视化 perf 采样画像
BOLT(Binary Optimization and Layout Tool)提供了一类直观的剖析能力:基于 perf 采样画像生成代码热力图(Code Heatmap)。它以彩色 ASCII 网格形式渲染二进制中每个地址区间被采样的次数,可直接在终端中查看,也可以转换为 HTML 在浏览器中分享。读完后,你将掌握完整的画像采集到热力图渲染的操作流程,理解热力图每个字符、行列坐标与颜色档位的含义,并能结合 llvm-bolt-heatmap 的源码实现读懂 -block-size、-line-size 等参数背后的机制,用于对比 BOLT 优化前后二进制的代码布局。
工作流程:从 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)); - 显示为
o或O:被采样地址不属于任何 text section; - 加
-print-mappings可在图例中打印字母与 text section 的对应关系(CommandLineOpts.cpp 中该选项默认关闭)。
颜色与图例:图例按每个 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 中出现的4KB、16kb、1MiB写法表明后缀解析同时接受大小写。
-block-size 的解析逻辑(HeatmapBlockSpecParser)值得注意两点:
- 后缀换算:
k/K左移 10 位、m/M左移 20 位、g/G左移 30 位(iB、B等后缀均被正则^[kKmMgG]i?[bB]?$接受),因此4KB、16kb、1M等价于十进制字节数 4096、16384、1048576; - 倍数约束:每个"缩出"粒度必须是前一级的整数倍(
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 主流程的哪些组件:
- 强制设置
opts::HeatmapMode = opts::HM_Exclusive与opts::AggregateOnly = true(heatmap.cpp),即只运行聚合阶段、不做重写; - 通过
createBinary读取输入文件,仅支持 ELF 对象文件(ELFObjectFileBase),随后创建RewriteInstance; - 调用
RI.setProfile(Filename)装入画像(perf.data或预聚合画像),再RI.run()驱动 BOLT 的画像聚合与 section 重映射逻辑——这也是工具描述中"支持 BOLT-optimized binaries"的原因:它能解析 BOLT 重排后二进制里的新 section 布局; - 采样落入
Heatmap::registerAddress(Heatmap.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。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

