在 Linux 中有效利用 3DNow!

作者:Jonathan Bush

1998 年,AMD(超微半导体公司)发布了新的 x86 CPU 系列,其中包括 3DNow! 功能。3DNow! 旨在为某些多媒体和浮点运算提供增强的性能。其他 x86 克隆 CPU 制造商,如 Cyrix 和 IDT(集成器件技术公司),最初也承诺在即将推出的 CPU 中支持 3DNow!。目前,IDT 最新一代处理器(WinChip 2)以及 AMD 的 K6-2、K6-3 和 Athlon (K7) 系列处理器均提供 3DNow! 支持。

在本文中,我们将介绍 3DNow! 技术(特别是它如何影响流行的 K6-2 和 K6-3 CPU 的性能),并展示如何在 Linux 中检测和利用 3DNow!。3DNow! 是一项令人兴奋的进展;有效地使用它可以释放 AMD 和 IDT 处理器的出色性能。

什么是 3DNow!?

3DNow! 构建于 Intel MMX(x86 的多媒体扩展)功能之上。Ariel Ortiz Ramirez 在《Linux Journal》第 61 期中描述了 MMX 以及如何在 Linux 中利用它,因此我们在此不再赘述 MMX 的细节。简而言之,MMX 为 x86 平台添加了八个 64 位“多媒体”寄存器(MM0 到 MM7)和 57 条指令,这些指令作用于这些寄存器。多个短整数可以存储(打包)到每个多媒体寄存器中,MMX 指令允许对这些打包整数进行并行计算。虽然 MMX 仅限于对整数进行运算,但 3DNow! 扩展了多媒体寄存器,使其能够将多个(两个)单精度浮点数存储(打包)到每个寄存器中。3DNow! 指令集包括 21 个新的多媒体寄存器操作。这些指令中的大多数提供了快速的、流水线的单精度(打包)浮点计算。

3DNow! 功能非常适合快速计算常见的图形操作,例如裁剪、光照和 3D 变换,以及涉及物理模型应用(例如,雾、云和重力效果)的特效。然而,任何具有相当数量浮点计算的应用程序都可以从使用 3DNow! 中受益。当有效使用时,3DNow! 可以将应用程序的浮点吞吐量提高两到四倍(甚至对于某些特殊用途的应用程序更高)。性能的提高是因为每个 3DNow! 操作产生两个结果(打包到每个多媒体寄存器中),而浮点单元 (FPU) 的标准浮点运算每个操作只产生一个结果。

此外,在 AMD K6-2 和 K6-3 中,MMX 和 3DNow! 操作可以访问双流水线执行单元,从而使最多两个 3DNow! 操作可以同时执行。因此,在 K6-2 和 K6-3 上,每个处理器时钟周期最多可以计算四个结果。(相比之下,奔腾 II 每个时钟周期最多只能产生一个浮点结果;因此,PII/450 的峰值性能为 450 MFLOPS(每秒百万次浮点运算),而 K6-2/450 的峰值性能为 1800 MFLOPS)。AMD K6-2 和 K6-3 上的标准浮点计算不是流水线的,这意味着每次完成标准浮点计算之间都有两个或多个时钟周期的延迟。使用 3DNow! 功能可以极大地提高利用 3DNow! 指令的程序的浮点吞吐量。对于配备 AMD K6-2、K6-3 或 IDT WinChip2 的计算机,只有包含 3DNow! 指令的程序才能实现峰值浮点性能。

入门指南

不幸的是,很少有编译器可以为编译代码生成 3DNow! 指令。因此,要在用 C/C++、FORTRAN 或 Pascal 等高级语言编写的程序中发挥 3DNow! 功能,必须包含具有 3DNow! 操作的显式汇编代码。这并不难做到,因此我们将演示如何在 Linux 的 C/C++ 程序中使用 3DNow!。

确定给定机器是否支持 3DNow! 的一种方法是下载并运行一个应用程序,该应用程序可以识别处理器并检查 3DNow! 功能。AMD 有一个这种类型的应用程序,可以从他们的公司网站下载。从程序内部确定主机 CPU 是否支持 3DNow! 的实用解决方案是使用 CPUID 指令,该指令返回有关处理器功能的信息,并受整个 x86 系列支持。如果程序确定存在 3DNow! 支持,它可以执行利用 3DNow! 的代码的相应部分。具体而言,可以通过调用指令 CPUID 8000_0001h 来确定 3DNow! 支持。此指令根据 CPU 的多媒体支持级别设置 EDX 寄存器中的标志位。EDX 寄存器的第 31 位指示是否存在 3DNow! 支持;因此,如果 CPU 支持 3DNow!,CPUID 会将此位设置为 1。如果第 30 位也设置为 1,则 CPU 支持新的 AMD Athlon 处理器中提供的 3DNow! 的增强扩展。

一些汇编器包含对 3DNow! 指令的支持;包含 3DNow! 指令的汇编语言模块将由这些汇编器顺利汇编。但是,许多汇编器不包含对 3DNow! 的直接支持。在许多情况下,仍然可以使用 3DNow! 指令与这些汇编器一起使用,尽管有必要使用数据块或 emits 将指令定义为伪指令。幸运的是,AMD 的网站上有一个 C++ 头文件,其中包含 3DNow! 指令集的宏定义。包含此头文件可以实现更高级语言程序中嵌入式汇编代码的开发。这些宏使用 emit 伪指令指定 3DNow! 指令的十六进制解码;头文件可能需要针对某些编译器进行修改,因为并非所有编译器都支持 emit。在 Linux 下,我们使用免费提供的 Netwide Assembler (NASM) 来汇编代码。NASM 允许使用 db 命令构建伪指令宏。我们创建了一个头文件,该文件使用 db 命令定义了 3DNow! 指令。此头文件可从 http://merlin.cs.uah.edu/visgig/threednow/ 下载。但是,0.98 及更高版本的 NASM 支持 3DNow!,因此只有旧版本才需要头文件。顺便说一句,我们发现 NASM 0.97 不允许 MM2、MM3、MM6 或 MM7 作为 3DNow! 操作的结果寄存器。NASM 0.98 没有这个问题。

在梯度计算中使用 3DNow!

一些应用程序特别适合 3DNow!,例如图形渲染。由于与汇编语言编码相关的额外时间,使用少量隔离的浮点运算优化应用程序可能不值得。在决定是否将 3DNow! 与 K6-2 或 K6-3 一起使用之前,需要考虑几个标准。首先,应用程序应至少在一部分程序中分组几个单精度浮点计算,因为切换到 MMX/3DNow! 模式会涉及一些开销。在 MMX/3DNow! 模式下,无法进行使用常规 FPU 的标准浮点运算。在 MMX/3DNow! 模式下,标准整数运算是可以的。据报道,新的 K7 不会有这种开销。MMX 模式切换也可能会中断内部指令流水线,这可能会增加开销。最大限度地减少开销影响的最佳方法是在包含多个单精度浮点运算的单元中使用 3DNow!。如果浮点数据以连续的、规则的格式(例如浮点数数组)组织,从而可以按顺序执行一系列 3DNow! 操作,则性能也将得到提高。

可以使用 3DNow! 有效实现的一个应用程序是图像梯度计算(边缘检测),尤其是范围图像梯度。为了说明 3DNow! 如何在 Linux 上实现高效操作,我们将研究如何针对范围和体积数据优化梯度计算。在 2D 网格上采集的图像中,梯度是特定点像素值(例如,像素强度)的局部变化的度量;在 3D 体积中,梯度测量体素(体积元素)与其相邻体素之间的强度差异。有多种方法可以确定梯度。对于在网格上采集的图像,计算梯度的一种方法是计算每个像素的四个直接相邻像素(北部/南部和东部/西部)的值的方向差异。当为整个图像计算此差异时,结果是位于区域边界上的点通常具有强梯度幅度。梯度幅度可以被视为图像——它们看起来像原始图像的区域边界的集合。图 1 中的图像是从激光测距数据生成的范围图像。在范围图像中,每个像素都是一个浮点值,表示成像对象上相应点到观察平面的距离。我们使用强度显示图像,其中较亮的值表示较近的点。图 2 显示了图 1 范围图像的计算出的四方向梯度。

Effectively Utilizing 3DNow! in Linux

图 1。

Effectively Utilizing 3DNow! in Linux

图 2。

一般来说,特定点的梯度幅度由以下公式给出

|Gradient|=sqrt((0.5*delta(

其中 delta(x) 等于 x 方向上像素强度的变化,delta(y) 是 y 方向上像素强度的变化。我们在图 3 中说明了像素 P1 的梯度。P1 的梯度幅度为

Gradient(P1)=sqrt(0.25*((depth(P2)-depth(P0))
+(depth(P4)-depth(P6))<+>2<+>)).
Effectively Utilizing 3DNow! in Linux

图 3。

3DNow! 非常适合对范围图像执行此计算。由于范围图像可以存储为数组,因此在行上彼此相邻的点将在内存中连续出现。MMX 有一条指令 movq,可将“四字”(四个字——两个单精度浮点数)从内存移动到多媒体寄存器中。这意味着可以使用一次移动将连续的图像像素 P4 和 P5 加载到多媒体寄存器中。如果将 P6 和 P7 加载到另一个 MMX 寄存器中,我们可以使用 3DNow! 操作 PFSUB 来减去寄存器对的内容。一个 3DNow! 减法的结果将是 P1 和 P2 的 delta(y)。再进行一次减法可以得到这些像素的 delta(x)。此外,3DNow! 操作可用于对 delta(x) 和 delta(y) 进行平方、将它们相加、应用乘法因子并取平方根。与使用标准浮点指令的实现相比,整个过程可以使用更少的汇编指令(以及大约一半的执行时间)来实现。

我们开发了汇编语言函数 XYGRAD 来协助计算范围图像梯度。(此函数的代码可以在存档文件 ftp.linuxjournal.com/pub/lj/listings/issue68/3685.tgz 中找到。)该函数使用 3DNow! 一次处理一行图像像素。XYGRAD 可以从任何 C 程序中使用代码中显示的原型调用。使用 NASM 汇编 XYGRAD 后,使用 gcc 将其与使用 XYGRAD 的 C 程序链接。

理解使用 3DNow! 进行梯度计算

XYGRAD 作为指向原始图像的指针(称为 img_ptr)以及要处理的行大小传递。该函数一次处理四个像素的图像行。对于一组四个像素,首先计算 x 方向的强度变化。包含 P0 和 P1 的四字使用 movq 移动到 MMX 寄存器 MM0 中。P2 和 P3 移动到 MM1 寄存器中。然后使用 3DNow! 打包减法(即,PFSUB MM0, MM1)减去这些寄存器。结果存储在第一个操作数 MM0 中,稍后使用打包乘法运算(PFMUL MM0, MM0)对其进行平方。遵循类似的步骤来计算 y 方向的梯度,平方差存储在 MM2 寄存器中。然后将 MM0 和 MM2 相加,得到 delta(x)<+>2<+> +delta(y)<+>2<+>,它存储在 result_img_ptr 引用的输出数组中。处理完整个图像后,C 程序会调用第二个 3DNow! 优化模块来计算结果图像中每个像素的平方根。这样就完成了梯度计算。用于范围图像梯度计算程序的 C 和汇编模块的完整源代码可以从 http://merlin.cs.uah.edu/visgig/threednow/gradient/ 下载。

3DNow! 优化

我们在编码过程中牢记的一件事是 K6-2 和 K6-3 CPU 在两个执行流水线中流水线化指令的能力。由于这些处理器的架构,某些限制适用于 3DNow! 指令的流水线化。也就是说,每个 3DNow! 指令都属于两个子集之一,并且来自同一子集的两个指令不能并行发出。例如,打包浮点减法 (PFSUB) 和打包浮点加法 (PFADD) 都属于同一子集,因此不能在同一时钟周期内发出。另一方面,打包浮点乘法 (PFMUL) 属于 3DNow! 指令的另一个子集;因此,只要它们之间没有操作数依赖性,PFMUL 和 PFADD 指令就可以同时执行。尽可能地交织来自每个子集的指令以增加并行计算的可能性是有益的。大多数整数 MMX 操作在 K6-2 和 K6-3 上没有这样的限制;实现最佳浮点性能确实需要程序员进行更多考虑。

梯度计算性能

我们经过 3DNow! 优化的梯度计算程序提供了出色的性能。我们在配备 300MHz AMD K6-2 CPU 的双启动 PC 上对程序进行了测试,以确定 3DNow! 在 Windows 和 Linux 环境中的性能。在 Linux 上,我们使用 Netwide Assembler (NASM) 0.98 版汇编汇编模块。我们使用 GNU C (gcc) 2.7 版编译 C 驱动程序并将驱动程序链接到汇编代码。在 Windows 上,我们使用流行的商业 C 编译器来汇编、编译和链接汇编代码和 C 代码。Windows 汇编器/编译器无法识别 3DNow! 指令,因此我们必须包含 AMD 头文件,该文件使用伪指令宏定义了 3DNow!。我们在几个范围图像和 3D 体积上测试了 3DNow! 优化的梯度计算。

表 1 和表 2 显示了两个代表性数据集的性能结果。在表 1 中,显示了在 Linux 和 Windows 环境中执行 3DNow! 优化的范围图像梯度计算的平均 CPU 时间。(时间是针对 240x240 范围图像在 2000 次试验中取平均值。)相比之下,在 Linux 下,3DNow! 优化的代码比未优化的标准 C 代码快约九倍,后者根据相同的像素访问模式计算数据,即,未优化的代码纯粹用 C 实现,未使用 3DNow!,并且在编译时未进行任何编译器优化。当标准 C 版本的梯度在 Linux 中使用 gcc 进行最大程度的优化(使用 -O2 -ffast-math 优化开关)编译时,它仍然比 3DNow! 优化的代码慢三倍以上。这次的改进非常显着;范围图像梯度计算的 3DNow! 优化版本可以在 Linux 下以实时帧速率完成。体积梯度计算的性能提升几乎同样令人印象深刻——表 2 中显示的时间是针对 72x256x256 体积的。3DNow! 优化的体积梯度执行速度比完全优化的标准 C 实现快 2.5 倍以上,比未优化的标准 C 实现快 4.5 倍以上。

表 1

我们还在运行 Linux 的奔腾 II/333 上测试了标准 C 范围图像梯度实现的性能(即,没有 3DNow! 代码的实现)。使用完全编译器优化,240x240 范围图像的梯度在 PII 上的计算时间比在 K6-2 上多 17%。因此,我们估计 K6-2 在 Linux 下计算范围图像梯度的速度比时钟频率相当的 PII 快约 30%。当然,我们应该补充说,与汇编语言相比,用 C 开发模块通常更容易。

表 2

结论

3DNow! 是桌面计算领域令人兴奋的进展,为许多应用程序提供了显着提高性能的潜力。3DNow! 可以使用 NASM 和 gcc 在 Linux 中有效利用。对于涉及浮点计算的应用程序,尤其是那些对速度至关重要的应用程序,集成利用 3DNow! 指令的模块可以释放流行的 CPU(如 IDT Winchip 2 和 AMD K6-2、K6-3 和 Athlon 处理器)的出色浮点吞吐量。

Effectively Utilizing 3DNow! in Linux
Jonathan Bush (jbush@cs.uah.edu) 最近获得了阿拉巴马大学亨茨维尔分校计算机科学学士学位。作为国家科学基金会本科生科研体验学者,他完成了本文中描述的大部分工作。他目前是计算机科学系的博士研究生助理。

Effectively Utilizing 3DNow! in Linux
Timothy S. Newman (tnewman@mailhost.cs.uah.edu) 是阿拉巴马大学亨茨维尔分校的计算机科学助理教授。当他不教书时,他经常可以在 Linux 机器上进行可视化和图像研究。
加载 Disqus 评论