关于PGO与LTO优化及其大致实践
简单介绍PGO与LTO这俩性能优化方法的基本概念
关于PGO与LTO优化及其大致实践
一、PGO基本概念
PGO即Profile-Guided Optimization是一种思想,不局限于语言(就算是Unity也有CSharp->IL->IL2CPP的路径,也可在CPP底层做PGO优化),核心在于减少间接调用(Indirect Call)带来的性能开销
1.1 优化点位
1.1.1 分支语句
- 对于如下函数,其编译后的汇编指令序列中
- 条件判断
if (Condition())部分对应的指令序列的相邻的下一行紧跟的是LogicA();指令序列,然后才是LogicB();指令序列 - 如果条件
Condition()命中,则此时会直接向下执行相邻的LogicA();指令序列,否则未命中则会执行跳转指令到LogicB();位置 - 由于指令集被加载到Cache中由CPU执行,当
Condition() == false时,跳转执行LogicB();对应的那几行指令更容易触发Cache的Miss,继而使得CPU从主存中访存,导致额外开销
- 条件判断
- 故若能知道
Condition()为true或false的概率谁更高,就可在生成汇编指令序列时将更高频的分支逻辑放到if (Condition())部分对应的指令序列的相邻后方即可,以增大Cache命中率以提升性能
1
2
3
4
5
6
7
8
9
10
11
void Func()
{
if (Condition())
{
LogicA();
}
else
{
LogicB();
}
}
1.1.2 内联函数
- 在前文基础上,新增
LogicC();调用,其对应指令序列紧随前面条件判断那块逻辑之后- 若
LogicA();或LogicB();中藏了行数爆多的inline函数,且编译器选择展开内敛函数,则整个Func()函数的汇编指令序列从头部到尾部LogicC();的长度就会十分庞大 - 如果这个巨大的内联函数所在的分支经常被执行,那就无伤大雅,但是若其所在分支极少被执行,则每次执行
Func()函数时都需要跳过一大串指令后再执行LogicC();部分指令,缓存不友好
- 若
- 所以对于庞大内联函数位于低频分支的情况,可综合考虑是否需建议编译器不对该内联函数进行展开(而是保留为一般的函数Call),目的同样是想要增大缓存命中率以提升性能
1
2
3
4
5
6
7
8
9
10
11
12
13
void Func()
{
if (Condition())
{
LogicA();
}
else
{
LogicB();
}
LogicC();
}
1.1.3 去虚拟化
- C++的多态基于虚函数
- 基础Devirtualization:子类继承自父类,且该子类没有下一代子类,当通过子类指针调用子类实现的某虚函数时,仍然为间接调用(因为编译器并不知道你的这个子类是否还有子类,也就无法确定该虚函数的具体版本,还需查表确定),但若将该虚函数修饰为
final,通过子类指针调用它就会变为直接调用 - 推导Devirtualization:通过父类指针调用虚函数时,需动态找到对应版本的虚函数表上的实现,此处判断当前父类指针指向的对象到底是父类还是某代子类的过程,同样会存在和前文条件判断一样的优化点,即我们可以统计该父类指针是父类还是子类的频率,而在选择虚函数的版本时选择更为紧凑的汇编指令排布方式
- 基础Devirtualization:子类继承自父类,且该子类没有下一代子类,当通过子类指针调用子类实现的某虚函数时,仍然为间接调用(因为编译器并不知道你的这个子类是否还有子类,也就无法确定该虚函数的具体版本,还需查表确定),但若将该虚函数修饰为
1.2 应用方案
- 前文的优化思路都建立在知道分支命中概率的基础上,这些参考数据必须在实际运行程序时才能够获得,故PGO落地的流程大致为
- 第一步:首先在编译源代码时选择PGO支持,得到可执行文件
- 第二步:然后运行可执行文件进行数据采集,输出Profile文件
- 第三步:最后根据Profile文件进行调整,关闭PGO支持重新编译,获得优化后的最终可执行文件
- 其中Profile文件数据的准确性决定最终优化效果好坏,故在第二步采集数据时,测试行为需尽可能接近真实使用行为
二、LTO基本概念
- LTO即Link-Time Optimization,其缺点使其只适合Release模式而不适合开发时(当然也存在thinLTO等从一定程度上缓解了此缺点的方案)
- 问题
- 传统的C++程序在编译时,编译器逐个编译各单元,其视野和编译器层面的优化只能局限在单个编译单元内,无法从全局的角度进行更精准的优化
- 方案
- LTO将每个编译单元生成的机器码(
.o或.obj文件)替换为中间表示(IR, Intermediate Representation)文件 - 然后进入Linking阶段(其链接对象从机器码变为IR文件),链接器载入全部IR到内存整合为全局视图,此时再执行传统时在编译阶段的那些优化
- 链接和优化完成后,再生成机器码,并完成传统的一般链接行为
- LTO将每个编译单元生成的机器码(
- 缺点
- 内存占用大
- 编译时间长
- 问题
- LTO与PGO结合可使得PGO的分支预测结果更为准确,例如对于使用同一个判断函数的位于不同编译单元内的分支,LTO使得其PGO分支预测从每个编译单元内单独计数统计,变为从全局进行计数统计
本文由作者按照 CC BY-NC-SA 4.0 进行授权