内核调优带来 40% 的性能提升

作者:Rick Gorton

API NetWorks 是一家领先的高性能、高密度服务器开发商。因此,我们(以及我们的客户)对系统性能问题非常敏感。由于我们的大多数客户都在基于 Alpha 的系统上运行 Linux,因此我们密切关注关键的开源组件和软件包,以确保我们不会无意中遗漏任何性能“潜力”。

在专有领域,软件产品发布后发现的改进会被保存到下一个版本中。在开源软件领域,改进之间的时间可以用小时或天来衡量。此外,调优技术会迅速共享,以增强社区知识库。考虑到这一点,我们检查了 Linux 内核中的汇编语言例程,着眼于利用最新的 Alpha 21264(也称为 ev6)处理器的微架构特性。

我们发现,通过重写 Linux 内核中特定于架构的部分中的少量例程,以利用 21264 的特性,我们可以获得显著的速度提升,在某些工作负载下可将开销降低 40%。本文介绍了这些优化工作原理以及原因,以便其他人能够使用这些技术来提升其 Alpha 应用程序的性能。例如,对于在基于 21264 的 Alpha 上运行的电子邮件服务器,应用这些调优技巧将使其能够扩展到更高的负载,从而将性能扩展到通常会逐渐下降的水平以上。最终用户会感觉他们的系统运行速度更快;系统管理员会感到高兴,因为他们不必很快购买另一台系统来处理增加的流量。

在 Linux 内核的情况下,性能关键的例程按架构仔细(且方便地)分离,并且大多以汇编语言实现。虽然有大量的开发人员不断努力改进内核的算法性能,但只有少数开发人员具备必要的兴趣、技能和知识,使他们能够调优这些汇编例程,从而最大限度地发挥处理器的性能。这些开发人员大多在为 x86 系统调优代码;相关的 Alpha 代码库在我们的调优之前已经沉寂了相当长一段时间。

轶事证据和之前的测量表明,当运行各种 Web 和网络服务器时,Linux/Alpha 内核内部花费的时间不成比例地多。鉴于 Linux 内核中已知的性能关键例程数量相对较少(20-30 个汇编语言例程),因此对它们进行排序以确保代码编写时考虑了 21264 是有意义的。快速阅读这些例程后发现,它们是为上一代 Alpha CPU 21164(也称为 ev5)精心手工调度的。

虽然差异可能看起来很小,但 21264 在微架构(芯片实现)级别上与 21164 存在显著差异,其特性使其在相同的时钟速度下性能大约是 21164 的两倍。

在详细介绍性能改进之前,了解 21164 和 21264 处理器之间的一些差异(见表 1)是有用的。

表 1. Alpha 21164 和 21264 处理器比较

在意识到仔细的重写可以产生显著的性能提升后,我们着手调优 Linux 内核中的 Alpha 汇编语言例程。(已经有人尝试动态和自动地解决在新 Alpha CPU 上高效运行为旧版本 Alpha CPU 编译的二进制文件的问题1,但这超出了本文的范围。)具体而言,很明显,linux/arch/alpha/lib 中大约 20 个例程和 linux/include/asm-alpha 中 4 到 6 个例程需要重写,以充分利用 21264 的特性。

改进来源

从内部角度来看,21264 是一款比旧款 21164 更复杂、更强大的 CPU。由于这些差异,性能改进的性质可能来自转换(如下所述),包括重新调度代码以防止处理器在获取时停顿、避免分支和分支目标惩罚、尽可能避免重放陷阱的发生、使用 21264 的指令延迟和调度规则、使用 21164 和 21264 中可用但未使用的指令、使用 21264 中新提供的指令,以及尽可能避免在 21264 上使用某些指令。

防止获取停顿

在编写代码以避免获取停顿时,必须确保在获取块中动态遇到的指令不会导致一个或多个功能单元过度订阅。在 21264 上,确保分支目标与获取块边界对齐也很重要。鉴于它一次获取四个指令,分支目标应具有 0mod4 指令的对齐方式,相当于 0mod16 的地址对齐方式。

避免分支惩罚

在 21264 上避免分支惩罚尤其重要。内置了复杂的、可训练的分支预测逻辑,并且如果一个获取块(一个“四指令包”)中只有一个控制流更改指令,则可以有效地工作。在 21164 调优的内核汇编语言例程中,有许多地方在一个四指令包中出现多个控制流更改指令。此外,分支目标与 8mod16 地址对齐,这通常导致分支目标标签出现在四指令包的中间。虽然这些序列在 21164 上运行良好,但在 21264 上运行相对较慢。

避免重放陷阱

当处理器必须回滚内存状态以强制按顺序访问特定内存位置,或者当对同一内存位置进行不同大小的访问时,会发生重放陷阱。但是,修改后的例程中的代码上下文使得重放陷阱不是问题,因此重写序列以避免重放陷阱是不必要的。

21264 的指令延迟

21264 的指令调度和槽位规则过于复杂,无法在此处列出,但对于那些对细节感兴趣的人,21264 编译器编写者指南是一个极好的参考。

可用但未使用的指令

字节和字大小的加载和存储在 21164A (ev56) 处理器中引入,但它们未在汇编语言例程的原始版本中使用。先前的经验(在应用程序的静态二进制转换的上下文中)表明,通过利用这些指令,性能通常可以提高 10% 到 20%。对于 stw(存储字)和 stb(存储字节)指令尤其如此,因为它以保证在 21264 上引起重放陷阱的方式消除了内存流量。在调优后的内核例程的上下文中,这些指令很有帮助,但通常局限于大型区域复制的尾部代码,而大部分数据移动使用 8 字节粒度的加载和存储指令。

Alpha 架构还具有各种形式的预取指令。预取指令是向内存子系统发出的提示,用于将内存块提取到数据缓存以供将来使用。这些指令通常不会出现在编译后的代码中,因为很少有编译器有足够的上下文来允许它们生成;Compaq 的编译器确实会生成预取指令。在移动大量数据的上下文中,汇编语言程序员可以(并且希望)利用预取。gcc 的 __asm__() 功能使程序员能够在不希望以汇编语言重写整个例程时,在例程中的关键点插入相关的预取指令。由于它们可以最大限度地减少或防止数据缓存停顿,因此使用这些指令可以显著提高性能。

新的可用指令

21264 是第一个包含对三个有助于提高性能的指令的支持的 Alpha 实现:CTLZ、CTTZ 和 WH64。

CTLZ 和 CTTZ 指令计算 64 位寄存器中前导/尾随零的数量,并且便于字符串操作。当程序执行涉及模式匹配的字符串操作(strlen() 匹配 NULL)时,通常需要寄存器中 8 字节值中模式匹配的字节数索引。如果没有 CTTZ,则需要大约十条指令,包括多个 CMOVxx(条件移动)指令来确定此索引。结果是代码大小的减小(始终有用),以及执行字符串操作所需的周期数减少。此外,还有一些文件系统原语涉及在位域中查找空洞,这些指令在其中很有用。

WH64(64 字节写入提示)是内存子系统提示,指示指定的 64 字节区域将在不久的将来被写入。处理器可以将此信息传递给内存子系统,内存子系统可以使目标内容无效,并避免一些内存系统周期来保持内存状态一致。由于进程上下文切换需要将大量内存信息从一个位置移动到另一个位置,因此内核空间内存和用户空间内存之间复制性能的任何提高都是好消息。同时,程序加载时间是操作系统中另一个依赖于执行大量内存到内存流量的地方。程序位都必须映射,并且所有清零的内存(可执行文件中的 .bss)都必须写入零。

要避免的指令

21264 上的 CMOVxx 条件移动指令已在硬件中实现,方法是将它们分解为处理器内部的两个单独指令。CMOVxx 指令的延迟结果至少为两个周期,最多可能为五个周期,具体取决于给定获取块中 CMOV 的数量。在某些情况下,用高度可预测的条件分支替换 CMOV 指令可以提高 21264 的性能。总的来说,一个好的经验法则是尽可能尝试最大限度地减少 CMOV 指令的数量。

数据收集和测试方法

数据是从 API NetWorks 的 CS20 服务器收集的,该服务器具有双 833MHz 处理器,配备 4MB DDR 缓存、1GB SDRAM 和 Ultra-160 SCSI 磁盘。运行了两个负载生成测试:五个 2.2.18 内核构建和五个 gcc-2.95.3 构建。使用各种并行级别的 make 记录了平均系统时间(由 /usr/bin/time -p 报告)(参见表 2 和表 3)。

表 2. 2.2.18 内核的负载生成测试结果

表 3. gcc-2.95.3 内核的负载生成测试结果

使用默认模式(所有性能补丁都存在)的 2.4.2 内核运行了类似的实验。将结果与未打补丁的 2.4.2 内核(其中大多数(但并非全部)性能更改已恢复)进行了比较。

表 4. 默认 2.4.2 内核的负载生成测试结果

表 5. gcc-2.4.2 内核的负载生成测试结果

该实验最初是在 API NetWorks 的 UP1000 主板系统上进行的,该系统具有 700MHz 处理器,配备 4MB 缓存、128MB SDRAM 和 IDE 磁盘。同样,运行了五个内核和 gcc 构建,并记录了平均时间。使用的内核是 2.4.0-test6,带有和不带有补丁。

表 6. UP1000 系统运行 2.4.0 内核的测试结果

表 7. UP1000 系统运行 gcc-2.4.0 内核的测试结果

在配置适中的 21664 系统 (UP1000) 上,性能提升在减少内核中花费的时间方面非常显著,对于某些活动(内核构建),改进幅度在 40% 范围内。在配置更慷慨的 CS20 上,对于测量的负载,我们始终如一地获得了 14-15% 的速度提升。

我们将 UP1000 和 CS20 系统之间的差异归因于它们的内存:UP1000 具有 800MB/秒、64 位总线,而 CS20 具有 2.65GB/秒、256 位总线。

总结

所有重写的例程都以一种或另一种形式(有些经过了后续重写)出现在 2.4.2 内核中。此外,我们还为内核 2.2.17 整理了一个补丁,并在我们的公司网站 http://www.api-networks.com/products/downloads/developer_support/ 的“Performance”下提供。通过额外的努力,这些改进也已迁移到 glibc 中,最终将有助于提高用户模式代码的应用程序性能。

Kernel Tuning Gives 40% Gains
Rick Gorton,API NetWorks 的技术人员,最初以 Alpha (BLADE) 的原始 32 位端口的形式接触到 Linux。自 1992 年以来,他一直在为 Alpha 开发二进制翻译器、优化器和其他二进制操作工具。可以通过 rick.gorton@api-networks.com 与他联系。
加载 Disqus 评论