60%+ 性能提升:持续性能分析与库匹配实战
第一部分:结合动态与静态分析进行性能优化
这是关于结合静态和动态分析进行性能优化的两篇系列文章中的第一篇。我将它们分开,否则会非常长,第二篇将在本周晚些时候上线。本文介绍了一些高级背景,讨论了为什么要结合分析,并提供了一个思考分析组合的松散框架。然后,我将解释一种简单的组合方法:通过持续性能分析与库名和版本的模式匹配,在实际客户基础设施中实现每个服务60%以上的性能提升。
在后续文章中,我将讨论去年使用CodeQL在C/C++代码库中寻找性能优化机会的实验性工作。CodeQL通常用于发现安全漏洞,但我将展示它如何用于查找可能导致编译器生成比可能慢10倍或更多的机器代码的C++代码模式。主要涉及那些阻止编译器自动向量化循环的代码结构,通过一些小的类型更改或语句重排序,我们可以实现显著的性能改进。
为什么要结合静态和动态分析?
几年前,我共同创立了optimyze.cloud,并构建了Prodfiler(现在是Elastic Universal Profiler)——一个全舰队范围的、持续的、全系统性能分析器。也就是说,一个足够高效的分析器,可以始终开启,无处不在,分析您舰队中每台机器上的整个系统。启用Prodfiler/EUP可以深入了解整个舰队中哪些代码行消耗了CPU资源,无论是在用户空间、内核、本地代码还是某些高级运行时中。这非常棒,但当面对舰队中最昂贵的10个或100个函数时,问题出现了:这些函数如此昂贵是“正常”的吗?如果不是,为什么它们如此昂贵,可以采取什么措施来解决问题?
如果您幸运,昂贵的函数在您编写和理解的代码中,这些问题可能很容易回答。如果您不幸,它可能在某些您不熟悉的第三方库中,用您不知道的语言编写,并且在您从未听说过的运行时之上。我们的用户常见的一个问题是,除了昂贵函数的列表,我们是否还可以建议根本原因,或者更好的修复方法。正是从这里,我们需要某种补充分析,可以“推理”为什么特定代码块可能昂贵,并在可能的情况下建议使其不那么昂贵的方法。
从构建错误查找和利用生成工具中我学到的一个教训是,工具的甜蜜点通常包括:
- 大规模、快速的动态分析,涉及软件/系统的执行,并在大量多样化输入下观察其行为。
- 自动静态分析,可以增强或被动态分析增强。
- 精心设计的UI/UX,即使在存在大量数据的情况下也能支持探索性工作流程。UI应汇集所有可用上下文,同时支持过滤、切片和搜索,几乎没有可感知的延迟。
在Prodfiler中,我们有1和3,但2在性能世界中相对未被探索,特别是在与持续性能分析结合的背景下。
我相信,就像安全一样,性能分析的甜蜜点在于1-3点的交集,动态和静态分析相互受益,直观且高性能的UI改变游戏规则。在本文中,我将专注于等式的静态分析部分。如果您想了解更多关于如何构建高性能持续性能分析引擎的信息,我们在Elastic博客上有关于这个主题的初始博客文章,未来还会有更多。
结合动态和静态分析的模式
一般来说,在程序分析研发中,没有硬性规定什么有效什么无效,这是一个在学术界和工业界都有大量持续工作的领域。也不是说一个人可能坚持结合一种静态分析和一种动态分析。您甚至可能决定完全跳过一种分析形式,并链接一堆 exclusively 动态分析,反之亦然。例如,为了错误查找的目的,可以对代码库中的每个函数应用一个过度近似但快速的基于抽象解释的分析,以找到潜在的错误。然后,潜在错误的函数可以传递给符号执行引擎,以在过程内上下文中验证错误。最后,可以使用模糊测试器尝试找到到达那些错误函数的路径,这些路径执行触发错误所需的条件。有关具体示例,请查看Sys,它结合了静态分析和符号执行,以利用各自的优势在Web浏览器中查找错误[代码][论文和演讲]。
在性能优化和分析方面,我可以想到三种操作模式,其中多种分析可能相互受益:
-
上下文用于排名:动态分析提供系统中最昂贵部分的基本事实,理想情况下是在生产环境中执行真实工作负载时。然后,此上下文可用于对静态分析的结果进行排名。排名很重要,原因有两个。首先,开发人员的时间有限。具有误报的近似分析需要开发人员验证每个结果。如果我们希望开发人员实际使用该工具,那么我们需要某种方式确保他们的努力在时间或精力耗尽之前应用于最有影响力的发现。其次,旨在查找性能问题的静态分析工具的发现是否有效,可能取决于相关代码是否在热路径上。静态分析的结果本质上是不完整的,除非结合指示哪些代码在生产工作负载上热的上下文。上下文用于排名的一个示例可能是生产中的持续CPU分析,用于对列出程序中编译器未能自动向量化的循环的分析输出进行排名。如果一个循环在仅在启动时执行一次的函数中未向量化,那么它可能无关紧要,但持续分析为您提供了一种过滤掉这种噪声并专注于重要循环的方法。
-
排名用于分析:轻量级动态分析提供系统中最昂贵部分的信息,以便可以集中精力进行重量级分析(或者,可以在这些区域进行更多轻量级分析)。一些分析运行起来是CPU/RAM密集型的。一个例子是符号执行。如果我们在性能优化的背景下使用这样的分析或类似重量的东西,那么我们希望使用其他数据源来指示哪些函数最昂贵,并将我们的努力集中在那里。
-
上下文用于操作:一种分析提供另一种分析操作所需的数据。这里的例子包括配置文件引导优化/AutoFDO或像Facebook的BOLT这样的链接后时间优化器。对于这些,分析信息是它们操作所必需的。我将在下一节中给出另一种此类分析的例子,它非常简单但有效。
低垂、美味、果实
本系列的第二篇文章,讨论使用CodeQL查找优化机会,属于“值得进一步调查的知识上有趣的研究”,但仍然相当早期。为了给象牙塔思维提供一些实用建议,这里有一个简单、明显且非常有效的分析组合,已反复为我们的客户在各种系统中带来胜利。
结合静态和动态分析进行性能优化的最直接成果是仅仅对应用程序性能配置文件中最重要的库和函数进行模式匹配,并且我们知道存在功能等效的、优化的版本可用。我们有真实世界的数据,客户通过这种方法获得了巨大的性能胜利。在Prodfiler中,这可以通过查看TopN视图来完成,该视图对您整个基础设施中最昂贵的N个函数进行排名。
我们遇到的一些具体例子:
-
如果大量时间花费在与内存分配相关的函数上(例如malloc、free、realloc等),并且应用程序使用标准系统分配器,那么通过切换到更优化的分配器(如mimalloc或jemalloc)可能会获得性能改进。这方面的一个例子在这篇博客文章中描述,我发现一个应用程序花费10%的时间处理内存分配。用mimalloc替换系统分配器将这段时间减半,并将应用程序的整体性能提高了约10%。Elastic的一个内部团队遇到了类似的模式,并通过将分配器换为jemalloc,除了减少应用程序的CPU开销外,还报告RAM使用量减少了20%。
-
如果大量时间花费在zlib上,那么切换到zlib-ng可以带来改进。我们的一个客户使用此更改将应用程序性能提高了30%。
-
如果大量时间花费在处理JSON或序列化和反序列化上,那么执行此操作的第三方库的性能存在巨大差异。通常,运行时提供的版本或最流行的版本可能比最先进的性能差得多。我们的一个客户通过将他们使用的JSON编码器替换为orjson,在他们最重的工作负载之一中减少了60%。
这个过程很简单,并且具有出色的努力回报比——查看结果,谷歌搜索“优化库X”,确保它满足您的功能和非功能需求,替换,重新部署,测量,继续。如果您开始结合分析,那么从这里开始。这可以轻松自动化并与您选择的通知框架集成。实际上有几种方法可以实现这种“grep for libraries”解决方案。一种是编写一个脚本,将您的分析器的Top N输出中的函数和/或库名称与已知可替换库的列表进行比较。这将是一个上下文用于操作的分析组合的例子,分析为regex/grep脚本的运行提供上下文。或者,您可以有一个工具扫描所有基础容器映像中的库,对于这些库,您知道存在更快的替代品。将此信息作为警报发送给开发人员只会惹恼他们,因为谁知道这些库是否甚至加载,更不用说在热路径上有代码了。然而,结合持续分析器,您可以过滤掉这种噪声,按它们在整个舰队中的影响对结果进行排名,然后仅在替换库可能带来显著收益的情况下分派警报(上下文用于排名的一个例子)。
结论
我希望上述内容有助于建立一个思考分析组合方式的框架。如果您是SRE/SWE,并在您的环境中集成了持续分析/grep-to-win方法,我很想听听它的效果如何。这个领域有很多研发空间,所以如果您有任何有趣且有用的分析例子,请在Twitter上给我发DM。我将在本周晚些时候跟进本系列的第二部分,该部分将讨论使用CodeQL查找循环向量化机会。