60%+性能提升:持续性能分析与库匹配——动态与静态分析结合的性能优化(第一部分/共两部分)
日期:2023年2月14日 作者:seanhn
这是关于结合静态和动态分析进行性能优化的两篇系列文章中的第一篇。我将它们分开发布,否则文章会过长,第二篇将在本周晚些时候上线。本文介绍了一些高层背景,讨论了为什么要结合分析,并提供了一个思考分析组合的松散框架。然后我将解释持续性能分析与库名称和版本模式匹配的简单组合,这在真实客户基础设施中为每个服务带来了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博客上有关于此主题的初始博客文章,未来还会有更多。
结合动态和静态分析的模式
一般来说,在程序分析研发中,关于什么有效和什么无效没有硬性规定,这是一个在学术界和工业界都有大量正在进行工作的领域。也不一定坚持将一个静态分析与一个动态分析结合。您甚至可以决定完全跳过一种分析形式,并链接一堆专门动态的分析,反之亦然。例如,为了错误发现的目的,可以对代码库中的每个函数应用一个过度近似但快速的基于抽象解释的分析来发现潜在错误。然后可以将潜在有错误的函数传递给符号执行引擎以在过程内上下文中验证错误。最后,可以使用模糊测试器尝试找到到达那些有错误函数的路径,这些路径执行触发错误所需的条件。有关具体示例,请查看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库"解决方案。一种是编写一个脚本,将剖析器Top N输出中的函数和/或库名称与已知可替换库列表进行比较。这将是一个上下文用于操作的分析组合示例,剖析为regex/grep脚本的运行提供上下文。或者,您可以有一个工具扫描所有基础容器映像中是否存在您知道存在更快替代方案的库。将此信息作为警报发送给开发人员只会惹恼他们,因为谁知道这些库是否甚至被加载,更不用说在热路径上有代码了。然而,结合持续剖析器,您可以过滤掉此类噪声,按它们在整个舰队中的影响对结果进行排名,然后仅在替换库可能带来显著增益的情况下发送警报(上下文用于排名的一个示例)。
结论
我希望上述内容有助于建立一个思考分析组合方式的框架。如果您是SRE/SWE并在您的环境中集成持续剖析/grep-to-win方法,我很想听听结果如何。这个领域有很多研发空间,所以如果您有任何有趣且有用的分析示例,请在Twitter上给我发DM。我将在本周晚些时候跟进本系列的第二部分,该部分将讨论使用CodeQL查找循环向量化机会。