每周性能技巧 #72:优化“优化”这件事

本节阅读量:

本文翻译自 Abseil 官网的 Performance Tip of the Week #72: Optimizing optimization

原文发布于 2023 年 8 月 7 日,作者 Chris Kennelly,更新于 2025 年 8 月 23 日。

随着摩尔定律放缓,我们不能再期待硬件创新免费带来性能提升,寻找优化机会变得越来越关键。本篇讨论如何选择、规划并推进优化项目。

规划

“计划没有价值,但规划至关重要。”

效率项目试图最大化硬件生产力,而项目本身也能通过有效规划来优化。项目选择会极大影响结果和影响力。

一个项目的实际影响,是多个因素的乘积。刚开始思考某个优化时,我们会估算所有这些因素。估算正确当然令人满意,但主要目标只是获得足够信息来做出更好决策,也就是优先做预期 ROI 更高的优化。

很多时候,一个有效数字就够了。花更多时间做更好估算或收集更多数据,本身不会提高效率。新信息到来时,我们可以更新先验。

确定要工作的领域后,就可以思考如何解决其中的问题。

最大潜在收益是什么

例如,如果一个函数只使用了整座仓库级计算机中的单个 CPU 核心,我们优化它的空间就有限。即使把成本降到零,影响也很低。相比之下,不去做更重要领域的机会成本很高。

我们能改变多少

只观察到“X 很昂贵”并不够,我们还必须能改变 X。

“光速”分析可以提供有用上界。如果一个操作必须从内存读取数据、处理并写回,那么它很难显著快过 memcpy。性能不必最终收敛到粗略类比对象,但如果已经很接近,就很难有意义地影响 X。

有些操作已经简化到极限,例如两个整数相加很难再加速。这时可能需要向栈上层看,减少相加次数,才能获得收益。

我们能否测量变化,以及如何测量

一个没有被测量的优化,就像森林里没人听见的倒树。

有些变化会产生高度非局部的效果,使准确测量更困难。TCMalloc 有一个“昂贵”的 prefetch,让 TCMalloc 看起来更贵,但会改善应用整体性能。在分布式系统中,客户端改变发送给服务器的请求,可能显著影响服务器成本,却不一定影响客户端看到的指标

成功概率多高

修改一个小实现细节可能很直接,也可能揭露依赖现有怪癖的 Hyrum 定律问题。一个全新 API可能容易实现,却难以推动采用。如果别人以前看过同一个问题,也许这次情况不同,但我们应该控制乐观程度。

需要多久

估算应包括初步分析、实现、代码评审、调试、发布、测量,以及发布后的改进期。

我们希望节省的资源成本是时间投入的数倍,这既保证正投资回报,也为估算误差留出余地。

能持续多久

优化会存在多年不变的代码,是更安全的下注。如果目标系统很快会被替换,就需要考虑工作的预期寿命。

有时,最好放下这个想法,继续处理优先级列表中的下一项。另一些时候,优化工作可以迁移,或提高新系统的效率基线,使经济影响超过原始实现寿命。

站在巨人肩上

“伟大的艺术家会偷师。”

很多机会来自扩大既有优化的采用范围,或增强既有优化。

  • 历史上,“hugepage text” 只在 fleet 中一部分服务器上用于减少 iTLB miss。对剩余服务器来说,获得应用生产效率提升只差打开一个 flag。许多规划因素已经降低风险:我们知道启用后会对 fleet 很大一部分产生可观影响,因此可以专注如何到达那里。
  • AutoFDO 使用生产 profile 反馈给编译器以优化程序性能。初始基础设施建好后,我们通过多路策略继续节省:扩大采用、提高 profile 质量,并在其上构建 FS-AFDO、cmov-vs-branch 和 function splitting 等优化。

从其他领域借想法,即使看似简单,也可能很有价值。“不要做额外工作”听起来显然,但用新鲜视角看问题并交叉传播想法,往往很有效。

有了要尝试的想法后,我们就该系统化评估它们。

优化生命周期

“如果它和实验不一致,那就是错的。这个简单陈述就是科学的关键。”

降低想法风险

在初始规划中,我们做了一些假设来估算项目可能的投资回报。第一优先级是通过原型和评估来降低这些假设的风险。

  • 用 benchmark 改进估算。这让我们能迭代改善估算,并在需要时投入更多精力获得更高保真度。
  • 评估对次级指标的影响。即使无法立即看到端到端、全 fleet 影响,PMU 事件、profile 或单个应用也可以告诉我们方向是否正确。
  • 先偷懒。做简化假设或绕过边界情况,可以更早拿到数据。即使还没有到最佳位置,也能确认关于机会的直觉是否有效。这也有助于区分真正关键的项目依赖和只是锦上添花的依赖。

如果尝试一个想法却无法移动预期指标,假设可能是错的。我们可能发现某事其实无法优化,或并不在预期关键路径上。这些解释可以帮助我们调整计划、直接清掉项目,或启发未来调查方向。

一旦证明手头有可行优化,就可以关注如何落地。

构建想法

测试失败可以指出约束实现的边界情况。这可能要求调整实现以保留行为,或清理不再允许的用法。选择主要依赖判断:更简单实现长期更容易继续优化,但过于艰巨的清理可能让任何东西都无法落地。

这也可能是加入随机化和其他防御的好时机,让这个领域未来的优化更顺畅。

借助 benchmark,我们可以微调想法,尝试不同参数,并确认它在多种情境下提升性能。

  • 不必找到精确最优参数,因为我们很容易过拟合模型或负载测试。最优性可能只是幻觉。
  • 对库做初始改动通常最难,尤其是继续推进时,会发现未知的未知。在真正克服启用障碍之前,优化每个周期或每个字节 RAM 都不重要。

例如,从 GoodFastHashstd::hash 迁移到 absl::Hash 改善了性能,但需要通过随机化哈希算法和识别脆弱测试来驯服 Hyrum 定律。克服这些问题后,我们才能继续迭代,把 absl::Hash 的算法换成另一个更高效的实现。

发布

生产最终才重要,我们的目标是把优化带到那里评估。发布并迭代优化有几种帮助:

  • 更早交付价值。随着时间推移的效率曲线下面积,会因多次小型连续落地而变大,而不是等一个“更好”的优化一次完成。TCMalloc 的 Temeraire 优化在后续几年中影响翻倍,但早发布比等待几年后一次性完成更早带来收益。
  • 实际表现可能和预期不同,无论好坏。这会启发后续寻找想法的方向。

对广泛启用的优化,opt-out 可以告诉我们哪些边界情况真正重要,且需要更多关注,从而产生进一步优化洞见。例如,Temeraire 的一个长期 opt-out 启发了两个不同优化。

一个领域的成功会带来交叉传播机会。我们可以把同一个算法或数据结构应用到相关但不同的领域。如果没有最初落地,可能永远看不到这种机会。

  • 情况一直在变。项目启动时的假设,几年后可能已经失效。中间里程碑让我们能一路验证假设。
  • 为了速度,我们可能绕过了非关键依赖。当这些依赖准备好后,可以重新审视选择,增强优化。

落地

优化发布后,我们希望正式落地,因此必须测量它。我们主要关注主指标,例如每 CPU 查询量上升。次级代理指标,例如某函数 CPU 时间、cache 或 TLB miss 等 PMU 事件,则帮助确认优化产生了预期效果。

这能把结果和巧合区分开来:我们是真的加速了某些东西,还是其他地方的改动碰巧让应用更快?估算用于指导开始方向,但我们也不希望这些预期偏置测量。真实数据可能比预想更好或更差,现实才重要。

总结

项目选择和项目执行都会极大影响优化结果。审慎选择挖掘领域,并采用“发布并迭代”的策略,可以释放显著节省。

上一篇:每周性能技巧 #62:识别并减少内存带宽需求

上一节

下一篇:每周性能技巧 #79:一次最多只做一个权衡

下一节