在不编写 AVX 代码的情况下使用 AVX

1. 简介和工具

英特尔® 高级矢量扩展指令集(AVX)是一套针对英特尔® SIMD 流指令扩展(英特尔® SSE)的 256 位扩展指令集,专为浮点密集型应用而设计。 英特尔® SSE 和英特尔® AVX 均为单指令多数据指令集的示例。 英特尔® AVX 作为第二代英特尔® 酷睿™ 处理器家族的一部分发布。 英特尔® AVX 采用更宽的 256 位矢量 — 一种全新的扩展指令格式(矢量扩展指令集或 VEX)并具备丰富的功能,使系统性能得到显著提升。 该指令集架构支持三种操作数,可提升指令编程灵活性,并支持非破坏性的源操作数。 传统的 128 位 SIMD 指令也经过了扩展,支持三种操作数和新的指令加密格式 (VEX)。 指令加密格式介绍了使用操作码和前缀,以处理器能够理解的格式来表达更高级别的指令的方式。 这增强了对数据和通用应用的管理,例如图像、音频/视频处理、科研模拟、金融分析和三维建模与分析等。

本文讨论了开发人员可通过哪些方式将英特尔® AVX 集成到应用中,且无需在低级别汇编语言中进行明确地编码。 对于 C/C++ 开发人员来说,访问英特尔® AVX 的最直接方式是使用兼容 C 的内部指令。 这些内部函数提供了到英特尔® AVX 指令集的访问,以及英特尔® 短矢量数学库 (SVML) 中的更高级别的数学函数。 这些函数分别在 immintrin.h 和 ia32intrin.h 头文件中进行声明。 应用编程人员还可通过其它方法来使用英特尔® AVX,而且无需将英特尔® AVX 指令添加至其源代码。 本文针对这些方法进行了调查(使用英特尔® C++ Composer XE 2011,定位于在 Sandy Bridge 系统上执行)。 Linux*、Windows* 和 Mac OS* X 平台均支持英特尔® C++ Composer XE。 本文将使用面向 Windows* 平台的命令行开关。 “简介和工具”部分提供的表格列举了面向各平台的同等命令行开关。

2. 单指令多数据 (SIMD) 概念回顾

支持矢量或 SIMD 的处理器能够在一次指令中,同时在多个数据操作数上执行一个操作。 在一个数字上由另外一个数字执行的操作以生成单个结果的流程被称作标量流程。 在 N 个数字上同时执行的操作以生成 N 个结果的流程被称作矢量流程 (N > 1)。 英特尔处理器或支持 SIMD 或 AVX 指令的兼容的非英特尔处理器均支持该技术。 将算法从标量转化为矢量的流程被称作矢量化。

一般性的 multiplyValues 示例可帮助解释标量和矢量流程(使用英特尔® AVX)之间的区别。


void multiplyValues(float *a, float *b, float * c, int size)
{
    for (i = 0; i < size; i++) {
        a[i] = b[i] * c[i];
    }
}


3. 面向英特尔® AVX 的重新编译

第一个方法是使用 /QaxAVX 编译器开关进行重新编译。 无需对源代码进行修改。 英特尔® 编译器将生成相应的 128 和 256 位英特尔® AVX VEX 加密指令。 当有助于提高性能时,英特尔® 编译器将针对英特尔处理器生成多个特定处理器,且具备自动分布功能的代码路径。 最合适的代码将在运行时执行。

4. 编译器自动矢量化

借助合适的架构开关来编译应用,是构建英特尔® AVX 就绪型应用的第一步。 借助自动矢量化功能,编译器可代表软件开发人员执行大部分矢量化工作。 自动矢量化是满足特定条件时编译器执行的优化。 英特尔® C++ 编译器可在生成代码期间自动执行相应的矢量化操作。基于英特尔 C++ 编译器的矢量化指南详细介绍了矢量化。 当优化级别为 /O2 或更高时,英特尔编译器将寻找矢量化机遇。

让我们来考虑一个简单的矩阵矢量乘法示例,该示例随英特尔® C++ ComposeXE 提供,详细阐释了矢量化的概念。 下列代码片段来自 vec_samples 归档的 Multiply.c 中的 matvec 函数:


void matvec(int size1, int size2, FTYPE a[][size2], FTYPE b[], FTYPE x[])
{
	for (i = 0; i < size1; i++) {
		b[i] = 0;

		for (j = 0;j < size2; j++) {
			b[i] += a[i][j] * x[j];
		}
	}
}


如果没有矢量化,外层循环将执行 size1 时间,内层循环将执行 size1*size2 时间。 借助 /QaxAVX 开关实现矢量化以后,内层循环可以展开 (unrolled),这是因为可在每次操作的单个指令中执行四次乘法和四次加法。 矢量化循环的效率比标量循环高得多。 英特尔® AVX 的优势还适用于单精度浮点数字,因为 8 个单精度浮点操作数可以存于 ymm 寄存器中。

循环必须满足特定的标准才能实现矢量化。 在运行时进入循环时,必须要知道循环运行次数。 运行次数可以是变量,但在执行循环时必须是常量。 循环必须具备单进和单出能力,而且退出不能依赖于输入数据。 此外还存在一些分支标准,例如不允许开关语句 (switch statement)。 如果 If 语句可作为隐蔽任务实施,则可允许这种类型的语句。 最内层的循环最有可能是矢量化的对象,而且在循环内部使用函数调用可能会影响矢量化。 内联函数和固有的 SVML 函数可增加矢量化机遇。

在应用开发的实施和调试阶段,建议对矢量化信息进行检查。 英特尔® 编译器提供了矢量化报告,可帮助你了解被矢量化以及未被矢量化的元素。 该报告可通过 /Qvec-report=<n> 命令行选项提供,其中 n 指定了报告的详细级别。 详细级别随 n 数值的增加而增加。如果 n=3,则可以提供相关性信息、被矢量化的循环和未被矢量化的循环。 开发人员可根据报告中的信息来修改实施,循环未被矢量化的原因提供了非常有帮助的信息。

开发人员在其具体应用方面具有深入的专业知识,因此有时可以忽略自动矢量化行为。 编译指示提供了额外的信息,以便为自动矢量化流程提供帮助。 部分示例包括:一直对循环进行矢量化操作、确定循环内的数据保持一致、忽略潜在的数据相关性等。addFloats 示例对部分重要点进行了说明。 你需要检查生成的汇编语言指令,以了解所生成的编译器。 当指定 /S 命令行选项时,英特尔编译器将在当前的工作目录中生成汇编文件。


void addFloats(float *a, float *b, float *c, float *d, float *e, int n)	{
	int i;
#pragma simd
#pragma vector aligned
	for(i = 0; i < n; ++i)	{
		a[i] = b[i] + c[i] + d[i] + e[i];
	}
}


请注意 simd 和矢量编译指示的使用。 它们在实现所期望的英特尔® AVX 256 位矢量化方面起着重要作用。 当使用 /QaxAVX 选项来编译 addFloats 时(没有包含 simd 和矢量编译指示的行),将生成下列代码。

没有 simd 和矢量编译指示


.B46.3::
        vmovss    xmm0, DWORD PTR [rdx+r10*4]
        vaddss    xmm1, xmm0, DWORD PTR [r8+r10*4]  
        vaddss    xmm2, xmm1, DWORD PTR [r9+r10*4]  
        vaddss    xmm3, xmm2, DWORD PTR [rax+r10*4]
        vmovss    DWORD PTR [rcx+r10*4], xmm3
        inc       r10
        cmp       r10, r11
        jl        .B46.3


汇编代码显示的是使用英特尔® 128 位 AVX 指令的标量版本。 目标是提供英特尔® 256 位 AVX 指令的打包(“矢量”的另一种说法)版本。 Vaddss 中的 ss 表示只将一组单精度浮点操作数添加在一起 — 标量操作。 如果使用 vaddps,那么该算法将更加高效;ps 表示在单精度浮点操作数上执行打包操作。

向代码只添加"#pragma simd"有助于生成英特尔® 128 位 AVX 指令的打包版本。 此外,编译器还将展开循环,从而减少与循环测试结束相关的执行指令数量。 由于每个指令只运行四个操作数,因此仍有进一步的优化空间。

使用 #pragma simd


.B46.11::
        vmovups   xmm0, XMMWORD PTR [rdx+r10*4]
        vaddps    xmm1, xmm0, XMMWORD PTR [r8+r10*4]
        vaddps    xmm2, xmm1, XMMWORD PTR [r9+r10*4]
        vaddps    xmm3, xmm2, XMMWORD PTR [rax+r10*4]
        vmovups   XMMWORD PTR [rcx+r10*4], xmm3
        vmovups   xmm4, XMMWORD PTR [16+rdx+r10*4]
        vaddps    xmm5, xmm4, XMMWORD PTR [16+r8+r10*4]
        vaddps    xmm0, xmm5, XMMWORD PTR [16+r9+r10*4]
        vaddps    xmm1, xmm0, XMMWORD PTR [16+rax+r10*4]
        vmovups   XMMWORD PTR [16+rcx+r10*4], xmm1
        add       r10, 8
        cmp       r10, rbp
        jb        .B46.11



指定"pragma vector aligned"有助于编译器针对所有阵列参考使用一致的数据移动指令。 使用"pragma simd"和"pragma vector aligned"可生成期望的 256 位英特尔® AVX 指令。 英特尔® 编译器选择 vmovups,这是因为当访问第二代英特尔®酷睿TM处理器上的一致内存时,使用不一致的转移指令不会出现任何问题。

使 #pragma simd 和 #pragma 矢量保持一致


.B46.4::
        vmovups   ymm0, YMMWORD PTR [rdx+rax*4]
        vaddps    ymm1, ymm0, YMMWORD PTR [r8+rax*4]
        vaddps    ymm2, ymm1, YMMWORD PTR [r9+rax*4]
        vaddps    ymm3, ymm2, YMMWORD PTR [rbx+rax*4]
        vmovups   YMMWORD PTR [rcx+rax*4], ymm3
        vmovups   ymm4, YMMWORD PTR [32+rdx+rax*4]
        vaddps    ymm5, ymm4, YMMWORD PTR [32+r8+rax*4]
        vaddps    ymm0, ymm5, YMMWORD PTR [32+r9+rax*4]
        vaddps    ymm1, ymm0, YMMWORD PTR [32+rbx+rax*4]
        vmovups   YMMWORD PTR [32+rcx+rax*4], ymm1
        add       rax, 16
        cmp       rax, r11
        jb        .B46.4


这展示了英特尔® 编译器的部分自动矢量化能力。 矢量化可通过矢量报告确认(simd 声称编译指令),或者通过检查生成的汇编语言指令来确认。 如果开发人员对其应用有着深刻的了解,那么编译指令能够为编译器提供进一步的帮助。 请参阅 基于英特尔 C++ 编译器的矢量化指南 ,了解更多在英特尔编译器中进行矢量化的信息。英特尔® C++ Compiler XE 12.0 用户与参考指南还介绍了有关矢量化、编译指示和编译器开关的用法。 英特尔编译器可为您完成大部分的矢量化工作,因此您的应用可以随时使用英特尔® AVX。

5. 面向阵列符号 (Notation) 的英特尔® Cilk™ Plus C/C++ 扩展

面向阵列符号的英特尔® Cilk™ Plus C/C++ 语言扩展是专用于英特尔的语言扩展,适用于算法在阵列上运行的情况,不需要阵列元素之间的特定操作顺序。 如果使用阵列符号来表达算法并通过 AVX 开关进行编译,英特尔® 编译器将生成英特尔® AVX 指令。 面向阵列符号的 C/C++ 语言扩展旨在帮助用户在其程序中直接表达高级并行矢量阵列操作。 这可帮助编译器执行数据相关性分析、矢量化和自动并行化。 从开发人员的角度来看,他们将获得更加可预测的矢量化、改进的性能和更高的硬件资源利用率。 通过结合使用面向阵列符号的 C/C++ 语言扩展和其它英特尔® CilkTM Plus 语言扩展,有助于简化并行和矢量化应用开发流程。

要实现上述优势,开发人员可以编写标准的 C/C++ 基本函数,以便通过标量句法来表示操作。 在不使用面向阵列符号的 C/C++ 语言扩展的情况下调用时,该基本函数可用于在一个元素上进行操作, 必须使用"__declspec(vector)"对该基础函数进行声明,以便用户能够通过面向阵列符号的 C/C++ 语言扩展来调用。

multiplyValues 示例作为一个基础函数来展示:


__declspec(vector) float multiplyValues(float b, float c)	
{
	return b*c;
}


该标量调用通过该简单的示例进行说明:


float a[12], b[12], c[12];
a[j] = multiplyValues(b[j], c[j]);


此外,借助面向阵列符号的 C/C++ 语言扩展,该函数还可在整个阵列或阵列的一部分上来操作。 片段操作符 (section operator) 可用于要在其上进行操作的阵列部分。 句法: [<lower bound> : <length> : <stride>]

下限是源阵列的开始索引、长度是结果阵列的长度,跨度表示的是整个源阵列的跨度。 跨度是可选的,默认为 1。

这些阵列部分示例有助于阐释具体的使用方式:


float a[12];

	a[:] refers to the entire a array
	a[0:2:3] refers to elements 0 and 3 of array a.  
	a[2:2:3] refers to elements 2 and 5 of array a
	a[2:3:3] refers to elements 2, 5, and 8 of array a


此外,符号还支持多维阵列。


float a2d[12][4];

	a2d[:][:] refers to the entire a2d array
	a[:][0:2:2] refers to elements 0 and 2 of the columns for all rows of a2d.  
	a[2:2:3] refers to elements 2 and 5 of array a
	a[2:3:3] refers to elements 2, 5, and 8 of array a


借助阵列符号,用户可以轻松地调用使用阵列的 multiplyValues。 英特尔® 编译器提供了矢量化版本,可以分别执行相应的操作。 以下为您列举了部分实例: 第一个示例在整个阵列上操作,第二个则在阵列的一个子集或部分上操作。

该示例调用了整个阵列的函数:


a[:] = multiplyValues(b[:], c[:]);

该示例调用了阵列的一个子集的函数:


a[0:5] = multiplyValues(b[0:5], c[0:5]);

面向阵列标记的 C/C++ 语言扩展可简化阵列应用的开发。 如果采用自动矢量化功能,则需要检查英特尔® 编译器生成的指令,以确认是否正在使用英特尔® AVX 指令。 通过 /S 开关进行编译可生成汇编文件。 搜索 mutliplyValues 将得到标量和矢量化这两个版本。

标量实施使用 VEX 编码 128 位英特尔® AVX 指令的标量 (ss) 版本:


vmulss    xmm0, xmm0, xmm1
        ret  


矢量实施使用 VEX 编码 256 位英特尔® AVX 指令的标量 (ps) 版本:


sub       rsp, 40
        vmulps    ymm0, ymm0, ymm1
        mov       QWORD PTR [32+rsp], r13
        lea       r13, QWORD PTR [63+rsp]
        and       r13, -32
        mov       r13, QWORD PTR [32+rsp]
        add       rsp, 40
        ret


这些简单的示例显示了,面向阵列标记的 C/C++ 语言扩展如何使用英特尔® AVX 的特性,而且不需要开发人员明确地使用任何英特尔® AVX 指令。 无论是否使用基础函数,都可以使用面向阵列标记的 C/C++ 语言扩展。 该技术使用最新的英特尔® AVX 指令集架构,为开发人员提供了更高的灵活性和更多的选择。 请参阅英特尔® C++ Compiler XE 12.0 用户与参考指南 ,了解面向阵列标记的英特尔® Cilk™ Plus C/C++ 语言扩展的更多信息。

开发人员可使用编译器来生成英特尔® AVX 指令,并对其应用进行自动矢量化处理。 此外,他们也可以选择面向阵列标记的英特尔® Cilk™ Plus C/C++ 语言扩展进行开发,以便充分利用英特尔® AVX。 开发团队还可以通过另外一种方式在不编写汇编语言的情况下使用英特尔® AVX。 英特尔® 集成性能基元库(英特尔® IPP)和英特尔® 数学核心函数库(英特尔® MKL)为开发人员带来了许多优势,包括支持英特尔® AVX 等最新的英特尔技术。

6. 使用英特尔® IPP 和英特尔® MKL 库

借助英特尔® 集成性能基元库和英特尔® 数学核心函数库,英特尔针对多媒体、数据处理、加密和通信应用提供了数千个高度优化的软件函数。 这些线程安全库支持多种操作系统,最快的代码将在指定平台上运行。 通过这种方式,用户可以轻松地向应用添加多核并行化和矢量化能力,并利用最新的处理器指令来执行代码。 英特尔® 集成性能基元库 7.0 包括大约 175 个针对英特尔® AVX 而优化的函数。 这些函数可用于执行 FFT、过滤、卷积、重新调整大小等操作。 英特尔® 数学核心函数库 10.2 支持面向 BLASS (dgemm)、FFT 和 VML (exp, log, pow) 的英特尔® AVX。 实施过程在英特尔® MKL 10.3 中得到了简化,因为开始不再需要调用 mkl_enable_instructions。 英特尔® MKL 10.3 可扩展英特尔® AVX,以便支持 DGMM/SGEMM、radix-2 Complex FFT、最真实的 VML 函数以及 VSL 分布生成器。

如果您已经在使用,或者考虑使用这些版本的库,那么您的应用将能够使用英特尔® AVX 指令集。 在 Sandy Bridge 平台上运行时,库将执行英特尔® AVX 指令,并且支持 Linux*、Windows* 和 Mac OS* X 平台。

如欲了解关于针对英特尔® AVX 而优化的英特尔® IPP 函数的更多信息,请访问: http://software.intel.com/zh-cn/articles/intel-ipp-functions-optimized-for-intel-avx-intel-advanced-vector-extensions/

更多有关英特尔支持的信息,请参阅在英特尔 MKL V10.3 中优化英特尔 AVX

7 总结

人们对更高计算性能的需求促使英特尔在微架构和指令集领域不断进行创新。 应用开发人员希望确保他们的产品能够利用技术上的进步,且无需投入更多的开发资源。 本文介绍的方法、工具和库可帮助开发人员从英特尔® 高级矢量扩展指令集的发展上获益,而且无需编写英特尔® AVX 汇编语言。

8. 更多信息和参考资料

英特尔高级矢量扩展指令集

英特尔编译器

英特尔 C++ Composer XE 2011 - 文档

如何面向英特尔 AVX 进行编译

基于英特尔 C++ 编译器的矢量化指南

面向英特尔高级矢量扩展优化的英特尔集成性能基元函数

支持在英特尔 MKL 中优化英特尔高级矢量扩展

在英特尔 MKL V10.3 中优化英特尔 AVX

表 1 – 英特尔® 编译器命令行开关总结

英特尔® 编译器命令行开关描述

Windows*

Linux*

Mac OS* X

如果存在性能优势,则可以针对英特尔处理器生成多个特定处理器的自动分布代码路径

/Qax

-ax

-ax

生成矢量化报告

/Qvec-report<n>

-vec-report<n>

-vec-report<n>

生成汇编语言文件

/S

-S

-S

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