dot 快速的未来即将在您所在的城市举行活动。

加入我们在 Redis 发布会

优化 Redis 的默认编译器标志

Redis 和英特尔联手发现,是否应用更激进的优化选项可以提高 Redis 的整体基线性能。我们的结论是:是的!通过更改编译器行为,我们在某些情况下测得整体性能提升了 5.13%,甚至更高。

Redis 假设在支持的操作系统发行版上使用默认的 GCC 编译器,原因是可移植性和易用性。自项目初期以来就存在的构建编译器标志在多年来一直保持相当保守,默认优化级别为 -O2。这些标志效果很好。它们提供一致的结果和良好的性能,而不会显著增加 Redis 服务器二进制文件的编译时间或代码大小。没有人愿意在编译上花费大量时间!

但是,我们能做得更好吗?我们是优化专家。我们总是问这个问题!

在这篇文章中,我们描述了我们对更改编译器版本(GCC 9.4、GCC 11 和 Clang-14)和编译器标志优化对 Redis 性能的影响的分析。我们使用性能自动化来评估编译器和标志的性能影响,正如我们在 介绍 Redis/英特尔基准规范,用于性能测试、分析和分析 中讨论的那样。由于这项工作,我们更新了 Redis 的默认编译器标志,因为它们可以确保更好的性能。

编译器和编译器标志的回顾

让我们比较两个流行的开源编译器:GNU 编译器 (GCC) 和 Clang/LLVM。

GCC

GCC 是一款经典的优化编译器。它以 C/C++ 编译器而闻名,但也具有其他语言的前端,包括 Fortran、Objective-C 和 Java。GCC 是 GNU 工具链的关键组件,它在 Linux 内核开发中发挥着重要作用,与 makeglibcgdb 等一起。

GCC 是许多类 Unix 操作系统的默认编译器,包括大多数 Linux 发行版。作为开源产品,GCC 由许多人和组织开发,英特尔就是其中之一。

在我们的实验中,我们选择了两个 GCC 版本

  • GCC 9.4.0:Ubuntu 20.04 中的默认版本
  • GCC 11:测试时 Ubuntu 20.04 上可用的最新主要版本

Clang

Clang/LLVM,或简称为 Clang 编译器,是 Clang 前端和 LLVM 后端的组合。Clang 将源代码转换为 LLVM 字节码,LLVM 框架执行代码生成和优化。Clang 与 GCC 兼容,定位为快速编译、低内存使用,并且使用非常友好,具有表达性强的诊断信息。目前,Clang 是 Google Chrome、FreeBSD 和 Apple macOS 的默认编译器。

Clang 的强大功能主要在于 LLVM 社区,因为许多 IT 公司和个人开发人员都参与其中。特别是,英特尔开发人员是积极的社区贡献者。英特尔 ICX 编译器基于 LLVM 后端,英特尔将对 LLVM 的增强功能贡献回社区。

为了进行性能测试,我们选择了实验时可用的最新主要版本,Clang 14

编译器标志

编译器拥有数百个配置设置和标志,开发人员可以切换这些设置和标志来控制编译器如何运行以及它生成的代码。这些会影响性能优化、代码大小、错误检查以及发出的诊断信息。虽然通常会复制和粘贴默认设置,但调整它们可以带来改变。很大的改变。

-O2 是一种基本的性能优化,它可以提高性能和编译时间。 -O2 优化了字符串处理(-foptimize-strlen),包括简单的循环优化(例如,-floops-align-ffinite-loops)以及部分内联(-fpartial-inlining)。这种优化级别包括对树上的循环和基本块进行向量化,并且具有非常便宜的成本模型(very-cheap 允许在向量代码可以完全替换被向量化的标量代码的情况下进行向量化)。关于 -O2 中的成本模型和其他优化标志的更多信息,可以在 编译器的优化标志列表 中找到。

-O2 一直是 Redis 源代码的默认优化级别。传统上,我们使用 -O2 作为我们的基线。在项目中,我们将其与更激进的优化选项进行了比较。

-O3 是一种更激进的编译器优化标志。它包含所有 -O2 优化行为,以及额外的循环优化,例如循环展开 (-floop-unroll-and-jam)。它还会在其中一部分始终为真而另一部分为假时拆分循环 (-fsplit-loops)。在 -O3 中,非常便宜的成本模型被更准确、更动态的成本模型替换,该模型具有额外的运行时检查。通过这种更改,整个应用程序可以更快,因为编译器定义了哪些部分的代码速度慢(例如,速度慢的标量循环)并对其进行了优化,因此那些本来就很好的部分现在变得更好。

这些 -O3 标志并不局限于这些选项。有关完整列表,请参见 控制优化的编译器选项

需要记住一些事情

  • 使用 -O3 标志时,编译器执行一组应该导致性能提升的转换,但不能保证 100% 成功。结果取决于具体的代码和输入数据。(所有事情不都是这样吗?)
  • -O3 是构建一些 Redis 依赖项目 的默认编译级别。

-flto 标志代表链接时间优化 (LTO)。这种优化由编译器在链接应用程序代码时执行。如果没有这个选项,每个文件将分别进行优化,然后链接在一起。使用 LTO,文件首先连接,然后进行优化;这可以提高优化质量。

当应用程序文件之间有大量连接时,此选项非常有用。例如,假设你在一个文件中定义了一个函数。你可能在其他文件中使用或不使用该函数。编译器的链接器在构建可执行文件时使用此知识。使用过的函数会被内联(这会使应用程序运行得更快);未使用过的函数会被从生成的二进制文件中排除。

LTO 有助于消除死代码以及始终为 TRUEFALSE 的条件(“巧克力好吃吗?废话!”)。当使用 LTO 编译选项时,代码中使用的全局变量也会被内联。

所有这些更改都对生成的二进制文件的执行时间产生了积极的影响。或者用英语来说:我们让 Redis 运行得更快。

实验方法

我们使用 性能自动化框架 进行实验,该框架包含 50 个测试用例。我们的目标是实现对 Redis 使用模型的良好覆盖,并确保我们不会对重要用例的性能造成负面影响。

使用自动化框架,Redis 和英特尔的联合团队创建了多个构建变体来表示编译器(gcc v9.4、gcc v11 和 clang v14)和编译器标志的组合以进行评估。除了 Redis 之外,我们还通过在构建阶段使用 REDIS_FLAGSFLAGS 选项,尝试修改 Redis 依赖项(例如,jemalloc 和 lua)的编译器和标志。总而言之,这些变化为我们提供了 24 个不同的二进制文件或构建变体。

为了评估每个构建变体的有效性,我们使用特定构建变体(例如,Clang 14 + “O3”)构建的 Redis 服务器运行所有 50 个测试用例。对于这些测试,我们计算了 几何平均值(或仅为 geomean)在三次重复运行中,并计算三次运行的 平均值。这个最终数字(以每秒操作次数 (ops/sec) 表示)表明了构建配置的性能。提供每个编译器和选项对的这些步骤,我们获得了一组数字进行比较。我们在下面显示了它们与基线之间的差异百分比。

我们在四台基于英特尔至强铂金 8360Y 处理器的服务器上进行了这些测试。

intel xeon scalable chart
图 1:硬件设置

我们的发现……以及它们的意义

图 2 总结了 我们的实验结果。作为基线,我们使用 GCC 9.4 编译 Redis,并使用默认优化标志。我们根据所有运行的平均操作数/秒以及 50 个测试用例的几何平均值来判断成功,这些用例以由该构建变体或其他构建变体构建的 redis-server 为基础。每秒操作数越多越好,表明 Redis 服务器速度更快。

总体而言,GCC 9.4 O3 + flto 相对于基线提供了 5.19%(包含依赖项)和 5.13%(不包含依赖项)的几何平均加速。

图 2:50 个用例的几何平均值,归一化为基线(使用默认优化标志的 GCC 9.4)

编译器和标志的影响在某些用例中更为明显(参见图 3)。例如,使用 GCC 9.4 -O3 -flto,与基线相比没有性能下降,并且有四个测试提高了 10% 以上。

换句话说,结果差异很大,这表明更改 Redis 优化标志可以显着影响性能。

在其他配置中,一些测试显示出比基线更差的性能。然而,一些测试的性能比基线提高了 20% 以上。这是因为 O3 标志启用了许多积极的优化技术来提高效率。结果是编译器可以重新排序指令并对代码进行其他更改。虽然这些优化通常有利,但它们也可能导致代码在某些情况下运行速度变慢,尤其是在引入额外开销或使代码不太友好的情况下。

intel test changes graph
图 3. 测试的更改百分比分布,跨 50 个用例归一化为基线(使用默认优化标志的 GCC 9.4)。对于每个测试,我们捕获了三个测试运行中观察到的最小结果。

简而言之,更改这些标志会影响 OSS Redis 的执行速度。

根据该实验的结果,Redis 核心团队批准了我们提出的将默认标志更新为 -O3 -flto 的建议(PR 11207)。此配置在所有测量用例中显示出 5.13% 的几何平均提升,并且没有测试显示性能下降。

我们的结论和计划

我们对编译器调优的工作并未到此结束。我们还有机会让 Redis 运行得更快。例如:

  • 配置文件引导优化 (PGO):使用 PGO,编译器会从不同的执行中收集运行时配置文件。运行时配置文件包含信息,例如关于分支是否被执行的统计信息;循环计数;代码块执行频率;等等。使用这些运行时信息,编译器可以为常见的运行时用例生成更好的代码。
  • 安全性:最近的编译器版本包含生成更安全代码的标志。我们希望进行实验,研究将这些标志融入最佳方式。例如
  • 英特尔编译器:英特尔正在使用我们在这些实验中收集到的经验教训,并将这些经验教训反馈给英特尔编译器开发团队。此外,还在 Redis 中实施了一些改进,以支持 ICC 编译(例如 PR 10708PR 10675),并且我们正在与 Redis 合作持续改进。

想看看这一切如何在软件中体现?尝试 免费使用 Redis 来探索其无数优势。