8.8 KiB
编译器利用 PGO 优化 App 性能
什么是 PGO?
Profile Guided Optimization (PGO) is an advanced feature for wringing every last bit of performance out of your app. It is not hard to use but requires some extra build steps and some care to collect good profile information. Depending on the nature of your app’s code, PGO may improve performance by 5 to 10 percent, but not all applications will benefit from it. If you have performance-sensitive code that needs that extra optimization, PGO may be able to help.
PGO 即 Profile Guided Optimization,
Using Profile Guided Optimization 官方文档大概意思是:PGO 是一种根据运行时 profiling data 来进行优化的技术。用起来不难,但需要一些额外的构建步骤和一些收集良好的 profile 文件。如果一个 application 的使用方式没有什么特点,那么我们可以认为代码的调用没有什么倾向性。但实际上,我们操作一个 application 的时候,往往有一套固定流程,尤其在程序启动的时候,这个特点更加明显。采集这种“典型操作流”的 profiling data,然后让编译器根据这些 data 重新编译代码,就可以把运行时得到的知识,运用到编译期,从而获得一定的性能提升。然而,值得指出的一点是,这样获得的性能提升并不是十分明显,通常只有 5-10%。如果已经没有其他办法,再考虑试试 PGO。
何时使用 PGO?
正常情况下不用,通常来说 LLVM 经过编译前端,通过词法分析、语法分析、语义分析构建出 AST,然后转换为 IR,IR 经过一些列优化 Pass,常见的 Pass 比如死代码消除等,LLVM 已经做了大量的优化工作。所以通常来说,我们需要正向的优化代码、优化算法、设计正确合理的架构、合理的 UI 层级。除此之外,你还想让 App 获得更好的性能,可以考虑采用 PGO 技术。
PGO 怎么样工作的?
PGO 假设你的应用程序的行为是可预测的,这样一个有代表性的配置文件就可以捕捉代码所有性能敏感方面的未来行为。当启用 PGO 时,Xcode 会构建一个专门检测的应用程序版本,然后运行它。您可以手动运行该应用程序,也可以使用 UI 自动化 XCTest 测试 App。当应用程序运行时,会统计并记录每条语句的执行次数。
应该收集一份具有典型、能够代表 App 真正用户行为的 profile 数据,PGO 统计每个语句的执行次数,并创建一个为该行为建模的概要文件。依据该文件,LLVM 编译器将优化工作集中在最重要的代码上。
举个例子:有一个稍微长一点的函数,刚好长到编译器不对它的调用进行 inline 优化,但是实际上,这个函数是一个热点调用,在运行时被调用的次数非常多。那么如果此时编译器也能帮我们把它优化掉,是不是很好呢?但是,编译器怎么能知道这个“稍微长一点的函数”是一个热点调用呢?PGO 根据这个 profile 文件进行优化
是一种优化编译器的技术,通过收集程序的实际运行数据,例如程序执行的分支情况,来指导编译器生成更优化的代码。
tips
首先,Xcode 已经提供了 PGO 的 UI 操作(详情可参考),所以如果是简单的 application,可以直接使用 UI 操作的方式,简单省事。不过,UI 操作有一些缺陷,具体表现在:
- 控制粒度粗糙,要么不打开 PGO,要么对所有 code 进行 PGO。如果项目中有 swift 代码,那么这种方式就不能用了,因为 swift 不支持 PGO;
- 只支持两种方式采集 profiling data。第一种是每次手动运行,运行结束后退出 application,Xcode 会产生一个 xxx.profdata,之后的编译,都会用这个文件,来进行优化;如果代码发生变更,Xcode 会提示 profdata file out of date。第二种方法是借助 XCTest 来采集 profiling data,这种方法提供了一定的 automation 能力,但是另一方面也限制了 automation team 的手脚,他们可能在使用另一些更好用的工具而不是 XCTest。
因为 PGO 优化是靠的 Profile 文件,所以每次代码变化后需要保证生成最新的 Profile 文件。 随着继续开发和更改应用项目中的代码,优化配置文件会过时。LLVM 编译器会识别出配置文件何时不再与应用程序的当前状态良好匹配,并提供警告。当收到此警告时,可以再次使用 Generate Optimization Profile 命令来创建更新的配置文件。每次重新生成优化配置文件时,Xcode 都会替换现有的配置文件数据。
这个很难办,所以需要利用 CI 手段,可以借助 -fprofile-instr-generate 和 -fprofile-instr-use 这两个 Clang 提供的编译选项搭配 CI。
LLVM 利用 PGO 大概怎么优化代码
LLVM利用PGO(Profile-Guided Optimization)可以实现多种优化,其中一些主要优化包括:
- 函数内联(Function Inlining):PGO可以根据实际运行时的函数调用情况,选择性地内联函数,减少函数调用的开销,提高程序执行效率。
- 循环展开(Loop Unrolling):通过分析循环执行次数和循环体内的代码,PGO可以决定是否展开循环,减少循环控制开销,提高循环执行效率。
- 代码重排(Code Reordering):根据实际运行时的代码执行路径,PGO可以优化代码的布局顺序,使得频繁执行的代码更容易被CPU缓存命中,提高程序的局部性和性能。
- 分支优化(Branch Optimization):PGO可以根据实际运行时的分支预测情况,优化分支指令,减少分支预测错误,提高程序的执行效率。
- 常量传播(Constant Propagation):根据实际运行时的数据流分析,PGO可以更好地进行常量传播,减少不必要的变量存储和加载操作,提高程序的执行效率。
- 内存访问优化(Memory Access Optimization):PGO可以根据实际运行时的内存访问模式,优化内存访问方式,减少内存访问延迟,提高程序的内存访问效率。 通过这些优化手段,PGO可以根据实际运行时的数据和行为模式,生成更加针对性和高效的优化代码,从而提高程序的性能和执行效率
PGO 和二进制重排的异同
PGO 是一个编译器特性,能够过程序实际执行的方法进行打点统计,找出最常执行的代码路径(热点函数),并根据这些信息对程序进行优化,这种优化包括重排代码已减少分支预测错误、优化内存使用以提高缓存命中率、函数内联、分支优化等等,这是一种动态优化技术,会根据实际程序运行收集到的 profile 信息做改变。
二进制重排则是程序编译完成后,对二进制代码进行优化的技术,主要要解决的是内存缺页异常的问题,可以减少缓存的错失率。这是一种静态优化技术,因为它不需要实际运行程序就能进行。
为什么我的App需要重排的符号个数这么少
二进制重排主要通过调整二进制文件中的代码顺序,以改善性能。以 iOS 举例,App 启动慢的一个原因就是 App 启动过程中用到的函数方法、可能排布在不同的 page 上,所以由于不断的切换 page,导致启动慢。App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理,放到某一页或者某几页」(虚拟内存中的页)。Xcode 工程允许开发者指定 「Order File」,可以「按照文件中的方法顺序去加载」,可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。
然而,虽然理论上所有的代码都可以进行重排,但实际上,根据应用程序的特性,可能只有一部分代码是频繁执行的,也就是所谓的"热点"代码。这部分代码会被优先考虑进行重排,因为这样可以最大化性能提升。
另外,重排过程需要考虑到许多限制和约束,如符号之间的依赖关系,这可能会限制哪些符号可以移动和重新排序。如果一些符号因为它们之间的关系而不能被移动,那么这些符号就不会被考虑在重排中。
因此,你看到的"需要重排的符号个数"相对较少,可能是因为只有这些符号是被识别出来的"热点"代码,或者它们是唯一可以在不违反任何约束和限制的情况下被移动的符号