60%+性能提升:持续性能分析与库匹配实战——动态与静态分析结合的性能优化(第一部分/共两部分)
为什么需要结合静态和动态分析?
几年前我共同创立了optimyze.cloud,并开发了Prodfiler(现为Elastic Universal Profiler)——一个全舰队范围的、持续的、全系统性能分析器。这种分析器效率足够高,可以始终开启,在您舰队中的每台机器上分析整个系统。启用Prodfiler/EUP可以精确显示整个舰队中消耗CPU资源的代码行,无论是在用户空间、内核、本地代码还是某些高级运行时中。
这非常棒,但当面对舰队中最昂贵的10个或100个函数时,问题出现了:这些函数如此昂贵是"正常"的吗?如果不是,为什么它们如此昂贵,可以采取什么措施来解决问题?
如果您幸运,昂贵的函数位于您编写和理解的代码中,这些问题可能很容易回答。如果您不幸,它位于某个您不熟悉的第三方库中,用您不知道的语言编写,并且在您从未听说过的运行时之上。我们的用户经常问的一个问题是,除了昂贵函数的列表之外,我们是否还可以建议根本原因,或者更好的修复方法。正是从这里,我们需要某种补充分析,可以"推理"为什么特定代码块可能很昂贵,并在可能的情况下建议使其不那么昂贵的方法。
结合动态和静态分析的模式
在程序分析研发中,关于什么有效、什么无效没有硬性规定,这是一个在学术界和工业界都有大量持续工作的领域。也不一定坚持将一种静态分析与一种动态分析结合。您甚至可以完全跳过一种分析形式,并链接一堆纯动态分析,反之亦然。
对于性能优化和分析,我认为有三种操作模式可以使多种分析相互受益:
1. 上下文排序(Context-for-ranking)
动态分析提供了系统中最昂贵部分的真实情况,最好是在生产环境中执行真实工作负载时。然后可以使用此上下文对静态分析的结果进行排序。排序很重要有两个原因:首先,开发人员的时间有限;其次,静态分析工具发现的性能问题是否有效可能取决于相关代码是否在热路径上。
2. 分析排序(Ranking-for-analysis)
轻量级动态分析提供系统中最昂贵部分的信息,以便可以集中进行重量级分析(或者可以在这些区域进行更多轻量级分析)。某些分析运行起来需要大量CPU/RAM资源。
3. 操作上下文(Context-for-operation)
一种分析提供另一种分析运行所必需的数据。这方面的例子包括配置文件引导优化/AutoFDO或像Facebook的BOLT这样的链接后优化器。
低垂的美味果实
结合静态和动态分析进行性能优化的最直接方法是模式匹配应用程序性能配置文件中最重要的库和函数,并且我们知道存在功能等效的优化版本。我们有真实世界的数据,客户通过这种方法获得了巨大的性能提升。
我们遇到的一些具体示例:
- 如果在内存分配相关函数(如malloc、free、realloc等)上花费了大量时间,并且应用程序使用标准系统分配器,那么通过切换到更优化的分配器(如mimalloc或jemalloc)可能会获得性能改进
- 如果在zlib上花费了大量时间,那么切换到zlib-ng可以带来改进
- 如果在JSON处理或序列化和反序列化上花费了大量时间,那么执行这些操作的第三方库的性能存在巨大差异
这个过程很简单,并且具有出色的努力回报比——查看结果,搜索"优化库X",确保它满足您的功能和非功能需求,替换、重新部署、测量,然后继续。如果您刚开始结合分析,请从这里开始。
结论
希望以上内容有助于建立一个思考分析组合方式的框架。如果您是SRE/SWE并在您的环境中集成了持续性能分析/简单匹配方法,我很想听听结果如何。这个领域有很多研发空间,所以如果您有任何有趣且有用的分析示例,请在Twitter上给我发私信。
本文是两部分系列的第一部分,第二部分将讨论使用CodeQL查找循环向量化机会。