PGO?Profile-Guided Optimization (PGO) 是一种 “以真实运行数据为依据” 的编译优化方式: 先用插桩或采样收集程序在代表性输入下的执行画像(热点路径、分支概率、调用频次等); 再用该画像指导编译器做更贴近实际负载的优化决策(代码布局、内联、分支预测、循环向量化与调度、寄存器分配等)。
PGO 原理PGO 是基于当前运行的程序生成的 Profile,基于 Profile 再通过编译器调整代码,原理如下:
比如有一段如下 C++ 代码,对其进行 PGO 优化:
bool some_top_secret_checker(int var)
{
if (var == 42) return true;
if (var == 322) return true;
if (var == 1337) return true;
return false;
}
...
// 多处高频执行如下代码:
bool ret = some_top_secret_checker(322);
...
(1)直接编译
如上 c++ 编译以后的汇编代码如下:
mov al, 1 ; 将立即数1存入AL寄存器(EAX的低8位),设置默认返回值为1
cmp edi, 42 ; 比较EDI寄存器的值与42
je .LBB0_4 ; 如果相等(ZF=1),跳转到标签.LBB0_4
cmp edi, 1337 ; 比较EDI寄存器的值与1337
je .LBB0_4 ; 如果相等(ZF=1),跳转到标签.LBB0_4
cmp edi, 322 ; 比较EDI寄存器的值与322
je .LBB0_4 ; 如果相等(ZF=1),跳转到标签.LBB0_4
xor eax, eax ; 将EAX寄存器清零(异或自身=0),设置返回值为0
.LBB0_4: ; 函数返回标签
ret ; 返回调用者,返回值在EAX中
(2)使用 PGO 编译
mov al, 1 ; 将立即数1存入AL寄存器(EAX的低8位),设置默认返回值为1
cmp edi, 322 ; 比较EDI寄存器的值与322
jne .LBB0_1 ; 如果不相等(ZF=0),跳转到标签.LBB0_1继续检查其他值
; 如果edi == 322,则直接执行下面的返回(返回值为1)
.LBB0_4: ; 函数返回标签
ret ; 返回调用者,返回值在EAX中
.LBB0_1: ; 继续检查其他值的标签
cmp edi, 42 ; 比较EDI寄存器的值与42
je .LBB0_4 ; 如果相等(ZF=1),跳转到返回标签(返回值为1)
cmp edi, 1337 ; 比较EDI寄存器的值与1337
je .LBB0_4 ; 如果相等(ZF=1),跳转到返回标签(返回值为1)
xor eax, eax ; 将EAX寄存器清零(异或自身=0),设置返回值为0
jmp .LBB0_4 ; 无条件跳转到返回标签
发现有什么不一样么?
if (var == 322) return true; 这段被高频执行代码被优化到前面了;
cmp edi, 322 这段逻辑被提前到代码的最前面的位置。
(3)值得注意
在 PGO 之后,ret 返回语句不再放在函数末尾,而是移到了函数开头。
这样做是为了提高 CPU 指令缓存的命中率,因为分支之后 322 很有可能直接返回,所以将接下来很有可能执行的代码放到一起能提升性能。
C++ 代码 PGO为了验证性能,实现如下代码:
...
bool some_top_secret_checker(int var) {
if (var == 42 && calculateSum(10000) > 100) {
returntrue;
}
if (var == 322) {
returntrue;
}
if (var == 1337 && calculateSum(10000) > 1000) {
returntrue;
}
returnfalse;
}
...
文件名为 main.cpp,按照如下步骤进行编译:
# 1) 普通构建
clang++ -std=c++17 -O3 -march=native -o main main.cpp
# 2) 插桩构建
clang++ -std=c++17 -O3 -march=native -fprofile-instr-generate -o main_pgo_gen main.cpp
# 3) 运行收集 raw profile(可用通配符生成多文件)
LLVM_PROFILE_FILE="default_%p.profraw" ./main_pgo_gen >/dev/null
# 4) 合并画像
llvm-profdata merge default_*.profraw -o default.profdata
# 5) 应用构建(使用画像)
clang++ -std=c++17 -O3 -march=native -fprofile-instr-use=default.profdata -o main_pgo main.cpp
# 6) 性能对比
time -p ./main >/dev/null
time -p ./main_pgo >/dev/null
# 生成阶段:带 profile 插桩
g++ -std=c++17 -O3 -march=native -fprofile-generate -o main_gen main.cpp
./main_gen >/dev/null # 运行后生成 *.gcda 数据
# 应用阶段:使用 profile 做优化
g++ -std=c++17 -O3 -march=native -fprofile-use -fprofile-correction -o main_pgo main.cpp

上面是没有 PGO 优化的执行时间,下面是 PGO 优化的执行时间,对比性能提升 10~20% 之间。
PGO 实现类型PGO 实现类型主要有两种:
PGO profiles,重新编译代码的时候,会将这些收集的信息集合,重新调整代码执行的顺序;PGO 通过硬件性能计数器(如 Linux perf)收集运行数据,再转换为编译器可读的配置文件,主流工具包括 Google AutoFDO(支持 GCC/LLVM)和 LLVM 内置的 llvm-profgen;(1)https://github.com/zamazan4ik/awesome-pgo (2)https://en.wikipedia.org/wiki/Optimizing_compiler