Linux 和 Alpha

作者:David Mosberger

自从 Alpha 架构(参见参考文献 3)在 1991 年秋季发布以来,它一直是世界上最快速系统的基础。事实上,除了短暂的一两次例外,Alpha 系统一直是基于单 CPU SPECmark 性能的最高性能系统。伴随着卓越的性能记录而来的是市场炒作,有时还有不切实际的期望。发现电子邮件或 USENET 文章说这样的话并不罕见:“我听说 Alpha 非常快,但现在我发现我的旧机器在 Alpha 上只比在其他系统上快 10%。”那么真相是什么呢?诚实的答案是这取决于您正在做什么。毫无疑问,Alpha 系统是快速的机器,但期望拿一台旧机器并在 Alpha 上运行就能获得最佳性能是不合理的。对于那些在 80 年代以 CPU 周期为稀缺资源而内存带宽充裕的思维模式下编写的程序来说,尤其如此。今天的现实看起来截然不同:CPU 时钟频率高于 150MHz 已成常态,甚至笔记本电脑也可以以 200MHz 或更高的频率运行。结果是,今天,内存系统——而不是 CPU——通常是首要瓶颈。

在本文的第 2 部分中,我们将演示一些简单的技术,这些技术有助于避免内存系统瓶颈。除了一种情况外,重点是整数密集型应用程序。优化浮点密集型应用程序的主题当然也同样重要,但不幸的是,远远超出了本系列的范围。所介绍的技术可以带来巨大的性能提升。虽然这些技术对所有现代系统都有帮助,但它们通常在基于 Alpha 的机器上提取最大的好处。造成这种偏差有两个原因。

首先,Alpha 架构的设计考虑了长寿命。具体而言,Alpha 架构在未来 15-25 年内应该仍然适用,这大致相当于总体性能提高 1000 倍。因此,一些设计权衡倾向于长期可行性而不是短期利益。例如,Alpha 从一开始就是一个 64 位架构,即使在发布时,32 位地址空间也被认为足够大。

其次,当前的 Alpha 实现旨在通过将时钟频率推向极限来获得高性能。这意味着对于基于 Alpha 的系统,CPU 到内存系统的性能差距最大。例如,假设内存访问需要 100 纳秒。在 500MHz Alpha CPU 上,这相当于 50 个时钟周期。相比之下,在 250MHz CPU 上,这仅为 25 个周期。因此,在时钟速度更高的 CPU 上,访问内存的相对性能损失要高得多。这听起来可能是一件坏事,但由于绝对性能是相同的,这实际上意味着运行内存密集型应用程序的快速时钟 CPU 系统将与较慢时钟的系统一样快,但是当运行内存明智型应用程序时,它会快得多。

在本系列的这一部分中,我将简要概述现有和即将推出的 Alpha 实现。虽然通常没有必要针对特定的 CPU 进行优化,但了解当前 CPU 和系统的特性是有帮助的。我还将讨论一些在 Linux 下可用的简单性能分析工具。当将遗留代码移植到现代系统时,这些工具非常宝贵,因为它们避免了浪费时间来优化很少执行的代码。

Alpha 家族概述

到目前为止,Alpha CPU 家族树跨越了三代;一切都始于 21064 芯片。在其推出之时,它是性能最高的 CPU,并且仍然可以作为不错的工作站,尽管它不再与最新一代 CPU 竞争。该芯片衍生出一个称为“低成本 Alpha”(LCA)的版本,也称为 21066 或 21068。芯片核心与 21064 相同,但它集成了内存和 PCI 总线控制器。这种高度集成使得以相对较低的成本为嵌入式系统市场构建基于 Alpha 的系统成为可能。不幸的是,该设计有一个主要的弱点——内存系统严重不足。这造成了一种自相矛盾的情况,即基于该芯片的系统在某些应用程序上的平均性能不比 100MHz 奔腾好多少,但性能优于以 200MHz 运行的 P6。因此,对该芯片的反应差异很大,并且可能导致 Digital 的许多客户感到失望。另一方面,毫无疑问,最终以低成本出售的基于 21066 的系统导致 Linux/Alpha 用户数量的量子级飞跃。

大约在 1994 年 6 月,发布了 21164 芯片。它比 21064 的性能有了显着提高,并且是第一个也是迄今为止唯一一个具有三级缓存层次结构的 Alpha CPU。一级和二级缓存都在芯片上,只有三级缓存在主板上。这种芯片,在略有改进的版本中,仍然表现强劲。在 1996 年秋季拉斯维加斯 Comdex 上,展示了这样一款芯片,它与液冷系统结合,以 767MHz 的频率运行。另一个版本,称为 21164PC,计划于 1997 年春季左右上市。它省略了相对昂贵的二级片上缓存,但增加了多媒体扩展和其他性能增强功能。顾名思义,这款芯片旨在与 PC 处理器(特别是即将推出的英特尔 Klamath(改进的 P6))在价格上具有竞争力。虽然价格具有竞争力,但 21164PC 应该比 Klamath 提供超过 50% 的性能提升。对于这款第二代低成本 Alpha 实现,Digital 及其联合设计师三菱似乎不会重蹈覆辙。21164PC 有望既便宜又快速。

如果您碰巧财力雄厚,或者想了解两三年后 PC 处理器的样子,那么 21264 可能会让您感兴趣。它计划于 1997 年下半年在高端机器中上市。借助这款芯片,CPU 性能有望再次实现巨大飞跃。目前的估计表明,性能水平将比当今可用的最快 CPU 快三到四倍。

在每个主要的芯片世代之间,通常都有“半代”CPU,它们的改进主要来自芯片制造工艺的缩小。例如,21064 芯片之后是 21064A,类似地,21164 之后是 21164A。在前一种情况下,芯片的核心与 21064 几乎相同,但主缓存的大小从 8KB 增加到 16KB。在后一种情况下,添加了用于字节和字访问的指令,并且最大时钟频率从 333MHz 提高到 500MHz。

表 1 总结了当前 Alpha 芯片系列的性能属性。

2394s1.ps (min=50, max=52)

表 1. Alpha 芯片家族概要

Linux 性能分析工具

Linux 性能分析工具的现状相当糟糕(这对于一般的免费软件来说是普遍现象,而不仅仅是 Linux)。商业产品目前在该领域具有优势。例如,Digital Unix 配备了一个出色的工具(或者更确切地说是一个工具生成器),称为 ATOM(参见参考文献 2)。ATOM 基本上是一个可以重写任何可执行文件的工具。在重写时,它可以向每个函数或基本块添加任意的检测代码。Digital Unix 配备了一系列使用 ATOM 构建的工具:3rd degree(一个内存泄漏和边界检查器,类似于著名的 purify)以及许多提供程序性能行为(例如缓存未命中频率、指令发出率等)非常详细信息的工具。目前,自由软件社区只能梦想拥有如此通用的工具。

虽然情况黯淡,但绝非没有希望。可用的少数工具在正确使用时可以成为强大的助手。即使是古老的 GNU gprof 也有一些您可能没有意识到的功能(稍后会详细介绍)。让我们从最基本的性能测量——时间开始。

精确测量时间

Unix 测量时间的方式是调用 gettimeofday()。这会返回当前实时时间,分辨率通常为一个定时器滴答(在 Alpha 上约为 1 毫秒)。此函数的优点是它在所有 Linux 平台上完全可移植。缺点是其分辨率相对较差(1 毫秒相当于 500MHz CPU 上的 500,000 个 CPU 周期),更严重的是,它涉及系统调用。系统调用相对较慢,并且有扰乱内存系统的趋势。例如,缓存会加载内核代码,以便当您的程序恢复执行时,它会看到许多在没有调用 gettimeofday() 的情况下不会看到的缓存未命中。这对于测量秒或分钟量级的时间是可以的,但对于更精细的测量,需要更好的方法。

幸运的是,大多数现代 CPU 都提供一个寄存器,该寄存器以 CPU 的时钟频率或其整数分数递增。Alpha 架构提供了 rpcc(读取处理器周期计数)指令。它允许访问一个 64 位寄存器,该寄存器在寄存器的低半部分包含一个 32 位计数器。此计数器每 N 个时钟周期递增一次。当前所有芯片都使用 N = 1,因此寄存器以全时钟频率递增。(未来可能会有 N > 1 的 Alpha 处理器)。rpcc 返回值的上半部分取决于操作系统。Linux 和 Digital UNIX 返回一个校正值,该值使得很容易实现一个仅在调用进程正在执行时运行的周期计数器(即,这允许您测量进程的虚拟周期计数)。使用 gcc,很容易编写 inline 函数来提供对周期计数器的访问。例如

static inline u_int realcc (void) {
    u_long cc;
    /* read the 64 bit process cycle
        counter into variable cc: */
    asm volatile("rpcc %0" : "=r"(cc)
                : : "memory");
    return cc; /* return the lower 32 bits */
    }
       static inline unsigned int virtcc (void) {
          u_long cc;
          asm volatile("rpcc %0" : "=r"(cc)
                        : : "memory");
          /* add process offset and count */
          return (cc + (cc<<32)) >> 32;
       }

有了这段代码,函数 realcc() 返回 32 位实时周期计数,而函数 virtcc() 返回 32 位虚拟周期计数(这类似于实时计数,只是当进程未运行时计数停止)。

调用这些函数的开销非常小。减速量级为每次调用 1-2 个周期,并且仅增加一两条指令(这小于函数调用的开销)。使用这些函数的一个好方法是创建执行时间直方图。例如,下面的函数测量对 sqrt (2.0) 的调用的单个执行时间,并将结果打印到标准输出(通常,必须注意确保编译器不会优化掉实际的计算)。打印单个执行时间使得很容易通过少量后处理创建直方图。

void measure_sqrt (void) {
          u_int start, stop, time[10];
          int i;
          double x = 2.0;
          for (i = 0; i < 10; ++i) {
             start = realcc();
             sqrt(x);
             stop = realcc();
             time[i] = stop - start;
          }
         for (i = 0; i < 10; ++i)
             printf(" %u", time[i]);
             printf(""n");
       }

请注意,结果是在一个单独的循环中打印的;这很重要,因为 printf 是一个相当大且复杂的函数,甚至可能导致一两个系统调用。如果 printf 是主循环的一部分,则结果将不太可靠。上述代码的示例运行可能会产生如下输出

120  101  101  101  101  101  101  101  101  101
由于此输出是在 333MHz Alpha 上获得的,因此 120 个周期对应于 36 纳秒,而 101 个周期对应于 30 纳秒。输出很好地显示了第一次调用是如何慢得多的,因为此时内存系统(特别是指令缓存)是冷的。由于平方根函数足够小,可以轻松放入一级指令缓存,因此除第一次调用外的所有调用都以完全相同的时间执行。

您可能想知道为什么上面的代码使用 realcc() 而不是 virtcc()。原因很简单——我们想知道受上下文切换影响的结果。通过使用 realcc(),遭受上下文切换的调用将比任何其他调用慢得多。这使得很容易识别和丢弃此类不需要的异常统计数据。

周期计数器提供了一种非常低开销的测量单个时钟周期的方法。缺点是它无法测量非常长的时间间隔。在以 500MHz 运行的 Alpha 芯片上,32 位周期计数器在短短八秒半后就会溢出。这在进行精细测量时通常不是问题,但重要的是要记住这个限制。

性能计数器

与大多数其他现代 CPU 一样,Alpha 芯片提供了各种性能计数器。这些计数器允许测量各种事件计数或速率,例如缓存未命中数、指令发出率、分支预测错误或指令频率。不幸的是,我不知道任何 Linux API 可以提供对这些计数器的访问。这尤其令人遗憾,因为奔腾和奔腾 Pro 芯片都提供了类似的计数器。Digital UNIX 通过 uprofilekprofile 程序以及 pfm(7) 手册页中记录的基于 ioctl 的接口提供对这些计数器的访问。希望最终 Linux 也能提供类似(但更通用)的东西。有了适当的工具,这些计数器可以提供丰富的信息。

GNU gprof

大多数读者可能都熟悉原始的 gprof(参见参考文献 3)。它是一个方便的工具,可以确定函数级别的主要性能瓶颈。但是,借助 gcc,GNU gprof 也可以查看函数内部。我们用一个真正简单的函数来说明这一点,该函数计算阶乘。假设我们已经在文件 fact.c 中输入了阶乘函数和一个简单的测试程序。然后,我们可以像这样编译该程序(假设已安装 GNU libc 版本 2.0 或更高版本)

gcc -g -O -a fact.c -lc

调用一次生成的 a.out 二进制文件会生成一个 gmon.out 文件,其中包含程序中每个基本块的执行计数。我们可以通过调用 gprof 并指定 -l--annotate 选项来查看这些计数。此命令生成一个源代码列表,其中显示了每行源代码中的基本块已执行多少次。

列表 1. 基本块执行计数

我们的阶乘示例生成了列表 1 中所示的列表。函数 main() 中以 printf 行开头的基本块执行了一次,因此它被注释为 1。对于阶乘函数,函数序言和尾声各执行了 20 次,因此函数 fact 的第一行和最后一行都注释为 20。在这 20 次调用中,19 次导致对 fact 的递归调用,剩余的一次调用仅返回 1。相应地,if 语句的 then 分支已注释为 19,而 else 语句的注释为 1。就这么简单。

函数 fact() 的行为当然没有什么意外,但在实际的、更复杂的函数中,或者在其他人编写的代码中,这些知识对于避免浪费时间优化很少执行的代码非常有帮助。

优化技术:让您的应用飞起来

下个月,我们将研究可以大大提高给定代码段性能的技术。它们中的大多数都不是新颖的。其中一些已经存在很长时间了,以至于即使不是不可能,也很难给予适当的赞誉。另一些是“显而易见的”(一旦您了解了它们)。关键是现代系统(尤其是基于 Alpha 的系统)的特性使这些技术如此重要和有价值。

致谢

作者要感谢德克萨斯 A&M 大学的 Richard Henderson 和红帽软件的 Erik Troan 在短时间内审阅了本文。他们的反馈极大地提高了文章的质量。错误和遗漏完全由作者负责。

David 是亚利桑那大学计算机科学系博士课程的研究生。他计划于 1997 年 8 月毕业,并最终找到一份“真正的工作”。在短暂地参与了与 Reed-Solomon 码和软盘驱动程序相关的 Linux 工作之后,他忘记了 Linux,直到需要一个基于 Digital 的 Alpha 处理器的低成本、高性能系统。结果,他参与了 Linux/Alpha 端口,并从此一直留在自由软件社区。当不玩电脑时,他喜欢与他可爱的妻子一起在户外活动。您可以通过电子邮件 David.Mosberger@acm.org 与他联系。

加载 Disqus 评论