BOLT Address Translation(BAT)详解:在已优化二进制上采集 Profile 并反译回输入二进制的原理与实践
【免费下载链接】llvm-projectThe LLVM Project is a collection of modular and reusable compiler and toolchain technologies.项目地址: https://gitcode.com/GitHub_Trending/ll/llvm-project
BOLT Address Translation(BAT)解决的是一个很实际的性能工程问题:如果只能在生产环境部署已经过 BOLT 优化的二进制,如何在其上采集采样 Profile,再把这些样本地址翻译回优化前的输入二进制,用于下一轮优化?读完本文,你将掌握 BAT 的端到端工作流(--enable-bat生成翻译表、perf2bolt反译 Profile)、.note.bolt_bat段的完整二进制编码格式(函数表、地址翻译表、次入口点表的 delta/LEB128 编码细节),以及BoltAddressTranslation类在 BOLT 源码中的构建、解析与查询链路。
1. 背景:为什么需要 BAT
BOLT 的常规 Profile 采集流程是:在未优化的输入二进制上插桩或用 perf 采样,得到基于输入二进制地址的 Profile,再交给 BOLT 做函数重排、基本块重排、冷热拆分等优化。但在真实部署场景中,往往只能部署已优化的二进制——也就是说采样只能发生在 BOLT 输出的二进制上。此时样本地址是"输出地址空间"中的地址,而 BOLT 又不支持直接对已优化二进制再做二次优化。
BAT 正是为此设计的:BOLT 在输出二进制中额外插入一个名为.note.bolt_bat的 note 段,其中保存地址翻译表和函数冷热拆分(split functions)的关联信息。有了这个段,就可以把采集自优化二进制的 Profile 映射回原始输入二进制,从而避免为了单纯采集数据而部署非 BOLT 版本。
这一设计目标在头文件注释中写得很明确(见 BoltAddressTranslation.h 的类注释):
We do not support reoptimizing a binary already processed by BOLT, but we do support collecting samples in a binary processed by BOLT. We then translate samples back to addresses from the input (original) binary, one that can be optimized. The goal is to avoid special deployments of non-bolted binaries just for the purposes of data collection.
从源码结构看,内存中的表示以函数为单位组织:每个函数有一张自己的std::multimap<uint32_t, uint32_t>翻译表(MapTy),键为输出地址空间中区域的起点偏移,值为输入地址空间中的偏移;翻译时先在表中找到小于等于样本地址的最大键,再据此换算出输入地址。此外,只有会改变控制流的指令(分支、调用)才会作为"分支源"条目出现在表中,而基本块起始位置被记录下来是因为它们可能是跳转目标(LBR trace 中的 To 地址),也用于重建函数内基本块布局,以便根据 LBR 轨迹恢复 fall-through(隐式顺序执行)边。
2. 使用方式:从生成 BAT 段到反译 Profile
BAT 的完整使用流程分两步:
第一步:启用 BAT 并运行 BOLT 优化。--enable-bat控制是否生成 BAT 段,将采样的 Profile 与(不含 BAT 的)输入二进制一起交给llvm-bolt,并在参数中加上--enable-bat。该选项定义于 CommandLineOpts.cpp,默认值为false,描述为 "write BOLT Address Translation tables":
cl::opt<bool> EnableBAT("enable-bat", cl::desc("write BOLT Address Translation tables"), cl::init(false), cl::ZeroOrMore, cl::cat(BoltCategory));第二步:在优化后的二进制上采集 Profile,并用perf2bolt反译。将采到的采样 Profile(如 perf 数据)连同包含 BAT 段的优化二进制一起传给perf2bolt。perf2bolt会读取.note.bolt_bat段,将其填充进BoltAddressTranslation对象,随后在样本处理过程中由DataAggregator查询该对象,把输出地址空间中的地址/偏移重建为输入二进制中的地址/偏移,最终产出一份针对原始输入二进制的 Profile。
在 DataAggregator.cpp 中可以观察到这一查询链路的具体调用点:BAT->translate(Func.getAddress(), ...)负责地址反译(普通样本与 LBR 样本各有一处)、BAT->fetchParentAddress(...)用于把拆分出的冷片段样本归并回其父热函数、BAT->getFallthroughsInTrace(...)用于从 LBR 轨迹推断被取到的 fall-through 边。仓库中的端到端测试 bolt-address-translation.test 演示了--reorder-blocks=normal --split-functions --enable-bat的组合用法,bolt-address-translation-yaml.test 则覆盖了--reorder-functions=cdsort --enable-bat --dyno-stats场景下的 Profile 输出;仓库还提供了 bat-dump 工具用于转储解析后的翻译表,方便人工检查。
需要注意的一个前提:llvm-bolt在写入 BAT 段时会记录每个基本块/指令的输入-输出地址映射(IO address map),BinaryFunction.cpp 中即有 "accurate IO address map later (used for BAT, ...)" 的相关逻辑;当二进制没有重定位(无 relocations)且函数体未被修改时,函数被视为"simple",无需翻译表(见 BoltAddressTranslation.cpp 中write的跳过条件)。
3. BAT 段的内容组织
.note.bolt_bat段整体组织如下:
Hot functions table (热函数表) └─ Address translation tables (各函数的地址翻译表) Cold functions table (冷函数表)即:先是一张热函数表(每个函数附带自己的地址翻译表),后跟一张冷函数表。热/冷两个表共享同一套编码,差异在下方各小节单独标出。
从实现角度看,BoltAddressTranslation类由 BOLT linker 提供的地址翻译信息构建(write遍历BinaryContext中的每个BinaryFunction,通过writeEntriesForBB为每个基本块及其内部的控制流指令生成条目),然后以 ELF note 段的形式编码进输出二进制。反过来,perf2bolt处理 BAT 启用的二进制时,parse方法从 note 段重建内存表示,供DataAggregator在样本处理时查询。
4. 编码格式详解
编码的权威定义在 BoltAddressTranslation.h 与 BoltAddressTranslation.cpp 中。整体布局为:
Hot functions table Cold functions table Functions table: |------------------| | Function entry | | | | Address | | translation | | table | | | | Secondary entry | | points | |------------------|4.1 函数表(Functions table)
热、冷函数表共享下述编码,差异在各条目最后一列标出。表头只有一个字段:
| 条目 | 编码 | 说明 |
|---|---|---|
NumFuncs | ULEB128 | 该函数表中的函数数量 |
表头之后是NumFuncs个函数条目。两个关键的压缩策略:
- 输出二进制地址采用 delta 编码:只存储与上一个输出地址的差值,地址隐含从 0 开始。输出地址在函数起始地址与函数内部偏移之间、以及热/冷片段之间都是"连续"的(continuous),这样差值更均匀,能更好地省空间。
- 热函数索引(HotIndex)同样 delta 编码,隐含从 0 开始。
各条目定义如下:
| 条目 | 编码 | 说明 | 热/冷 |
|---|---|---|---|
Address | Continuous, Delta, ULEB128 | 函数在输出二进制中的地址 | 两者 |
HotIndex | Delta, ULEB128 | 对应热函数在热函数表中的索引 | 冷 |
FuncHash | 8b | 输入函数的哈希 | 热 |
NumBlocks | ULEB128 | 原始函数中基本块的数量 | 热 |
NumSecEntryPoints | ULEB128 | 原始函数中次入口点的数量 | 热 |
ColdInputSkew | ULEB128 | 需要应用到所有输入偏移上的偏移量(skew) | 冷 |
NumEntries | ULEB128 | 该函数地址翻译表中的条目数 | 两者 |
EqualElems | ULEB128 | 函数开头输入偏移等于输出偏移的条目数 | 两者 |
BranchEntries | Bitmask,alignTo(EqualElems, 8)个 bit | 当EqualElems非零时,标记哪些条目带BRANCHENTRY位的位图 | 两者 |
函数头部之后依次是含NumEntries个条目的地址翻译表,以及(仅热函数)含NumSecEntryPoints个条目的次入口点表。
其中HotIndex的作用可以从 BoltAddressTranslation.h 的成员ColdPartSource("Links outlined cold blocks to their original function")印证:冷函数表条目通过HotIndex指向其父热函数,perf2bolt侧的fetchParentAddress查询正是利用这份关联把冷片段上的样本归并到父函数;FuncHash与NumBlocks则对应内存结构FuncHashesTy/BBHashMapTy,用于在 BAT 模式下校验函数身份并重建输入二进制的基本块布局(DataAggregator写 YAML Profile 时会调用getBFHash、getNumBasicBlocks、getBBHashMap填充这些元数据)。
4.2 地址翻译表(Address translation table)
delta 编码的含义是:只编码与上一个对应条目的差值,输入偏移隐含从 0 开始。
| 条目 | 编码 | 说明 | 分支/BB |
|---|---|---|---|
OutputOffset | Continuous, Delta, ULEB128 | 函数在输出二进制中的偏移 | 两者 |
InputOffset | Optional, Delta, SLEB128 | 输入二进制中函数的偏移,最低位(LSB)为BRANCHENTRY位 | 两者 |
BBHash | Optional, 8b | 输入二进制中基本块的哈希 | BB |
BBIdx | Optional, Delta, ULEB128 | 输入二进制中基本块的索引 | BB |
格式上的三个要点:
- 等偏移省略:表会省略函数开头
EqualElems个"输入偏移等于输出偏移"的条目——因为这些位置本身就不需要翻译。 BRANCHENTRY位的语义:该位标记该偏移对是否为控制流源(分支或调用指令);未置位则表示控制流目标(基本块偏移)。这与 BoltAddressTranslation.h 中const static uint32_t BRANCHENTRY = 0x1;一致,也解释了为何InputOffset用 SLEB128 且值域左移一位。- 等偏移条目的输入地址省略:当输入、输出函数中偏移相等时,
InputOffset字段整体省略,此时BRANCHENTRY位单独编码在BranchEntries位向量中(即上表 4.1 中的位图字段)。
在 BoltAddressTranslation.cpp 的writeEntriesForBB中可以看到条目生成的实际规则:每个输出基本块至少产生一条 BB 起始条目(Map.emplace(BBOutputOffset, BBInputOffset << 1),左移一位即为BRANCHENTRY位留空);随后遍历块内记录的位置符号,为每个控制流指令追加一条(InputOffset << 1) | BRANCHENTRY条目。对于被优化删除的基本块,编码上输出其OutputOffset等于函数大小——这类条目不影响地址翻译,只参与输入基本块映射(DataAggregator中isInputBlock的判断正依赖完整的输入块映射)。
值得注意的工程细节:当多个输入块映射到同一输出地址时(例如只含 NOP 或跳转的块被删除,与后继块共享地址),编码策略是"后写者胜出",优先保留后继块(successor)的映射,保证样本不会错误落到已删除块上。
4.3 次入口点表(Secondary Entry Points table)
该表仅对热片段输出,包含NumSecEntryPoints个偏移,表示次入口点(函数体内允许从外部直接跳转进入的位置,如跳表、尾调用目标等),采用 delta 编码、隐含从 0 开始:
| 条目 | 编码 | 说明 |
|---|---|---|
SecEntryPoint | Delta, ULEB128 | 次入口点偏移 |
write流程通过Function.forEachEntryPoint收集这些偏移并缓存到SecondaryEntryPointsMap;查询侧的translateSymbol/getSecondaryEntryPointId则用于把优化二进制的符号引用翻译回父函数与对应入口点(YAMLProfileWriter.cpp 在写被调方地址时即调用translateSymbol)。
5. 小结:BAT 在 BOLT 工作流中的位置
把前述内容串起来,BAT 支撑的完整闭环是:
llvm-bolt --enable-bat ...优化输入二进制,输出二进制中带有.note.bolt_bat段(热函数表 + 翻译表 + 次入口点表;冷函数表带HotIndex关联);- 该优化二进制上线运行,perf 采样得到基于输出地址的 Profile;
perf2bolt将采样数据与优化二进制一起处理:BoltAddressTranslation::parse从 note 段重建翻译表,DataAggregator在处理每个样本时调用translate/fetchParentAddress/getFallthroughsInTrace把地址反译、冷片段归并、fall-through 边补全,产出输入二进制地址空间的 Profile;- 该 Profile 回到步骤 1,驱动下一轮 BOLT 优化。
对使用者而言需要记住的约束:BAT 面向"采集"而非"二次优化"——已 BOLT 过的二进制本身不能作为 BOLT 的输入再优化;翻译表只覆盖会出现在 LBR 中的控制流点与基本块位置,普通指令不占条目,这正是编码能够保持紧凑的原因。
【免费下载链接】llvm-projectThe LLVM Project is a collection of modular and reusable compiler and toolchain technologies.项目地址: https://gitcode.com/GitHub_Trending/ll/llvm-project
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考