GlobalMerge

这算是个中型 Pass,代码量有 770 多行。因为大模型理解得比我深,所以这个例子主要使用模型的输入作为参考。

其作用是:将具有内部链接(internal linkage)的全局变量合并为一个大的全局结构,从而==优化内存访问并降低寄存器压力,尤其是在处理多个全局变量的场景下。==
它具体做了什么?

  1. 查找所有具有内部链接的全局变量(如 static int foo[N]; 之类的)。
  2. 将这些变量合并为一个结构体(struct),例如:
static struct {
    int foo[N];
    int bar[N];
    int baz[N];
} merged;
  1. 替换原来的变量访问,使所有原本独立变量的访问都通过结构体的偏移量访问。

📈 为什么这样做是有益的?

  • 在某些体系结构(如 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 会根据 全局变量的可合并性和使用共现性,构造局部结构体将其打包,用结构偏移访问代替多个全局地址访问,从而减少寄存器和内存负担。