避免 AVX-SSE 转换造成的性能损失

避免 AVX-SSE 转换造成的性能损失 (PDF 678 KB)


由于硬件必须保存和恢复 YMM 寄存器的上层 128 位,因此一个程序中256 位英特尔® AVX 指令与传统英特尔® SSE 指令之间的转换可能会带来性能损失。本文讨论了这些转换损失产生的方式和原因、检测 AVX-SSE 转换的方法,以及消除转换或避免转换损失的方法。本文还讨论了 CPU 调度对 AVX-SSE 转换可能带来的影响,并针对如何在使用英特尔® AVX 时避免问题提供了一般建议。

1. AVX-SSE 转换损失简介

英特尔® 高级矢量扩展指令集(英特尔® AVX)是一种全新的 SIMD 扩展指令集,应用于第二代英特尔® 酷睿® 处理器家族中。英特尔® AVX 采用更宽的 256 位矢量和全新的指令,并使用全新的矢量扩展(VEX) 扩展指令编码格式,后者加强了对三个(或更多)运算指令的支持。英特尔® AVX 还包括与所有传统英特尔® SIMD 流指令扩展(英特尔® SSE)128 位指令相当的 128 位 VEX 编码指令。

使用英特尔® AVX 指令时,非常有必要了解将 256 位英特尔® AVX 指令与传统(非 VEX 编码)英特尔® SSE 指令混合在一起可能导致性能损失。256 位英特尔® AVX 指令运算于 256 位 YMM 寄存器上,后者是现有 128 位 XMM 寄存器的 256 位扩展。128 位英特尔® AVX 指令运算于 YMM 寄存器的底层 128 位,并对上层 128 位进行归零处理。然而,传统的英特尔® SSE 指令运算于 XMM 寄存器上,而且对 YMM 寄存器的上层 128 位一无所知。因此,硬件会在从 256 位英特尔® AVX 转换到传统的英特尔® SSE 时保存 YMM 寄存器的上层 128 位的内容,然后在从英特尔® SSE 转换回英特尔® AVX(256 位或 128 位)时恢复这些值。保存和恢复运算均会给每次运算带来总计数十个时钟周期的损失。

几种不同的情况下会发生 AVX-SSE 转换,例如当 256 位英特尔® AVX intrinsic 指令或内嵌汇编与下列任何一种指令混合时:

a. 128 位 intrinsic 指令
b. 英特尔® SSE 内嵌汇编
c. 编译成英特尔® SSE 的 C/C++ 浮点代码
d. 对包含以上任意一种指令的函数或库的调用

此外,在包含 256 位英特尔® AVX 指令的代码正在执行时如果包含传统英特尔® SSE 指令的中断服务例程 (ISR) 发生中断,也可能会发生 AVX-SSE 转换。对于ISR 造成的 AVX-SSE 转换损失,应用开发人员无法避免。ISR 开发人员在其例程中使用 XMM/YMM 寄存器时应当知晓这一可能的损失,应当使用以下类似的方法来避免 AVX-SSE 转换损失,并确保在必要时对整个 YMM 状态进行保存和恢复。

一般来说,通过将传统的英特尔® SSE 指令转换成与其对等的 VEX 编码指令能够消除 AVX-SSE 转换。当转换无法消除时,通常可以通过对 YMM 寄存器的上层 128 位进行明确归零来避免损失。在这种情况下,硬件不会保存这些值。避免 AVX-SSE 转换损失的方法将在第 3 部分深入讨论。

考虑下面同时使用 128 位和 256 位 intrinsic 指令的例子。生成的汇编(如下所示)主要包含英特尔® AVX 指令(前缀为“v”)。不过,该汇编还包含传统的英特尔® SSE 指令 (movaps)。在 movaps 指令之前,硬件将保存 YMM 寄存器上层 128 位的内容。当发现下一个英特尔® AVX 指令(将在下一次迭代出现)时,硬件将恢复这些值。下列代码由英特尔® 编译器(版本 12.0.4,–O3)的命令行编译而成。

图 1. C 源和反汇编(例如)显示了 AVX-SSE 转换发生的位置。

2. 检测 AVX-SSE 转换

2.1. 利用英特尔® 软件开发仿真器

英特尔® 软件开发仿真器(英特尔® SDE)是一款面向 Windows* 和 Linux* 的命令行工具,能够帮助开发人员检测其程序中的动态 AVX-SSE 转换,即使处理器不支持英特尔® AVX 也能如此。英特尔® SDE 将针对函数中特定数据块报告 AVX-SSE 和 SSE-AVX 转换的数量。下面列出的命令行用法和示例输出提供了有关AVX-SSE 转换的详细信息。使用英特尔® SDE 既有优势,也有劣势。其优势在于该工具免费提供,使用非常简单和快捷,而且可用于不支持英特尔® AVX 的处理器上;劣势在于该工具无法显示导致转换的特定指令。如欲了解有关此工具的更多信息,请访问 英特尔® 软件开发仿真器网站

图 2. 使用英特尔® SDE 检测 AVX-SSE 转换的命令和英特尔® SDE 的示例输出。

2.2. 使用英特尔® VTune™ Amplifier XE

第二代英特尔® 酷睿™ 处理器家族支持与从 256 位英特尔® AVX 到英特尔® SSE (OTHER_ASSISTS.AVX_TO_SSE) 和从英特尔® SSE 到英特尔® AVX (OTHER_ASSISTS.SSE_TO_AVX) 转换相对应的硬件事件。开发人员可在第二代英特尔® 酷睿™ 处理器上使用英特尔® vTune™,利用这些硬件事件来检测 AVX-SSE 转换。要利用英特尔® vTune™中的这些事件,您将需要创建一个新的基于硬件事件的自定义分析。具体步骤,请参见下图 Microsoft* Visual Studio 2010 SP1 中的注释(注意:在 Microsoft Visual Studio* 2010 中使用英特尔® AVX 时,需要 SP1):

  1. 创建新分析
  2. 单击“New(新建)”并选择“New Hardware Event-based Sampling Analysis(新建基于硬件事件的采样分析)”
  3. 单击“Add Event(添加事件)”,选择 OTHER_ASSISTS.AVX_TO_SSE 和 OTHER_ASSISTS.SSE_TO_AVX 事件,然后单击“OK(确定)”
  4. 单击“Start(开始)”启动分析

分析完成后,您将看到按函数统计的事件数,从中您能够确定哪些函数包含 AVX-SSE 转换。您也可以单击任意一个函数以查看源代码或反汇编中发生这些特定事件的热点,从而确定造成转换的具体指令。

使用英特尔® vTune™来检测 AVX-SSE 转换的优势在于它能够向您显示您的源代码和反汇编中发生转换的确切位置,而劣势在于它只有在支持英特尔® AVX 的处理器上才能检测 AVX-SSE 转换事件。

图 3.利用英特尔® vTune™创建自定义分析来检测 AVX-SSE 转换的步骤。

3. 避免 AVX-SSE 转换损失的方法

3.1. 方法1:利用编译器标志自动转换成 VEX

消除 AVX-SSE 转换或消除转换损失有几种方法。避免 AVX-SSE 转换损失的最简单方法是利用英特尔® 编译器(使用 –xavx(Windows* 上为 /Qxavx)或 –mavx(Windows* 上为 /arch:avx)标志)来编译相关的源文件。这些标志告诉英特尔® 编译器生成专用于支持英特尔® AVX 的处理器的指令。其中,-xavx 标志还告诉英特尔® 编译器尽力为支持英特尔® AVX 的处理器优化代码。

使用这些标志时,处理器将在合适的地方自动生成 VEX 编码的指令而非传统的英特尔® SSE 指令,从而在这些文件中消除英特尔® AVX 和英特尔® SSE 之间的转换。这些标志还告诉编译器自动插入 vzeroupper 指令,后者对 YMM 寄存器的上层 128 位进行归零(请参见下一部分)。使用这些标志时,编译器将在包含英特尔® AVX 代码的函数的开头部分插入一个 vzeroupper 指令,前提是参数中无 YMM 寄存器或 __m256/__m256d/__m256i 数据类型;编译器还将在函数的末尾插入一个 vzeroupper 指令,前提是返回的值不是 YMM 寄存器或 __m256/__m256d/__m256i 数据类型。插入 vzeroupper 指令能够防止在可能包含传统英特尔® SSE 指令的例程中的文件中调用函数时发生 AVX-SSE 转换。如欲更多了解面向特定处理器优化的英特尔® 编译器标志,请参阅英特尔® 编译器文档。

这一方法的优势在于编译器自动完成相关操作。此外,这也是强制 128 位 intrinsic 指令生成 VEX 编码指令(当未使用 –xavx 或 –mavx 时,无法保证 128 位 intrinsic 指令会生成 VEX 编码指令)的唯一方法。在某些情况下,编译器会将 C/C++ 浮点代码编译成与 x87 指令相对的英特尔® SSE 指令。如果 C/C++ 浮点代码被编译成英特尔® SSE 指令,那么使用 –xavx 或 –mavx 标志将是强制编译器生产 VEX 编码指令的唯一方法。将 C/C++ 浮点代码编译成 x87 指令不会造成转换损失。

这一方法的劣势在于它需要访问相关的源文件,因此无法避免由于调用没有使用 –xavx 或 –mavx 标志进行编译的函数而造成的 AVX-SSE 转换。另一可能的劣势是在使用 –xavx 或 –mavx 标志进行编译的文件中,所有英特尔® SSE 代码将被转换成 VEX 格式,并将只能运行在支持英特尔® AVX 的处理器上。如果某个文件包含计划在多个不同代处理器上运行的代码,那么您应该考虑将函数分散在各个文件,并利用相关的编译器标志来编译每个文件。另请参阅第 4 部分有关 CPU 调度的内容。

回到本例中,通过使用 –xavx 标志来编译文件,编译器现在将能够生成 vmovaps 指令而非 movaps 指令,从而消除了 AVX-SSE 转换。在消除转换之前,该代码每次迭代消耗 230 个周期。利用 –xavx 编译后,该代码现在每次迭代仅消耗约 70 个周期 1


1在运行 Mac OS X 10.6.8 的 2.3 GHz 英特尔® 酷睿™ i7 处理器上,利用英特尔® 编译器 12.0.4 (–O3)编译。

图 4. 使用 –xavx 时,编译器将使用 VEX 编码版本的 128 位指令。

3.2. 方法2:利用编译指示自动转换成 VEX

利用英特尔® 编译器自动转换成 VEX 的另一方法是使用英特尔® 特定编译指示:#pragma intel optimization_parameter target_arch=avx;英特尔® 编译器 12.1不支持这一编译指示。当置于函数首部时,此编译指示会产生仅将 –mavx 应用于该函数的效果。这将造成在函数中合适的地方自动生成 VEX 编码的指令,同时在函数的开头和末尾自动插入一个 vzeroupper 指令。

这一方法的优势在于它能够应用于与文件级别相对的函数级别,就像 –xavx 和 –mavx 一样。因此,无需将计划在多个不同代处理器上运行的函数分散在多个不同的文件中。这一方法的劣势在于它需要访问相关的源文件(就像 –xavx 和 –mavx 一样),因此无法避免由于调用无法访问的函数而造成的 AVX-SSE 转换。这一方法的另一劣势在于如果英特尔® 编译器选择了标有此编译指示的函数来嵌入,那么目前英特尔® 编译器将不会将 –mavx 应用于该代码。这一点可以通过利用 __declspec(noinline) 关键字避免英特尔® 编译器嵌入该函数来避免。

图 5. 使用 optimization_parameter 编译指示和 __declspec(noinline) 关键字的示例

3.3. 方法3:归零寄存器

在许多情况下,从英特尔® AVX 到英特尔® SSE 的转换可能无法消除。例如,当必须调用使用传统英特尔® SSE 的库时。在这些情况下,可使用 intrinsic 指令或嵌入汇编来调用 vzeroupper 指令,后者可对 YMM 寄存器的上层 128 位进行归零(同样地,也可使用 vzeroall 指令来对 YMM 寄存器的全部 256 位进行归零)。当利用 vzeroupper 指令将 YMM 寄存器的上层 128 位设置为零后,硬件无需保存这些值,从而也就不会发生硬件辅助(hardware assist)。Vzeroupper 指令必须用在 256 位英特尔® AVX 代码之后、英特尔® SSE 代码之前,这将消除保存和恢复两种运算。利用其它方法(例如,利用 XOR)对 YMM 寄存器进行归零将不会防止 AVX-SSE 转换损失。

这一方法的优势在于:在使用包含传统英特尔® SSE 的函数或库和不在您控制之下的函数或库时,这是唯一能够避免 AVX-SSE 转换损失的方法。这一方法的另一优势在于通过利用 intrinsic 指令 _mm256_zeroupper() 和 _mm256_zeroall(),此方法无需编写汇编语言即可实施。这一方法的劣势在于 vzeroupper 指令必须正确放置才能避免所有转换损失。

要解决示例代码中的问题,我们必须在最后一个 256 位英特尔® AVX intrinsic 指令之后和 128 位 intrinsic 指令之前添加一个对 vzeroupper (使用 _mm256_zeroupper() intrinsic 指令)的调用。在添加代码以对 YMM 寄存器的上层 128 位进行归零后,此代码每次迭代仅消耗约 70 个周期。

图 6. 归零寄存器以避免 AVX-SSE 转换损失。

3.4. 方法4:手动将汇编转换成 VEX

避免 AVX-SSE 转换损失的最后一个方法是手动将任意传统的英特尔® SSE 汇编指令转换成相应的 VEX 编码指令,从而消除 AVX-SSE 转换。如欲了解有关 VEX 编码指令的信息,请参阅英特尔® 架构软件开发人员手册

手动转换为 VEX的优势在于它允许您在一个文件中有选择性地将汇编转换成 VEX,而非利用 –xavx 全部转换为 VEX。此外,如果出于某种原因,无法利用 –xavx 或编译指示或效果不理想,那么手动将汇编转换成 VEX

将是唯一的选择。手动转换成 VEX 的另一个优势是它允许您在汇编中充分利用非破坏性的三指令运算(three-operand forms)。这一方法的劣势在于它必须在汇编代码中以手动方式进行,而且代码将仅能运行在支持英特尔® AVX 的处理器上。

4. AVX-SSE 转换和 CPU 调度

在许多情况下 ,拥有特定函数的多个版本并针对特定的 CPU 特性(如英特尔® SSE2 和英特尔® AVX 等)对每个版本进行优化是一种理想选择。例如,当您希望拥有某个函数的英特尔® AVX 版本和非 AVX 版本,以便您能够充分利用英特尔® AVX 处理器并同时支持非 AVX 处理器,这可能非常有用。在这些情况下,可根据程序所在的 CPU ,利用 CPU 调度将执行“调度”到最合适的函数版本。实施 CPU 调度有三种方法:利用英特尔® 编译器自动调度、利用英特尔® 编译器的手动调度特性手动调度,或利用开发人员提供的自定义机制手动调度。我们将讨论利用英特尔® 编译器进行的自动和手动 CPU 调度以及这两种方法对 AVX-SSE 转换的影响。我们不能保证这些方法能与其它编译器一起使用,因此开发人员应当明白在其它编译器上进行 CPU 调度可能是他们自己的事。

4.1. 英特尔® 编译器的自动调度特性

要充分利用英特尔® 编译器的自动调度特性,需使用 –axavx 标志(在 Windows* 上为/Qaxavx)。该标志引导英特尔® 编译器查找利用任意英特尔® SIMD 指令(直至英特尔® AVX)扩展来优化现有代码的机会。发现足够的性能优势后,英特尔® 编译器将生成现有函数特定于某个处理器的优化版本,并将生成函数以自动调度到正在执行的合适函数。英特尔® 编译器将始终会生成一个包含原始代码的类函数,但是否会生成特定于某个处理器的特定版本则不确定。如欲了解有关英特尔® 编译器的自动调度特性的更多信息,请参阅英特尔® 编译器面向 SSE 生成和特定处理器优化的选项

使用英特尔® 编译器的自动调度特性时,编译器将逐个函数地确定是否生成特定于某个处理器的自动调度版本。如果编译器选定某个函数进行自动调度并生成一个针对英特尔® AVX 优化的代码路径,那么将根据情况生成英特尔® AVX 指令,该函数中的所有相关指令将自动采用 VEX 编码,并且将在该函数的开头和末尾自动插入 vzeroupper 指令。然而,如果某函数未被选中进行自动调度且开发人员已经手动添加了英特尔® AVX intrinsic 指令,那么该函数中的所有相关指令是否采用 VEX 编码以及是否会自动插入 vzeroupper 指令将无法保证。切记,仅仅使用 –axavx 并不能保证英特尔® 编译器会优化英特尔® AVX 的代码,如果您没有使用 –axavx(使用 –axavx 与使用 –xavx 不同),您的程序仍有可能存在相同的 AVX-SSE 转换。

4.2. 英特尔® 编译器的手动调度特性

英特尔® 编译器的手动调度特性支持开发人员明确定义特定于某个处理器的函数版本。之后,英特尔® 编译器将自动生成函数以在执行中调度到合适版本。当您希望明确定义函数的英特尔® AVX 版本,并希望明确支持其它不支持英特尔® AVX 的处理器(例如,英特尔® AVX 版本、英特尔® SSE 版本和通用版本)时,手动调度非常有用。

手动调动利用关键字 __declspec(cpu_dispatch()) 和 __declspec(cpu_specific()) 实施。关键字 __declspec(cpu_dispatch(cpuid,应当置于将要调度的函数的根(stub)上,cpuid 参数应当指定所有正被明确指向的特定处理器。关键字 __declspec(cpu_specific(cpuid, 应当置于函数的特定处理器实施之上,必须提供一个或多个特定指向处理器的 cpuid。core_2nd_gen_avx cpuid 用于指向不支持英特尔® AVX 的处理器。如欲了解有关英特尔® 编译器手动调度特性的更多信息和示例,请参阅如何手动指向支持英特尔® AVX 的第二代英特尔® 酷睿™ 处理器

在指定 core_2nd_gen_avx cpuid 的函数版本中,所有相关的 intrinsic 指令和内嵌汇编2 将自动采用 VEX 编码,并且将在函数的开头和末尾自动插入 vzeroupper 指令。没有指定 core_2nd_gen_avx cpuid 且包含英特尔® AVX intrinsic 指令的任意函数将被指向不支持英特尔® AVX 的处理器,并会在运行时产生异常。

表 1. 不同编译器标志和调度场景对代码生成的影响。

5. 总结和建议

由于硬件会保存和恢复 YMM 寄存器的上层 128 位,因此当在 256 位英特尔® AVX 指令与英特尔® SSE 指令之间切换时会产生性能损失。要消除此损失,您可以使用英特尔® 编译器的 –xavx 或 –mavx 标志、全新的英特尔® 特定编译指示或通过手动转换汇编,将所有传统的英特尔® SSE 指令转换成VEX 编码指令。当无法避免转换时,您可以利用 vzeroupper 指令在 256 位英特尔® AVX 指令之后或英特尔® SSE 指令之前对 YMM 寄存器进行归零,以此来消除损失。


1 当函数参数中不存在 YMM 寄存器或 __m256/__m256d/__m256i 数据类型时在开头,当返回的值中不存在 YMM 寄存器或 __m256/__m256d/__m256i 数据类型时在末尾。在这些情况下,当前英特尔® 编译器不会将英特尔® SSE 内嵌汇编转换成 VEX 编码的指令。建议英特尔® SSE 内嵌汇编采用 VEX 编码。该问题正在调查中,不久将得到解决。

为了最大限度减少英特尔® AVX使用中的问题,建议您利用 –xavx 标志对计划运行在支持英特尔® AVX 的处理器之上的任意源文件进行编译。如果您的代码包含计划运行在多个不同代处理器上的函数,那么建议您使用全新的英特尔® 特定编译指示而非利用 –xavx 进行编译。此外,您应当使用 128 位指令的 VEX 编码形式以避免 AVX-SSE 转换。即便您的代码不包含传统的英特尔® SSE 代码,当您在代码中使用完256 位英特尔® AVX 时,仍应尽快使用 vzeroupper 指令或 intrinsic 指令对寄存器进行归零。这样做能够帮助您避免在未来引入转换或在可能使用您代码的程序中造成转换。最后,在开发包含英特尔® AVX 的程序时,建议您始终利用英特尔® 软件开发仿真器或英特尔® vTune™来检查是否存在 AVX-SSE 转换。

6. 作者简介

Patrick Konsor 是英特尔位于圣克拉拉的 Apple 支持团队的一名应用工程师,擅长于软件优化。Patrick 毕业于美国威斯康辛大学欧克莱尔分校,拥有计算机科学专业学士学位。Patrick 的业余爱好是阅读和骑自行车兜风。

有关编译器优化的更完整信息,请参阅优化通知