使用Prodfiler优化eBPF优化器(重发版)
如何在不修改任何代码的情况下将应用程序性能提升近2倍?请继续阅读!
这是我在2021年10月4日最初发表在prodfiler.com上的博客重发版。Prodfiler已被Elastic收购,现为Elastic Universal Profiler。
在本文中,我将逐步介绍如何使用Prodfiler发掘K2(eBPF优化编译器)中的优化机会。K2完全受CPU限制,采用引导式搜索技术,依赖高速创建和检查候选方案的能力。通过Prodfiler,我们可以轻松发现K2中消耗最多CPU周期的组件,从而进行相应优化。最终结果是K2版本速度提升1.4-1.9倍,意味着在相同资源下可以探索更大的搜索空间。
预期改进来自:
- 用性能更好的mimalloc替换系统分配器
- 辅助编译器自动向量化热点循环
- 对K2及其重要依赖Z3应用PGO和LTO
引言
随着eBPF更多用例的发现(许多处于关键路径),编译器生成的eBPF代码性能日益重要。虽然clang通常能生成高效程序,但有时会产生存在冗余、指令序列选择异常、操作数宽度不必要狭窄等问题的代码,导致手动优化程序明显更快。手动优化eBPF指令具有挑战性、耗时且容易出错,因此与传统目标平台编译一样,存在投入编译时间以换取运行时性能提升的工具市场。
罗格斯大学研究人员今年8月发布了K2,这是eBPF代码的优化编译器。K2输入现有eBPF程序,搜索语义等效但更小更快的程序。论文证明该方法可减少程序大小6-26%,降低平均包处理延迟1-55%,相对最佳clang编译程序提升吞吐量达5%。
K2架构概述
K2核心采用MCMC与Metropolis-Hastings接受准则。算法核心是:
- 从当前状态生成新候选
- 为候选分配成本
- 根据候选成本与当前状态成本有条件更新当前状态
算法重头戏在步骤2。为计算候选成本,K2使用自定义用户空间eBPF解释器在输入集上解释候选。如果候选产生与原程序相同输出,K2将候选转换为符号表示并使用Z3定理证明器进行等价检查。因此MCMC搜索内循环计算密集,每个候选需要大量计算工作。
设置基准测试
K2可在GitHub找到,作者还上传了包含测试和基准的会议工件。我克隆此仓库添加了辅助脚本。主要K2仓库缺乏安装运行信息,但幸运的是有安装脚本可遵循。脚本在11个不同eBPF程序上调用K2寻找更高效实现,我们将以其为基础进行基准测试。
使用Prodfiler进行基准测试和优化
Prodfiler入门简单。按照文档创建新项目并部署后,运行上述基准测试收集基线数据。我创建的脚本默认每个基准运行15次,还提供"快速"模式快速验证假设。
第一幕:内存分配优化
Top N Functions视图显示malloc和free函数位列第5和第6。扩展列表发现内存管理函数占前20位中5席,总计消耗应用10% CPU时间!我们选择用mimalloc替换系统分配器。
使用mimalloc后,free函数完全退出前10,内存管理总开销降至约5%。基准测试显示平均加速1.08倍,最小1.05倍,最大1.12倍。
第二幕:优化std::vector
Top N Functions前两位占15% CPU预算,涉及std::vector
这些向量大小固定(11和512),但编译器未能自动向量化复制循环。将bool替换为uint8_t性能改进微弱,因uint8_t存在别名问题。改用C++20的char8_t类型后,编译器生成直线SIMD代码。
此更改带来平均1.31倍加速(相对mimalloc版本),最大1.57倍,最小1.12倍。相对原始版本平均加速1.43倍,操作符开销从12.3%和3.93%降至0.82%和0.62%。
第三幕:优化Z3
火焰图显示Z3_solver_check()及其子函数占K2工作量的46%。我们选择通过PGO和LTO优化Z3编译方式。
对Z3应用PGO和LTO后,平均获得1.1倍性能提升。最终对K2本身应用PGO,再获平均1.03倍提升。总体平均改进1.62倍,最大1.91倍,最小1.42倍。
结论
本文展示了如何使用Prodfiler作为迭代过程的一部分显著提升应用性能。我将Prodfiler视为"疑问生成器",其各种视图能引发"这很奇怪"的思考,从而发现消耗CPU的意外特性。结合持久性,现代软件栈中存在巨大性能改进空间。