后端Pass简介——GlobalMerge
GlobalMerge
这算是个中型 Pass,代码量有 770 多行。因为大模型理解得比我深,所以这个例子主要使用模型的输入作为参考。
其作用是:将具有内部链接(internal linkage)的全局变量合并为一个大的全局结构,从而==优化内存访问并降低寄存器压力,尤其是在处理多个全局变量的场景下。==
✅ 它具体做了什么?
- 查找所有具有内部链接的全局变量(如
static int foo[N];
之类的)。 - 将这些变量合并为一个结构体(struct),例如:
static struct {
int foo[N];
int bar[N];
int baz[N];
} merged;
- 替换原来的变量访问,使所有原本独立变量的访问都通过结构体的偏移量访问。
📈 为什么这样做是有益的?
- 在某些体系结构(如 ARM)上,每个全局变量的地址都需要通过寄存器持有,因此如果你有多个全局变量,就需要多个寄存器。
- 将这些变量合并到一个结构中后,只需要一个寄存器持有结构体的基地址,其他变量都可以通过偏移访问。
- 可以显著减少寄存器压力,提高代码执行效率,尤其是在循环中频繁使用多个全局变量时。
💡 一个例子说明优化前后区别(ARM 架构):
static int foo[N], bar[N], baz[N];
for (i = 0; i < N; ++i)
foo[i] = bar[i] * baz[i];
对应ARM指令:
ldr r1, [r5], #4 ; 加载 bar[i]
ldr r2, [r6], #4 ; 加载 baz[i]
mul r1, r2, r1 ; 相乘
str r1, [r0], #4 ; 存入 foo[i]
优化后代码(合并为结构体):
static struct {
int foo[N];
int bar[N];
int baz[N];
} merged;
for (i = 0; i < N; ++i)
merged.foo[i] = merged.bar[i] * merged.baz[i];
对应ARM指令:
ldr r0, [r5, #40] ; 加载 bar[i]
ldr r1, [r5, #80] ; 加载 baz[i]
mul r0, r1, r0
str r0, [r5], #4 ; 存入 foo[i]
仅需一个结构体基址寄存器(r5
),节省了其他寄存器。
⚠️ 但这个优化也有副作用:
- 调试器会发现原本的变量“消失了”,难以还原变量名与结构成员的关系;
- 链接器的优化机会减少(比如 Linker Optimized Hot/Cold Splitting, LOHs 等);
- 会引入偏移寻址(indexed addressing),在某些架构上可能性能不佳;
- 如果变量的使用分布很分散,反而可能增加寄存器压力。
这个 Pass 是可调参的:
// FIXME: This is only useful as a last-resort way to disable the pass.
// 是否启用全局变量合并优化(默认启用)。
static cl::opt<bool>
EnableGlobalMerge("enable-global-merge", cl::Hidden,
cl::desc("Enable the global merge pass"),
cl::init(true));
// 合并变量时允许的最大偏移量(0 表示不限制)
static cl::opt<unsigned>
GlobalMergeMaxOffset("global-merge-max-offset", cl::Hidden,
cl::desc("Set maximum offset for global merge pass"),
cl::init(0));
// 根据变量的使用关系分组合并,提高合并质量
static cl::opt<bool> GlobalMergeGroupByUse(
"global-merge-group-by-use", cl::Hidden,
cl::desc("Improve global merge pass to look at uses"), cl::init(true));
// 是否无视使用情况强制合并所有常量(const)全局变量
static cl::opt<bool> GlobalMergeAllConst(
"global-merge-all-const", cl::Hidden,
cl::desc("Merge all const globals without looking at uses"),
cl::init(false));
// 忽略仅单独使用的变量,不考虑其参与合并
static cl::opt<bool> GlobalMergeIgnoreSingleUse(
"global-merge-ignore-single-use", cl::Hidden,
cl::desc("Improve global merge pass to ignore globals only used alone"),
cl::init(true));
// 是否允许对常量变量启用全局合并
static cl::opt<bool>
EnableGlobalMergeOnConst("global-merge-on-const", cl::Hidden,
cl::desc("Enable global merge pass on constants"),
cl::init(false));
// FIXME: this could be a transitional option, and we probably need to remove
// it if only we are sure this optimization could always benefit all targets.
// 是否对具有外部链接(external linkage)的变量启用合并
static cl::opt<cl::boolOrDefault>
EnableGlobalMergeOnExternal("global-merge-on-external", cl::Hidden,
cl::desc("Enable global merge pass on external linkage"));
// 设置参与合并的最小变量大小(单位字节)
static cl::opt<unsigned>
GlobalMergeMinDataSize("global-merge-min-data-size",
cl::desc("The minimum size in bytes of each global "
"that should considered in merging."),
cl::init(0), cl::Hidden);
STATISTIC(NumMerged, "Number of globals merged");
源码的核心处理逻辑如下:
他的核心筛选逻辑是:
if (!(Opt.MergeExternal && GV.hasExternalLinkage()) &&
!GV.hasLocalLinkage())
continue;
它找的是:大小适中、非特殊用途、可以本地重定位的全局变量
🧠 2. 分组策略:如何决定哪些变量要一起合并?
分两种模式:
✅ 简单合并(直接全部合并)
如果禁用了 GlobalMergeGroupByUse
或启用了 GlobalMergeAllConst
:
BitVector AllGlobals(Globals.size(), true);
return doMerge(Globals, AllGlobals, M, isConst, AddrSpace);
💡 所有候选变量都直接丢到一起合并
✅ 智能合并(基于使用行为)
核心逻辑:分析变量在函数中的“共现关系”,即哪些变量在同一个函数里一起被使用。
DenseMap<Function *, size_t /*UsedGlobalSetIdx*/> GlobalUsesByFunction;
具体行为:
- 每个函数内分析哪些变量一起被用到;
- 构建多个
UsedGlobalSet
(每个是一组一起用的变量 + 出现次数); - 最后根据
(使用次数 × 集合大小)
作为“合并优先级”; - 忽略只在一个函数单独用的(
GlobalMergeIgnoreSingleUse
); - 从高优先级开始,逐一尝试合并。
📌 核心思想是:尽量合并经常被一起用的变量,避免引入无谓的偏移寻址
🏗️ 3. 执行合并:如何实现合并操作?
在 doMerge()
中:
- 构造一个
StructType
表示新的大变量 - 所有变量的值作为结构体的成员
Inits
- 加入对齐填充(padding),保持变量对齐
- 替换所有原变量的引用为 GEP(偏移访问结构体成员)
- 删除原始变量
- 创建
GlobalAlias
保持原始变量名
📦 合并后:所有变量通过同一个 base pointer + 偏移来访问
✅ 总结:核心逻辑一句话
GlobalMerge Pass 会根据 全局变量的可合并性和使用共现性,构造局部结构体将其打包,用结构偏移访问代替多个全局地址访问,从而减少寄存器和内存负担。
评论