持续性能分析与库匹配带来60%+性能提升——动态与静态分析结合的优化实践

本文探讨如何结合动态与静态分析进行性能优化,通过持续性能剖析识别热点函数,结合库匹配技术实现显著性能提升,包含具体案例和实施框架。

60%+性能提升:持续性能剖析与库匹配——动态与静态分析结合的优化实践(第一部分)

这是关于结合静态和动态分析进行性能优化的两篇系列文章中的第一篇。我将它们分开,否则会变得异常冗长,第二篇将在本周晚些时候上线。本文提供了一些高级背景,讨论了为什么要结合分析,并提出了一个思考分析组合的松散框架。然后,我将解释一种将持续性能剖析与库名称和版本的模式匹配相结合的简单方法,该方法在实际客户基础设施中为每个服务带来了60%以上的性能提升。

在后续文章中,我将讨论去年使用CodeQL在C/C++代码库中寻找性能优化机会的实验工作。CodeQL通常用于发现安全漏洞,但我将展示它如何用于查找可能导致编译器生成比可能慢10倍或更多的机器代码的C++代码模式。主要来说,这些模式涉及阻止编译器自动向量化循环的代码结构,通过一些小的类型更改或语句重新排序,我们可以实现显著的性能改进。

为什么要结合静态和动态分析?

几年前,我共同创立了optimyze.cloud,并构建了Prodfiler(现为Elastic Universal Profiler)——一个全舰队范围、持续、全系统的性能剖析器。也就是说,一个足够高效的性能剖析器,可以始终开启,无处不在,在您舰队中的每台机器上剖析整个系统。启用Prodfiler/EUP可以准确洞察在整个舰队中哪些代码行消耗了CPU资源,无论是在用户空间、内核、本地代码还是某些更高级的运行时中。这非常棒,但是,当面对舰队中最昂贵的10个或100个函数时,问题出现了:这些函数那么昂贵是"正常"的吗?如果不是,为什么它们如此昂贵,可以采取什么措施来解决问题?

如果您幸运,昂贵的函数在您编写和理解的代码中,这些问题可能很容易回答。如果您不幸,它位于您不熟悉的某个第三方库中,用您不知道的语言编写,并且建立在您从未听说过的运行时之上。我们的用户经常问的一个问题是,除了仅仅列出昂贵的函数外,我们是否还可以建议根本原因,或者甚至更好的修复方法。正是从这里,我们认识到需要某种补充分析,能够"推理"为什么特定代码块可能昂贵,并在可能的情况下建议使其减少昂贵的方法。

我从构建错误发现和漏洞利用生成工具中吸取的一个教训是,工具的甜蜜点通常包括:

  1. 大规模、快速的动态分析,涉及软件/系统的执行,并在大量多样化的输入集下观察其行为。
  2. 自动化的静态分析,可以增强动态分析或被动态分析增强。
  3. 精心设计的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的一个内部团队遇到了类似的模式,并且除了减少其应用程序的CPU开销外,还报告通过将其分配器换为jemalloc,RAM使用量下降了20%。

  • 如果大量时间花在zlib上,那么切换到zlib-ng可以带来改进。我们的一个客户使用此更改使应用程序性能提高了30%。

  • 如果大量时间花在处理JSON或序列化和反序列化上,那么执行此操作的第三方库的性能存在巨大差异。通常,运行时提供的版本或最流行的版本可能比最先进的性能差得多。我们的一个客户通过将其使用的JSON编码器替换为orjson,在其最重的工作负载之一中减少了60%。

这个过程很简单,并且具有极好的努力回报比——查看结果,谷歌搜索"optimised library X",确保它满足您的功能和非功能需求,替换,重新部署,测量,继续前进。如果您刚开始结合分析,那么从这里开始。这可以轻松自动化并与您选择的通知框架集成。实际上有几种方法可以实现这种"grep for libraries"解决方案。一种是编写一个脚本,将性能剖析器的Top N输出中的函数和/或库名称与已知可替换库的列表进行比较。这将是一个用于操作的上下文分析组合的例子,性能剖析为regex/grep脚本的运行提供了上下文。或者,您可以有一个工具扫描所有基础容器镜像中是否存在您知道存在更快替代品的库。将此信息作为警报发送给开发人员只会惹恼他们,因为谁知道这些库是否甚至被加载,更不用说在热路径上有代码了。然而,结合持续性能剖析器,您可以过滤掉此类噪音,根据它们在整个舰队中的影响对结果进行排名,然后仅在替换库可能带来显著增益的情况下分派警报(用于排名的上下文的一个例子)。

结论

我希望上述内容有助于建立一个思考分析组合方式的框架。如果您是SRE/SWE并在您的环境中集成了持续性能剖析/grep-to-win方法,我很想听听结果如何。这个领域有很多研发空间,所以如果您有任何有趣且有用的分析例子,请在Twitter上给我发DM。我将在本周晚些时候跟进本系列的第二部分,该部分将讨论使用CodeQL查找循环向量化机会。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计