使用 Profile Guided Optimization 提升 Application 的性能

什么是 Profile Guided Optimization

在编译时,通过指定优化等级,编译器已经可以帮助我们进行适当的优化,比如 inline 一些短函数等。现在考虑这样一个场景:有一个稍微长一点的函数,刚好长到编译器不对它的调用进行 inline 优化,但是实际上,这个函数是一个热点调用,在运行时被调用的次数非常多。那么如果此时编译器也能帮我们把它优化掉,是不是很好呢?但是,编译器怎么能知道这个“稍微长一点的函数”是一个热点调用呢?

这就是 Profile Guided Optimization(PGO)发挥作用的地方。PGO 是一种根据运行时 profiling data 来进行优化的技术。如果一个 application 的使用方式没有什么特点,那么我们可以认为代码的调用没有什么倾向性。但实际上,我们操作一个 application 的时候,往往有一套固定流程,尤其在程序启动的时候,这个特点更加明显。采集这种“典型操作流”的 profiling data,然后让编译器根据这些 data 重新编译代码,就可以把运行时得到的知识,运用到编译期,从而获得一定的性能提升。然而,值得指出的一点是,这样获得的性能提升并不是十分明显,通常只有 5-10%。如果已经没有其他办法,再考虑试试 PGO。


使用 PGO 的经验

下面具体说说在 MacOS 上进行 PGO 的一些方法和经验,不过核心知识可以迁移到其他平台,只要使用的编译器是 Clang 即可。

首先,Xcode 已经提供了 PGO 的 UI 操作(详情可参考:developer.apple.com/lib),所以如果是简单的 application,可以直接使用 UI 操作的方式,简单省事。不过,UI 操作有一些缺陷,具体表现在:

  1. 控制粒度粗糙,要么不打开 PGO,要么对所有 code 进行 PGO。如果项目中有 swift 代码,那么这种方式就不能用了,因为 swift 不支持 PGO;
  2. 只支持两种方式采集 profiling data。第一种是每次手动运行,运行结束后退出 application,Xcode 会产生一个 xxx.profdata,之后的编译,都会用这个文件,来进行优化;如果代码发生变更,Xcode 会提示 profdata file out of date。第二种方法是借助 XCTest 来采集 profiling data,这种方法提供了一定的 automation 能力,但是另一方面也限制了 automation team 的手脚,他们可能在使用另一些更好用的工具而不是 XCTest。


在真正的开发环境中,我们一般使用 automation tests 作为 training set,而非手动执行;另一方面,自动化测试用具一般很难集成到 XCTest 中。Xcode 的后端编译器用的是 Clang,PGO 的 UI 功能也是来源于 Clang,如果直接从 command line 入手,或许就能克服上述缺陷。基于这个想法,我进行了一些调研,在这篇问答(stackoverflow.com/quest)中,作者提到了他在命令行中采用的办法:

I compile with -fprofile-instr-generate:
clang++ -o test -fprofile-instr-generate dummy.cpp

The executable "test", when launched, generates a default.profraw file

I can merge the profiles with llvm-profdata merge At the end I can compile with the profiles integration, with -fprofile-instr-use on the .profdata

所以我们能够知道,使用方法大致是这样:先带着 -fprofile-instr-generate 进行编译,然后运行 application 获得 profraw 文件(比如可以通过 automation tests 来“可重现”地获得这些文件),如果有多个 profraw,需要使用 llvm-profdata 工具进行合并,得到一个 profdata 文件,最后再带着 -fprofile-instr-use=xxx.profdata 进行编译。


经过一系列的尝试,得到了下面这些经验,我认为对加深理解和正确使用 PGO 都有指导意义(环境:MacOS 10.13,Xcode 9.3):

  1. 同一个 binary 中的不同 object 文件之间,没有强制传染性:A.cpp 和 B.cpp 两个源文件,编译 A 时带着 -fprofile-instr-generate,B 不带着,结果是 A.o 中包含 clang 插入的函数调用(___llvm_xxx), B.o 中没有,链接后的可执行文件中包含 clang 插入的函数调用。执行该可执行文件,可以产生 default.profraw。
  2. 执行了 PGO 的库文件,对 client 没有强制传染性:A 带着 -fprofile-instr-generate,并且被编译为 dylib,B 不带着,并且被编译为 executable,但是使用 A 这个 dylib;dylib 中有插入的函数,B 的可执行文件中没有;执行可执行文件,可以产生 default.profraw。
  3. 上面的两个发现说明,即使是部分 code with profiling,也可以正确产生 pgo 的 profraw 文件。
  4. 在 Mac 上,必须直接启动可执行文件,才能产生 profraw;若使用 open XXX.app 的方式,则没有 profraw 文件产生。
  5. 产生的 profraw 默认名字为 default.profraw,该文件就在启动 application 命令执行时的目录下,即当前目录。
  6. default.profraw 可以被指定名字。有两种方法:第一,通过 -fprofile-instr-generate=XXX.profraw 指定;第二种是通过设置环境变量 LLVM_PROFILE_FILE。当然,因为我们是希望通过自动化方式进行,完全可以做到,跑完一个 automation test,就把当前目录下的 profraw 移动到另一个地方去,同时重命名:mv $(pwd)/default.profraw <somewhere/else/time-stamp.profraw>。
  7. 在 Mac 上,直接执行 llvm-profdata 一般会提示没有该命令,这时候可能第一想法是去 download 或者 install 一个,其实不必这么麻烦。因为 Xcode 本身使用 Clang 作为编译器,因此这个工具已经安装好了,要运行它,只需借助 xcrun 命令:xcrun llvm-profdata merge <path/to/folder>/*.profraw -output pgo.profdata。
  8. -fprofile-instr-generate/use 不仅是编译器选项,同时也是 linker flag,所以在配置 Xcode 工程时,要同时配置 OTHER_CPLUSPLUSFLAGS 和 OTHER_LDFLAGS。
  9. 借助环境变量和 xcconfig file,可以很容易实现流程的自动化,这属于基础知识了。


总结

在编译期优化的基础上,我们还可以使用 PGO 技术来获取运行期信息,进一步提升性能 5-10%;对于一般规模的应用程序,Xcode 提供的 UI 操作已经能够满足要求,但是如果程序规模较大,并且希望整合到自动化流程中,我们需要借助 -fprofile-instr-generate 和 -fprofile-instr-use 这两个 Clang 提供的编译选项,获得更加灵活的实现方案。

发布于 2018-11-01 17:20