技术总结|十分钟了解性能优化PGO

什么是 PGO

Profile-Guided Optimization (PGO) 是一种 "以真实运行数据为依据" 的编译优化方式:

先用插桩或采样收集程序在代表性输入下的执行画像(热点路径、分支概率、调用频次等);

再用该画像指导编译器做更贴近实际负载的优化决策(代码布局、内联、分支预测、循环向量化与调度、寄存器分配等)。

PGO 原理

PGO 是基于当前运行的程序生成的 Profile,基于 Profile 再通过编译器调整代码,原理如下:

  • 分析阶段:程序首先在启用分析工具的情况下进行编译和执行,在此阶段,编译器会收集不同代码路径、函数调用和其他程序行为的频率和分布数据;
  • 分析数据:分析阶段会生成一个分析数据文件,其中包含有关代码热点(经常执行的部分)的信息以及有关程序运行时行为的其他相关详细信息;
  • 重新编译:编译器随后会使用此配置文件数据来指导优化过程,它可以做出明智的决策,确定哪些代码路径最为关键,以及哪些优化可能产生最大的影响;
  • 优化:基于分析信息,编译器可以应用各种优化措施,例如内联函数、重新排序代码以改进分支预测以及优化循环结构。这些优化旨在提高程序的整体性能;
  • 重建:重新编译应用了优化的代码,从而生成一个新的可执行文件,该文件应根据从分析数据中获得的见解更高效地运行;

分析代码

比如有一段如下 C++ 代码,对其进行 PGO 优化:

csharp 复制代码
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++ 编译以后的汇编代码如下:

ini 复制代码
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 编译

ini 复制代码
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

为了验证性能,实现如下代码:

ini 复制代码
...
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,按照如下步骤进行编译:

Mac/Clang编译

ini 复制代码
# 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

Linux/GCC 编译

ini 复制代码
# 生成阶段:带 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 实现类型主要有两种:

  • 基于插桩(Instrumentation-based):
    • 原理:插桩实现即在编译或者运行时收集统计信息,比如执行频率,哪些会执行,哪些不执行,这些文件被保存为 PGO profiles,重新编译代码的时候,会将这些收集的信息集合,重新调整代码执行的顺序;
    • 问题:插桩会导致插装的运行时的程序性能下降,而且二进制的编译文件体积会增加;
  • 基于采样(Sampling-based):
    • 原理:为降低插桩的开销,采样 PGO 通过硬件性能计数器(如 Linux perf)收集运行数据,再转换为编译器可读的配置文件,主流工具包括 Google AutoFDO(支持 GCC/LLVM)和 LLVM 内置的 llvm-profgen;
    • 问题:由于依赖硬件性能计数器等工具,所以需要依赖工具链建设;

参考

(1)github.com/zamazan4ik/...

(2)en.wikipedia.org/wiki/Optimi...

相关推荐
yinke小琪5 小时前
从秒杀系统崩溃到支撑千万流量:我的Redis分布式锁踩坑实录
java·redis·后端
SXJR5 小时前
Spring前置准备(八)——ConfigurableApplicationContext和DefaultListableBeanFactory的区别
java·后端·spring
G探险者5 小时前
深入理解 KeepAlive:从 TCP 到连接池再到线程池的多层语义解析
后端
Takklin5 小时前
Java 面试笔记:深入理解接口
后端·面试
右子6 小时前
理解响应式设计—理念、实践与常见误解
前端·后端·响应式设计
濑户川6 小时前
深入理解Django 视图与 URL 路由:从基础到实战
后端·python·django
武子康6 小时前
大数据-120 - Flink滑动窗口(Sliding Window)详解:原理、应用场景与实现示例 基于时间驱动&基于事件驱动
大数据·后端·flink
用户281113022216 小时前
分布式事务总结
后端
xuejianxinokok6 小时前
新版本 python 3.14 性能到底如何?
后端·python