背景
随着字节跳动的业务迅速增长,微服务的性能优化工作显得尤为重要,对于头部应用来说,提升若干百分点的性能也能为公司节省巨大的服务器资源成本。
编译器优化是软件性能优化的一种常用方法,相比其它特定的性能优化方法,它的适用性更广,能更全面地获得性能收益,从而降低成本。编译反馈优化(PGO)是常见的编译器的优化方法,字节跳动 STE 团队在编译反馈优化技术方向进行了持续的探索,并在字节跳动业务上积极地开展了实践工作,成功将编译反馈优化技术大规模落地于字节跳动业务,为公司节省了大量资源。
PGO简介
PGO(Profile-guided optimization)通常也叫做 FDO(Feedback-directed optimization),它是一种编译优化技术,它的原理是编译器使用程序的运行时 profiling 信息,生成更高质量的代码,从而提高程序的性能。
传统的编译器优化通常借助于程序的静态分析结果以及启发式规则实现,而在被提供了运行时的 profiling 信息后,编译器可以对应用进行更好的优化。通常来说编译反馈优化能获得 10%-15% 的性能收益,对于特定特征的应用(例如使用编译反馈优化 Clang本身)收益高达 30%。
编译反馈优化通常包括以下手段:
- Inlining,例如函数 A 频繁调用函数 B,B 函数相对小,则编译器会根据计算得出的 threshold 和 cost 选择是否将函数 B inline 到函数 A 中。
- ICP(Indirect call promotion),如果间接调用(Call Register)非常频繁地调用同一个被调用函数,则编译器会插入针对目标地址的比较和跳转指令。使得该被调用函数后续有了 inlining 和更多被优化机会,同时增加了 icache 的命中率,减少了分支预测的失败率。
- Register allocation,编译器能使用运行时数据做更好的寄存器分配。
- Basic block optimization,编译器能根据基本块的执行次数进行优化,将频繁执行的基本块放置在接近的位置,从而优化 data locality,减少访存开销。
- Size/speed optimization,编译器根据函数的运行时信息,对频繁执行的函数选择性能高于代码密度的优化策略。
- Function layout,类似于 Basic block optimization,编译器根据 Caller/Callee 的信息,将更容易在一条执行路径上的函数放在相同的段中。
- Condition branch optimization,编译器根据跳转信息,将更容易执行的分支放在比较指令之后,增加icache 命中率。
- Memory intrinsics,编译器根据 intrinsics 的调用频率选择是否将其展开,也能根据 intrinsics 接收的参数优化 memcpy 等 intrinsics 的实现。
编译器需要 profiling 信息对应用进行优化,profile 的获取通常有两种方式:
- Instrumentation-based(基于插桩)
- Sample-based(基于采样)
Instrumentation
Instrumentation-based PGO 的流程分为三步骤:
- 编译器对程序源码插桩编译,生成插桩后的程序(instrumented program)。
- 运行插桩后的程序,生成 profile 文件。
- 编译器使用 profile 文件,再次对源码进行编译。
图片
Instrumentation-based PGO 对代码插桩包括:
1. 插入计数器(counter)
- 对编译器 IR 计算 MST,计算频繁跳转的边,对不在 MST 上的边插入计数器,用于减少插桩代码对运行时性能的影响。
- 在函数入口插入计数器。
2. 插入探针(probes)
- 收集间接函数调用地址(indirect call addresses)。
- 收集部分函数的参数值。
Sampling
Sample-based PGO 的流程同样分为三步骤:
- 编译器对程序源码进行编译,生成带调试信息的程序(program with debug information)。
- 运行带调试信息的程序,使用 profiler(例如linux perf)采集运行时的性能数据。
- 编译器使用 profile 文件,再次对源码进行编译。
图片
其中步骤2采集的数据为二进制级别采样数据(例如 linux perf 使用 perf record 命令收集得到 perf.data 文件)。二进制采样数据通常包含的是程序的 PC 值,我们需要使用工具,读取被采样程序的调试信息(例如使用 AutoFDO 等工具),将程序的原始二进制采样数据生成程序源码行号对应的采样数据,提供给编译器使用。
探索与落地
对比 sampled-based PGO,Instrumentation-based PGO 的优点采集的性能数据较为准确,但繁琐的流程使其在字节跳动业务上难以大规模落地,主要原因有以下几点:
- 应用二进制编译时间长,引入的额外编译流程影响了开发、版本发布的效率。
- 产品迭代速度快,代码更新频繁,热点信息与应用瓶颈变化快。而 instrumented-based PGO 无法使用旧版本收集的 profile 数据编译新版本,需要频繁地使用插桩后的最新版本收集性能数据。
- 插桩引入了额外的性能开销,这些性能开销会影响业务应用的性能特征,收集的 profile 不能准确地表示正常版本的性能特征,从而降低优化的效果,使得 instrumented-based PGO 的优点不再明显。
使用 Sample-based PGO 方案可以有效地解决以上问题:
- 无需引入额外的编译流程,为程序添加额外的调试信息不会明显地降低编译效率。
- Sample-based PGO 对过时的 profile 有一定的容忍性,不会对优化效果产生较大影响。
- 采样引入的额外性能开销很小,可以忽略不计,不会对业务应用的性能特征造成影响。
我们的目标是在字节跳动业务上大规模落地 PGO 技术,使字节跳动大量的应用能够从 PGO 技术中受益。PGO 本身是一项较为成熟的编译器优化技术,我们面临的主要问题为如何简化 PGO 繁琐的流程,自动化数据采集与优化流程,保证优化的可靠性。我们设计了系统解决这些问题,该系统的架构如图所示:
图片
其中包含的主要任务有:
- 集群维度的业务性能数据采集、维护与处理。
- 业务二进制与调试信息维护。
- 采样数据的查询与使用。
- 生成编译器可用的信息(LLVM profile)。
- Profile 更新,业务性能测试与发版上线。
数据采集与处理
我们做了大量的前期工作,在 SPEC CPU2017 等 benchmark 上进行了 PGO 测试与结果分析,基于测试结果得出结论,包括:不同采样事件以及 LBR 数据对 Profile 准确度的影响,单机采样频率与额外性能开销之间的联系,集群采样数据聚合策略以及样本数量与优化效果的联系等。
我们在字节跳动服务器集群的物理机实例中部署了采样程序,该程序在后台以用户无感知的方式进行着性能数据的常态化采集工作,并将采集的性能数据上传。原始数据被解析处理后,最终维护在采样数据库中。
对于每一条采样记录,我们会维护它的原始信息,以及额外的元数据,这些信息包括:
- 采样地址
- 采样的 LBR(last branch record)数据
- 采样事件类型
- 采样所属二进制的 ID
- 采样业务的 PSM
- 其它元数据
收集二进制与符号
我们搭建了二进制仓库,维护着所有线上运行的二进制以及其描述信息。对于加入到二进制仓库的二进制,系统会触发任务对其进行处理,提取 PGO 任务所需的符号信息,维护在指定的数据库中。
对于符号信息的查询,系统会返回指定二进制的 PC 值对应的 inline stack 信息,供 PGO 任务使用。
使用PGO优化流程
我们搭建了 PGO 平台,供业务用户使用 PGO 优化流程。业务接入 PGO 优化流程,首先需要通过平台指定策略。策略的作用是:
- 指定目标业务的二进制以及使用场景等信息。
- 指定生成 profile 需要的样本量以及时间窗口等信息。
- 创建定时任务,为指定业务生成适用于 LLVM 的 PGO profile。定时任务会定时向采样数据库发送请求,查询目标二进制某一时间窗口内的性能数据,查询结果会被序列化。随后任务会解析采样数据以及相应的符号信息,生成 profile 文件,并将产物上传到指定存储中,供业务编译时使用。
Profile 文件会被策略定期更新,业务开发同学在构建时通过选项控制使用 profile 编译得到 PGO 优化后的二进制版本,进行常规的版本发布流程,性能对比测试,决定是否上线优化版本。
落地成果
我们在字节跳动内部头部 30+ 应用上落地了 PGO 优化流程,业务均获得明显的 CPU 以及延迟收益,平均获得了超过 5% 的 CPU 收益。
图片
总结
PGO 技术大规模在字节跳动业务落地实践后取得了显著收益,在其过程中,STE 团队也克服了诸多技术难题:完成了集群维度的数据采集工作,保证了数据的准确性同时做到了对业务无感知;在业务快速迭代的现状下,完成了二进制仓库的建设,规范了二进制符号信息的查询流程;搭建了 PGO 优化平台,简化了流程,业务方在构建程序时只需添加指定编译选项,大幅降低业务使用 PGO 的成本;在业务程序组成复杂依赖众多的现状下,分析预编译库的热点信息以及 PGO 效果不及预期的原因,推进了源码依赖的编译流程;同一个程序有可能被多个业务使用,在不同使用场景、时间段下,程序特征会有明显区别,我们在系统设计层面,有效地解决了该问题。
STE 团队会在后续的工作中完善已有系统,并对 PGO 技术进行持续的探索。包括支持 PGO 与 LTO 对业务的共同优化,在 PGO 平台引入 post-link time optimizer 对二进制进行优化,实现基于采样数据的软件预取优化等等,使字节跳动业务从中获得更大的受益。