每周性能技巧 #94:在数据不完美的世界中做决策
本节阅读量:本文翻译自 Abseil 官网的 Performance Tip of the Week #94: Decision making in a data-imperfect world。
原文发布于 2025 年 6 月 26 日,作者 Chris Kennelly,更新于 2025 年 6 月 27 日。
Profiling 工具对于缩小“我们所有可能改动”的搜索空间非常关键,它能突出最值得投入时间的方向。本篇讨论:什么时候应该继续寻找更多数据,什么时候已经进入边际收益递减。
聚焦结果
我们最终希望从工具中得到的,是能帮助我们改善性能的洞见。
我们几乎总能做更多分析,例如增加时间跨度、考虑辅助指标、尝试替代分桶策略,来帮助做决策。这在初期有助于发现未预料到的情况,但也可能让我们陷入伪发现或分析瘫痪。
改变决策
在某个时刻,冗余工具、额外精度或进一步实验带来的价值会低于获取它们的成本。虽然我们仍然可以追求这些东西,但如果它们不会改变我们的决策,就可以没有它们,并得到相同结果。
寻找“最后一个证据”很容易成为陷阱,但这有机会成本。我们偶尔会做出错误,以失败项目的形式出现。虽然事后看,其中一些本可以通过额外分析或一个小的“快速失败”原型避免,但当风险较低时,这些错误是可以承受的。
考虑做一个实验以获取额外信息时,可以问自己:“可能的结果范围会说服我改变主意吗?”否则,构造更多实验可能只是服务于确认偏误。下面这些例子中,额外分析看起来很有吸引力,但最终并不必要:
- 我们可能用很小的 rollout 范围和复杂度尝试一种新的优化技术。如果它有效,我们就获得信心。如果失败,就应该改变计划。如果这两种可能结果都不足以说服我们,那就应该改为制定能提供明确下一步的计划。
- 如果计划是从微基准到负载测试再到生产迭代推进,那么早期基准给出中性结果时,我们应该停止或至少重新考虑,而不是继续推到生产。基准可能给出与生产不同的结果,这个事实也许会促使我们继续前进,但如果计划是无条件忽略这些数据,那么费力收集它们就没有意义。
- 估算只需要好到能打破平局。如果计划是查看某个库最大的前 10 个用户寻找性能机会,更高准确性可能稍微调整顺序,但不会剧烈改变。用昨天数据和上周数据排名,历史数据可能给出略有不同的顺序,但分布主体通常大体一致。
快速失败
设想一个项目:我们乐观地认为它会生效,但要完成还需要漫长跋涉。我们可以使用几种策略快速获得信心,或者在它无法提供预期收益时放弃:
- 一个新 API可能更具表达力,也更容易优化,但我们需要迁移现有用户。测试和金丝雀发布单个用户,比先到处部署再切换实现更容易尝试,也更容易在失败时回滚。
- 我们可能想部署一种新的数据布局,无论是内存内还是磁盘上。一个模拟该布局近似形态的简单原型,可以告诉我们很多可能的性能特征。即使还不能处理所有边界情况,“最好情况”没有兑现,也能给我们停止并不再继续的理由。
例如,当我们试图移除数据间接访问时,可能会对某个数据结构的若干候选布局做微基准,从现状 std::vector<T*> 开始,比较 std::deque<T>、absl::InlinedVector<T*, 4> 和 std::vector<T>。这些方案各有优点,取决于 T 的约束和程序访问模式。
了解当前访问模式下最大机会在哪里,可以帮助我们聚焦注意力,避免在还没降低风险前就把时间沉入迁移。
把事情推过终点线可能仍然是 80% 的工作,但初始工作已经降低了结果风险。为最终失败的项目投入打磨时间,机会成本很高。
更新先验
我们做分析只是为了决定应该采取哪条行动。最终,我们关心的是行动的影响,而不是计划有多优雅。一个由良好估算和初步基准支持的有前途改动,只有在部署到生产后成功,才算真正成功。好的基准本身不是我们追求的结果。
有时,有前途的分析或基准不会转化为生产收益,这种结果反而是学习机会。
如果我们曾在几个候选方案中做选择,就应该确认它们的特性是否经得住考验。例如,我们可能选择了一个更复杂但看似更可靠的策略。即使项目其他方面成功了,但可靠性反而受损,也应该重新考虑替代方案是否值得回头看。
数据删失
工具有时存在盲点,我们在使用它们时需要考虑这些盲点。为得到“足够好”结果而做的简化可以帮助我们形成先验,但也要谨慎,避免过度外推。
带着同样限制的更多数据点只会让我们过度自信,而不会更准确。当风险更高时,用其他信息交叉验证可以帮助发现缺口。来自不同观察点的更多数据,比来自同一观察点的更多数据更有价值。我们应该预先思考:哪些新证据会让我们重新考虑计划?
Profiler 的局限
许多 profiler 为了降低自身影响,会有成本意识。为此,它们采用会遗漏一些数据点的采样策略。
我们的哈希表 profiler在表第一次发生 mutation 时做采样决定。避免在默认构造函数中做采样决定能保持高效,但也意味着空表不会出现在统计信息中。借助其他 profiler,我们可以确定许多被销毁的表实际上是空的。
历史上,TCMalloc 的 lifetime profiler 也有类似限制。为简化初始实现,它只报告 profiling 会话期间既被分配又被释放的对象。它遗漏了会话开始前已有的对象(左删失)和活过会话结束的对象(右删失)。这个 profiler 后来已经改进,包含这些对象,但理解 profiler 限制对于避免从偏置数据中得出错误结论至关重要。
跟踪 CPU 周期的 profiler 通常测量的是某条指令 retire 花了多久。profile 会隐藏高延迟指令之后的指令成本。在其他情况下,分散归因的成本可能遮蔽函数真实关键路径,而真实关键路径只有通过仔细分析或 llvm-mca 这类工具才能找到。
这些例子说明,并非所有东西都能用基于采样的 profiler 测到,但通常存在不同方法。
局部群体
对单个应用运行负载测试,甚至在生产中金丝雀发布改动,都可以增加我们对“某事会生效”的信心。不过,有限范围并不能保证效果在更广群体中相同。
这个陷阱有正反两面。如果我们针对一个库有优化,而某个应用根本不用这个库,那么无论对该应用测试多少,都不太可能产生真实影响。没有机会展现收益时的负向结果不应该阻止我们,但我们可能因为伪结果放弃一个好想法。
反过来,当优化扩展到更大规模时,也可能因为初始数据点受路灯效应影响而遇挫。
可以通过测量更广群体上的效果,或选择一组有广泛代表性的数据点,而不只是一个数据点,来避免这个问题。最低限度上,我们应该确信自己选择的优化评估方法有潜力显示正向或负向影响。
结语
当我们收集数据来指导决策时,应该努力确保自己寻找的是会改变计划的特征。追求更多数据点来增强信心很容易成为陷阱,因为我们可能只是落入确认偏误。