动态与静态分析结合实现60%+性能提升:持续性能分析与库匹配实战

本文探讨如何结合动态与静态分析进行性能优化,通过持续性能分析识别热点函数,并利用库匹配实现显著性能提升。内容包括分析框架、实际案例及自动化实现方法,帮助开发者在生产环境中快速定位并解决性能瓶颈。

60%+性能提升:持续性能分析与库匹配实战——动态与静态分析结合优化性能(第1/2部分)

引言

这是关于结合静态和动态分析进行性能优化的两篇系列文章中的第一篇。本文将介绍高层背景,讨论为何要结合分析,并提供一个思考分析组合的松散框架。随后,我将解释一种简单的组合方法:通过持续性能分析与库名和版本的模式匹配,在实际客户基础设施中实现每个服务60%以上的性能提升。

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

为何结合静态和动态分析?

几年前,我共同创立了optimyze.cloud,并构建了Prodfiler(现为Elastic Universal Profiler)——一个全舰队范围、持续、全系统性能分析器。Prodfiler/EUP可以高效地始终开启,无处不在,分析您舰队中每台机器上的整个系统。启用Prodfiler/EUP可以精确洞察整个舰队中消耗CPU资源的代码行,无论是在用户空间、内核、本地代码还是某些高级运行时中。

这非常棒,但当面对舰队中最昂贵的10个或100个函数时,问题出现了:这些函数如此昂贵是“正常”的吗?如果不是,为什么它们如此昂贵,以及可以采取什么措施来解决问题?

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

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

  1. 大规模、快速的动态分析,涉及软件/系统的执行,并在大量多样化输入下观察其行为。
  2. 自动静态分析,可以增强动态分析或被动态分析增强。
  3. 精心设计的UI/UX,即使在大量数据存在的情况下,也能支持探索性工作流程。UI应汇集所有可用上下文,同时支持过滤、切片和搜索,几乎没有可感知的延迟。

在Prodfiler中,我们拥有1和3,但2在性能世界中相对未被探索,特别是在与持续性能分析结合的背景下。

我相信,就像安全一样,性能分析的甜蜜点在于1-3点的交集,动态和静态分析相互受益,直观且高性能的UI改变游戏规则。在本文中,我将专注于等式的静态分析部分。如果您想了解更多关于如何构建高性能持续性能分析引擎的信息,我们在Elastic博客上有一篇关于此主题的初始博客文章,未来还会有更多。

结合动态和静态分析的模式

在程序分析研发中,通常没有硬性规则规定什么有效什么无效,这是一个在学术界和工业界都有大量持续工作的领域。也不一定坚持将一种静态分析与一种动态分析结合。您甚至可能决定完全跳过一种分析形式,并链式执行一系列纯动态分析,反之亦然。例如,为了错误发现的目的,可以对代码库中的每个函数应用过度近似但快速的基于抽象解释的分析,以发现潜在错误。然后,潜在错误的函数可以传递给符号执行引擎,在过程内上下文中验证错误。最后,可以使用模糊测试器尝试找到到达那些错误函数的路径,这些路径执行触发错误所需的条件。有关具体示例,请查看Sys,它结合静态分析和符号执行,利用各自的优势在Web浏览器中发现错误[代码][论文和演讲]。

在性能优化和分析方面,我可以想到三种操作模式,其中多种分析可能相互受益:

  1. 上下文用于排名:动态分析提供系统中最昂贵部分的基本事实,理想情况下是在生产环境中执行真实工作负载时。然后,此上下文可用于对静态分析的结果进行排名。排名重要有两个原因。首先,开发人员的时间有限。具有误报的近似分析需要开发人员验证每个结果。如果我们希望开发人员实际使用该工具,那么我们需要某种方式确保他们的努力在时间或精力耗尽之前应用于最有影响力的发现。其次,旨在发现性能问题的静态分析工具的发现是否有效可能取决于相关代码是否在热路径上。静态分析的结果本质上是不完整的,除非结合指示生产工作负载中哪些代码是热的上下文。上下文用于排名的示例可能是使用生产中的持续CPU分析来对列出程序中编译器未能自动向量化的循环的分析输出进行排名。如果循环在仅在启动时执行一次的函数中未向量化,则可能无关紧要,但持续分析为您提供了一种过滤掉此类噪声并专注于重要循环的方法。

  2. 排名用于分析:轻量级动态分析提供系统中最昂贵部分的信息,以便可以集中精力进行重量级分析(或者 alternatively,可以在这些区域进行更多轻量级分析)。一些分析运行CPU/RAM密集型。这方面的示例是符号执行。如果我们在性能优化的背景下使用此类分析或类似重量的分析,我们会希望使用其他数据源来指示哪些函数最昂贵,并将精力集中在那里。

  3. 上下文用于操作:一种分析提供另一种分析操作所需的数据。这里的示例是配置文件引导优化/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输出中的函数和/或库名称与已知可替换库列表进行比较。这将是一个上下文用于操作的分析组合示例,分析提供上下文供正则表达式/grep脚本运行。或者,您可以有一个工具扫描所有基础容器映像中的库,对于这些库,您知道存在更快的替代方案。将此信息作为警报发送给开发人员只会惹恼他们,因为谁知道这些库是否甚至被加载,更不用说在热路径上有代码。然而,结合持续分析器,您可以过滤掉此类噪声,按它们在整个舰队中的影响对结果进行排名,然后仅在替换库可能带来显著增益的情况下分派警报(上下文用于排名的示例)。

结论

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

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