在大型的 C++ 应用和动态库中,存在着很多函数体一样的函数,大概有 10% 左右的可以做合并操作,这种技术称之为 ICF (Identical Code Folding)。在 C++ 当中,主要是因为对模版的使用,会导致相同实现体的函数出现(比如以类型的指针来进行实例化,而指针类型大小是一致的)。
在链接时,ICF 会检测出带有相同实现体的函数,然后合并为一个单独的副本。显而易见,这样操作的好处就是减小了二进制体积(实验 Google 体积下降 64MB,6% 左右,性能无影响)。
但 ICF 不一定安全,因为它会改变需要函数有唯一地址的运行时行为。ICF 可以安全地用在那些不需要进行地址比较的需合并的函数身上。还有,对二进制进行调试(debugging)和探测(profiling)时会迷惑,因为合并的函数的 PC 地址指向不明,所以也有新的 DWARF 格式对此来进行兼容。
ICF 也扩展到 read-only string section、unwind info 等场景。
2、ICF 算法
ICF 要如何检测到相同实现体的函数?为了使链接器可以实现这一手段,编译器需要把每一个函数放置在一个单独的 section,这个功能 GCC 编译器已经提供了支持 -ffunctions-sections,当然,实际上拿到 text 段中每一个函数的位置再进行计算也是可行的。
从链接器的角度而言,在代码段,一个函数的实现体包含有代码文本以及重定位信息。那么,只要代码文本二进制完全相同、而且它们的重定位指向相同的 section,就可以认为是相同的。这里的重定位信息指向相同的 section、或者指向被认为相同的函数,都被认为是相同的。
// zip 和 zap 是相同的,所以 foo 和 bar 也被认为是相同的
int foo () { return zip (); }
int bar () { return zap (); }
int zip () { return 0; }
int zap () { return 0; }
/***
What the linker sees :
Disassembly of section .text._Z3foov:
0000000000000000 <_Z3foov>:
55 push %rbp
48 89 e5 mov %rsp,%rbp
e8 00 00 00 00 callq 9 R_X86_64_PC32 relocation to zip
c9 leaveq
c3 retq
// 实际上 funcA 和 funcB 是相同的,但消极方式无法识别出来
int funcA (int a) {
if (a == 1) return 1;
return 1 + funcB(a 1);
}
int funcB (int a) {
if (a == 1) return 1;
return 1 + funcA(a 1);
}