分支判断 PGO 优化示例第二个例子是一个针对分支判断的优化示例 。假设有如下代码片段,该代码片段判断参数 a 是否为 true,若是,则执行 a_staff 的逻辑;否则,执行 b_staff 逻辑 。
if (a)// do a_staff...else// do b_staff...return
在编译时,由于编译器并不能假设 a 为 true 或者 false 的概率,通常按照同样的 block 顺序输出机器指令,伪汇编代码如下 。其中,先对参数 a 进行 bool 判断,若为 true ,则紧接着执行 a_staff 的逻辑,再 return;否则,便跳转到 .else 处,再执行 b_staff 的逻辑 。
test a, aje.else; jump if a is false.if:; do a staff...ret.else:; do b staff...ret
在 CPU 的实际执行中,由于指令顺序执行以及 pipeline 预执行等机制,因此,会优先执行当前指令紧接着的下一条指令 。上面的指令对 a_staff 是有利的,如果 a 为 true,那么整个流水线便一气呵成,没有跳转的开销;相反的,指令对 b_staff 不利,如果 a 为 false,那么 pipeline 中先前预执行的 a_staff 计算则会被作废,转而需要从 .else 处的重新加载指令,并重新执行 b_staff,这些消耗会显著降低指令的执行性能 。
从上面的分析可以得出,如果恰好在实际运行中,a 为 true 的概率比较大,那么该代码片段会比较高效,反之则低效 。借助对程序运行期的 profile 数据进行采集,则可以得到上面的分支判断中,实际走 if 分支和走 else 分支的次数 。借助该统计数据,在 PGO 编译中,若走 else 分支的概率较大,编译器便可以对输出的机器指令进行调整,类似如下的伪汇编指令,从而对 b_staff 更有利 。
test a, ajne.if; jump if a is true.else:; do b staff...ret.if:; do a staff...ret
Profile 数据的采集及转换为了采集 mesh proxy 运行期的 profile 数据,首先需要进行正常的最优编译并生成二进制 。为了避免二进制中同名 static 函数符号的歧义,以及区分同一行 C++ 代码中多个函数的调用,提高 PGO 的优化效果,我们需要新增
-funique-internal-linkage-names 和
-fdebug-info-for-profiling 这两个 clang 编译参数,此外,还需要增加 -Wl,--no-rosegment 链接参数,否则 linux-perf 收集到的 perf 数据无法通过 AutoFDO 转换工具转换成 LLVM 所需的格式 。
完成编译后,选择合适的 benchmark 或者真实流量运行程序,并采用 linux-perf 工具采集 perf 数据 。经过实践验证,使用 linux-perf 采集时,启用 LBR(Last Branch Record)功能可以获得更佳的优化效果 。我们采用如下命令对 mesh proxy 进程进行 perf 数据采集 。
【字节跳动 Service Mesh 数据面编译优化实践】perf record -p <pid> -e cycles:up -j any,u -a -- sleep 60
完成 perf 数据采集后,使用 AutoFDO 工具(
https://github.com/google/autofdo)将 perf 数据转换成 LLVM profile 格式 。
create_llvm_prof --profile perf.data --binary <binary> --out=llvm.prof
带 PGO 的优化编译得到 profile 数据后,即可进行最后一步带 PGO 优化的重编译步骤,需要注意的是,该次编译的源码必须和之前 profile 采集用的源码完全一致,否则会干扰优化效果 。为了开启 PGO 优化,只需要再添加 -fprofile-sample-use=llvm.prof clang 编译参数,使用该 llvm.prof 文件中的 profile 数据进行 PGO 编译优化 。
经过 PGO 编译优化后,mesh proxy 二进制整体的 indirect call 数量降低了 80%,基本完成了 C++ Devirtualization 的目标 。此外,PGO 会根据 profile 中的热点函数及指令进行更进一步的内联,对热点指令及内存进行重排,并进一步增强常规的优化手段,这些都能给性能带来显著的收益 。
其他编译优化工作全静态链接及 LTO 实践在字节 mesh proxy 达到一定的线上规模后,我们遇到了动态链接上的一些问题,包括运行机器的 glibc 版本可能较低,以及动态链接的函数调用本身有多余开销 。
考虑到 mesh proxy 本身其实是作为一个独立的 sidecar 运行,并不需要作为一个程序库供其他程序使用,因此,我们提出将 binary 进行全静态链接的想法 。这样做的好处有:一是可以避免 glibc 版本问题,二是消除动态链接函数跳转开销,三是全静态链接下可以进一步应用更多编译优化 。
支持全静态链接后,由于 binary 没有任何外部库依赖,我们又增加了进一步的编译优化,包括将 thread local storage 的模型改为 local-exec,以及 ThinLTO(Link Time Optimization)优化 。其中,ThinLTO 带来了将近 8% 的性能提升 。
推荐阅读
- 主机字节序和网络字节序
- 图文教程 微信小程序如何调用后台service
- 脸上跳动是什么原因?
- 内存为什么是以字节为单位的?
- Android面试题集锦之 Service
- 抖音推荐特征系统演进历程
- 什么是微服务?
- Service Worker与缓存
- 虽然「字节跳动」来晚了,但整个车联网行业都在等它
- 汉字节是几月几号