单指令流多数据流(SIMD,即 Single Instruction, Multiple Data)是一种采用一个控制器来控制多个处理器,同时对一组数据中的每一个分别执行相同的操作从而实现空间上的并行性的技术。这里提到的一组数据又称向量。
AltiVec 是由 Apple、IBM、Motorola PowerPC 联合开发的单指令多数据指令集,每个公司使用不同的名字在市场上进行销售。IBM 把对 AltiVec 的实现和扩展称为 VMX,即 Vector/SIMD Multimedia Extension,而后又进行了进一步扩展推出了 VSX,即 Vector Scalar Extension。
使用向量技术能够获得更快的执行速度,对于类似多媒体这种大量使用算法的应用中效果尤其显著。
开发者在使用 XL C, XL C/C++和 XL Fortran 等编译器时可以使用向量技术。本文主要以 XL C/C++ 编译器为例介绍如何使用向量技术。
回页首
单指令流多数据流是由处理器支持的,IBM 的 POWER 系列处理器自 POWER6 开始支持 VMX 向量技术,并随后扩展到了 VSX,详见表 1。
表 1.POWER 架构支持的向量技术
架构 | 支持的向量技术 |
---|---|
POWER5 | N/A |
POWER6 | VMX |
POWER6 in enhanced mode | VMX |
POWER7 | VMX, VSX |
POWER8 | VMX, VSX |
回页首
使用向量技术的必要条件是在编译时指明以下两个编译选项:
使用-qarch 编译选项,而且必须指向某个支持向量技术的处理器架构,如-qarch=pwr7,这就要求编译环境中处理器本身支持向量技术。开发者可以使用 less /proc/cpuinfo 命令在 Linux 平台查询处理器型号。
使用-qaltivec 编译选项,否则编译器无法识别下文提到的向量类型和向量操作符。
用户可以使用 vector 或_vector 关键字再加上要声明的变量类型来声明向量类型,例如 vector signed char。除了计算机的基本变量类型,例如 signed char,还可以使用 pixel 类型。pixel 是向量技术中的一个新的关键字,一个 pixel 变量占用 2 字节,并分为 4 部分,第一部分占 1 比特,其余三部分分别占 5 比特。
所有向量类型都占 16 字节,根据其变量类型不同,每种向量可能包括 2,4,6,8,16 个元素。例如,每个 unsigned char 和 signed char 占 1 字节,所以 vector unsigned char 和 vector signed char 包含 16 个对应的基本类型的变量;而每个 unsigned int 和 signed int 占 4 字节,所以 vector unsigned int 和 vector signed int 包含 4 个对应的基本类型的变量。
表 2.向量类型对应的基本类型
向量类型 | 包含内容 |
---|---|
vector unsigned char | 16 unsigned char |
vector signed char | 16 signed char |
vector bool char | 16 unsigned char |
vector unsigned short | 8 unsigned short |
vector unsigned short int | |
vector signed short | 8 signed short |
vector signed short int | |
vector bool short | 8 unsigned short |
vector bool short int | |
vector unsigned int | 4 unsigned int |
vector unsigned long | |
vector unsigned long int | |
vector signed int | 4 signed int |
vector signed long | |
vector signed long int | |
vector bool int | 4 unsigned int |
vector bool long | |
vector bool long int | |
vector unsigned long long | 2 unsigned long long |
vector bool long long | |
vector signed long long | 2 signed long long |
vector float | 4 float |
vector double | 2 double |
vector pixel | 8 unsigned short |
对于上述的某些类型,如 vector unsigned long long, vector bool long long, vector signed long long 和 vector double,处理器必须支持 VSX,如 POWER7 及以上处理器。
开发者可以通过向量常量(vector literal)对向量类型进行初始化。向量常量的类型就是第一个括号中的向量类型,其值是第二个括号中一系列的常量表达式表示(如果只有一个常量表达式,则所有向量元素都被赋为该值)。除此之外,还可以使用其他已赋值的向量进行赋值。参考例 1。
而对于 C 的程序,开发者还可以用初始化列表。初始化列表以花括号为界,列表中值的个数必须小于或等于向量类型中的元素个数。未指明的元素被初始化为 0。参考例 2。
总体而言,向量类型支持以下适用于基本类型的操作符。
一元操作符:++,--,+,-,~
二元操作符:+,-,*,/,%,&,|,^,<<,>>,[]
关系操作符:==,!=,<,>,<=,>=
然而,具体的支持情况由向量类型决定,如 bool vector 就不支持加减乘除或大于小于等操作符。
这里特别要说明的是下标运算符[]是可以用于向量类型操作的,这让向量类型更像传统意义上的数组。 通过下标运算符就能选中向量中的某个元素。如果所指定的位置超出了有效区间,则结果是未定义的。参考例 1 和例 2 对[]操作符的运用。
清单 1
#include <stdio.h> int main() { vector unsigned int v1 = (vector unsigned int)(5,10,15,20); vector unsigned int v2 = (vector unsigned int)(10); vector unsigned int v3 = v2; printf("%i,%i,%i,%i/n",v1[0],v1[1],v1[2],v1[3]); printf("%i,%i,%i,%i/n",v2[0],v2[1],v2[2],v2[3]); printf("%i,%i,%i,%i/n",v3[0],v3[1],v3[2],v3[3]); return 0; } 结果: 5,10,15,20 10,10,10,10 10,10,10,10
清单 2
#include <stdio.h> int main() { int i; vector unsigned int v2 = {1,2,3,4}; for (i=0;i<4;i++) printf("%i/n",v2[i]); return 0; } 结果: 1 2 3 4
开发者可以利用内置的向量函数来操作向量类型的元素,比如对多个向量进行运算、比较两个向量、将向量从内存中加载到寄存器中、将寄存器中的向量储存到内存中。
除了支持 AltiVec Technology Programming Interface Manual 中提及的函数,IBM 的 XL 编译器还进行了扩展, 提供了更多的函数来更有效操作向量。开发者可以在程序中直接调用这些向量函数。
参考例 3 中对 vec_cmpeq()函数的使用。
清单 3
#include <stdio.h> int main() { int i; vector unsigned char v1 = {'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p'}; vector unsigned char v2 = (vector unsigned char) 'a'; vector bool char v3; v3 = vec_cmpeq(v1,v2); for (i=0;i<16;i++) printf("%i/n",v3[i]); return 0; } 结果: 255 0 0 0 0 ...
除了上文提到的[ ]操作符来对向量内元素进行操作,还可以通过以下访问某个指定的向量元素并进行操作。
d=vec_extract(a, b) 返回 vector b 中的第 a 个元素 d=vec_insert(a, b, c) 把 vector b 中的第 c 个元素换成 a 值 d=vec_promote(a, b) 返回元素 b 为 a 值的 vector,其余元素都未定义 d=vec_splats(a) 返回所有元素均为 a 值的 vector
开发者可以通过编译器的用户文档了解向量函数的更多内容。
IBM 的 Mathematical Acceleration Subsystem(数学加速子系统,简称 MASS)包括了一系列针对 IBM 特定处理器调优的数学函数,搭载于在 XL C, XL C/C++ 和 XL Fortran 编译器中。MASS 库函数提供了比标准数学库函数更优化的性能。具体而言,MASS 库包括了标量 C/C++函数库,向量函数库和 SIMD 函数库。
用户可以显式调用这些函数,步骤如下:
在程序中使用 MASS 库函数。
了解库函数的命名规则有助于开发者使用这些函数。
对于双精度函数:标量函数库沿用了函数基础名称,向量函数库在函数基础名称上加了 v 前缀,而 SIMD 函数库在函数基础名称上加了 d2 后缀。
对于单精度函数:标量函数库在函数基础名称上加了 f 后缀,向量函数库在函数基础名称上加了 vs 前缀,而 SIMD 函数库在函数基础名称上加了 f4 后缀。
我们以 acos()函数为例。acos(x)函数返回 x 的 acosin 值。
表 3.acos 函数对应的 MASS 标量函数,MASS 向量函数和 MASS SIMD 函数
双精度 | 单精度 | |
---|---|---|
标量函数 | acos() | acosf() |
向量函数 | vacos() | vsacos() |
SIMD 函数 | acosd2() | acosf4() |
在程序中包含对应的头文件。
在编译时链接对应的库。例如如果需要链接 libmass.a,则编译时还需要在命令行再输入 -lmass 选项。
以 XL C/C++ for Linux, 13.1 对应的头文件和库为例:
表 4.XL C/C++ for Linux, 13.1 中 MASS 函数库对应的头文件和库文件
头文件名称 | 库名称及说明 | ||
---|---|---|---|
标量函数库 | math.h | l ibmass.a | 针对 32 位处理器 |
libmass_64.a | 针对 64 位处理器 | ||
向量函数库 | mathv.h | libmassvp5.a | 已针对 POWER5 调优 |
libmassvp6.a | 已针对 POWER6 调优 | ||
libmassvp7.a | 已针对 POWER7 调优 | ||
libmassvp8.a | 已针对 POWER8 调优 | ||
SIMD 函数库 | math_simd.h | libmass_simdp7.a | 针对 32 位 POWER7 处理器并调优 |
libmass_simdp7_64.a | 针对 64 位 POWER7 处理器并调优 | ||
libmass_simdp8.a | 针对 32 位 POWER8 处理器并调优 | ||
libmass_simdp8_64.a | 针对 64 位 POWER7 处理器并调优 |
和下文提到的代码自动向量化,开发者在代码中直接使用向量技术能精准地利用向量技术的优势,而且通常能避免无法预料的结果。但是手动使用向量技术编程对开发者要求较高,而且可能需要系统性地进行代码改造。
除了手动编程方法,开发者还可以在编译时启用对应的编译选项将程序自动向量化。
-qsimd 控制了编译器是否能自动利用处理器所支持的向量指令。
-qsimd=auto 将某些循环中的对数组中连续元素的操作转化为 VMX 或 VSX 指令。向量指令能一次性计算多个结果,比顺序执行要快。这对于含有大量图像处理的程序非常有用。
当-qhot=vector 生效时,对于某些对于数组中连续元素的循环操作,如求平方根或反平方根,编译器会调用 MASS 库函数中对应的函数。-qhot=vector 支持单精度和双精度浮点数学运算,所以对于需要处理大量数学运算的程序非常有用。
当-qhot=vector 生效时,在程序调用系统数学函数时,编译器通过衡量函数名、调用次数等确定是否进行自动向量化,如果是,则转化为调用对应的 MASS 向量函数,某些函数如 vdnit,vdint,vcosisin 除外。如果无法向量化,编译器会尝试调用对应的 MASS SIMD 函数。
《 怎样提高调用数学函数的程序的性能 》介绍了如何通过自动向量化使用 MASS 高性能数学库,并展示了加速后的效果。
需要注意的是,由于向量化可能影响程序结果的精确性,如果这种精确性偏差超出了接受范围,则开发者可以用-qhot=novector 来关闭上述操作。
除了由开发者显示开启编译选项,XL 编译器将一些有利于提高性能的优化操作打包起来,提供从 O2 到 O5 的预定义优化等级,供开发者在不熟悉如何组合各种编译选项来提高性能时使用。例如,-O4 和-O5 优化等级包含了-qhot=vector 和-qsimd=auto;对于 POWER7 及以上的处理器,-O3 -qhot 优化等级已包含了-qsimd=auto。
通常,更高的优化等级是在其上一个优化等级的基础上新增了其它编译选项。
当以下编译选项或编译选项组合生效时,-qhot=vector 也被启用。
回页首
字节顺序是指当数据存储跨多个字节时最高有效字节和最低有效字节在内存中的存放方式。在大端模式下,数据的最高有效字节存放在低地址;在小端模式下,数据的最低有效字节存放在低地址。例如一个跨 4 个字节的 32 位 int 型数 0x12345678,其最高有效字节是 0x12,最低有效字节是 0x78,它在 CPU 内存中有两种存放方式,此处假设从地址 0x0000 开始存放:
表 5.内存存放布局(方式 1:大端模式 )
地址 | 0x0000 | 0x0001 | 0x0002 | 0x0003 |
---|---|---|---|---|
存放内容 | 0x12 | 0x34 | 0x56 | 0x78 |
表 6.内存存放布局(方式 2:小端模式 )
地址 | 0x0000 | 0x0001 | 0x0002 | 0x0003 |
---|---|---|---|---|
存放内容 | 0x78 | 0x56 | 0x34 | 0x12 |
方式 1 的存放形似称为大端模式,最高有效字节 0x12 存放在低地址中,方式 2 的存放形似称为小端模式,最低有效字节 0x78 存放在低地址中。
这两种模式并没有本质上的区别,大多数开发者并不用关心具体的细节。但如果涉及代码在两种模式下移植时,就需要格外注意。
常见的大端模式的机型有:IBM Power 系列、IBM z 系列
常见的小端模式的机型有:Intel x86 系列
值得一提的是,IBM Power8 既支持大端模式,也支持小端模式。
回页首
由于所有向量变量都占 16 字节并以 16 字节对齐,这决定了在内存中是向量是跨字节存储的。向量的存储在大端模式下和小端模式下不同,让我们来看一个简单的例子:
初始化向量 v,v 含有四个元素:
例 4
vector unsigned int v = {1,2,3,4};
向量元素在内存中存放布局如图 2 所示:
表 7.向量元素在内存中的存放布局(大端模式)
地址 | ADD | ADD+0x1 | ADD+0x2 | ADD+0x3 |
---|---|---|---|---|
向量元素 | v[0] | v[1] | v[2] | v[3] |
元素值 | 1 | 2 | 3 | 4 |
表 8.向量元素在内存中的存放布局(小端模式)
地址 | ADD | ADD+0x1 | ADD+0x2 | ADD+0x3 |
---|---|---|---|---|
向量元素 | v[3] | v[2] | v[1] | v[0] |
元素值 | 4 | 3 | 2 | 1 |
若程序需要在大端模式和小端模式间进行移植,开发者需要关注字节顺序不同带来的变化,可能需要进行某些转换以保证数据的正确性。总体而言,字节存储顺序对大多数向量函数并没有影响,比如当使用 vec_add(a,b)求向量 a 和向量 b 对应元素的和,在大端模式和小端模式上并无区别。然而,以下三类函数会受到向量元素顺序的影响:
加载函数
存储函数
指向某个向量元素的函数
回页首
现在,小端模式有着越来越好的生态圈,IBM 针对 Linux 也推出了小端模式,开发者可能希望把已有的基于大端模式的程序移植到小端模式平台。为了让开发者花最小的代价进行这种移植,XL C/C++ for Linux (little-endian distributions), 13.1.1 版本编译器新增了一个编译选项-qaltivec=be。
-qaltivec=be 表示内存中向量元素的顺序遵循大端模式。在移植遵循大端模式的已有代码时,这个编译选项不改变向量元素在内存中存储的字节顺序,即内存依然沿用移植前的大端模式,即对例 4 中向量 v 的存储应如图 3 所示,只是在调用上述提到的与字节顺序有关的向量函数时,对向量元素顺序进行处理以符合现在的小端模式环境。在移植后,现在的编译环境是小端模式,即向量元素从高地址向低地址依次排列,在-qaltivec=be 编译选项生效时:
加载函数
使用 vec_ld(), vec_xld2(), vec_xlw4(), vec_xl()把数据从内存加载到向量寄存器时,编译器会逆转向量元素的顺序,即寄存器中向量会以小端模式储存,符合处理器的小端模式字节顺序。以图 4 为例。
表 9.内存中向量存储(内存(移植前的大端模式))
地址 | ADD1 | ADD1+0x1 | ADD1+0x2 | ADD1+0x3 |
---|---|---|---|---|
向量元素 | v[0] | v[1] | v[2] | v[3] |
元素值 | 1 | 2 | 3 | 4 |
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 小端模式下字节顺序 <<<<<<<<<<<<<<<<<<<<<<<
移植到小端模式平台后,假设寄存器直接沿用以上布局,按照小端模式字节顺序,向量元素排序变成 4,3,2,1,与程序意图不符。
启用-qaltivec=be 后,编译器会逆转向量元素的顺序,即寄存器中向量会以小端模式储存,寄存器的布局变为图 4 所示:
表 10.-qaltivec=be 下寄存器中向量存储
向量元素 | v[0] | v[1] | v[2] | v[3] |
---|---|---|---|---|
元素值 | 4 | 3 | 2 | 1 |
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 小端模式下字节顺序 <<<<<<<<<<<<<<<<<<<<<<<
我们看到启用-qaltivec=be 后寄存器的布局向量元素的顺序发生了逆转,按照小端模式,向量元素排列变成 1,2,3,4,符合程序意图。
存储函数
使用 vec_st(), vec_xstd2(), vec_xstw4(), vec_xst()把数据从向量寄存器储存到内存时,编译器会逆转向量元素的顺序,即内存中向量会以大端模式储存,即从图 5 向图 6 进行转换,从而符合内存中原本的大端模式字节顺序。
表 11.寄存器中向量存储
向量元素 | v[0] | v[1] | v[2] | v[3] |
---|---|---|---|---|
元素值 | 4 | 3 | 2 | 1 |
表 12.-qaltivec=be 下内存中向量存储
地址 | ADD2 | ADD2+0x1 | ADD2+0x2 | ADD2+0x3 |
---|---|---|---|---|
向量元素 | v[0] | v[1] | v[2] | v[3] |
元素值 | 1 | 2 | 3 | 4 |
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 大端模式下字节顺序 >>>>>>>>>>>>>>>>>>>>>>>>
指向某个向量元素的函数
对于 vec_extract(), vec_insert(), vec_mergeh(), vec_mergel(), vec_pack(), vec_promote(), vec_splat(), vec_unpackh(), vec_unpackl(), vec_erm(),编译器会以大端模式字节顺序计算元素位置,使之符合向量元素的正确顺序。
-qaltivec=be 只能影响上述提到的向量函数,对于用 union 初始化向量的情况不能产生影响,参考例 5,在小端模式下 union 初始化向量始终按照小端模式。开发者应该用 vec_xl 或 vec_ld 加载函数替代 union,以保证与后续操作一致,即同时受-qaltivec=be 影响。
清单 5
int main() { union { vector signed int a4; int c4[4]; } x; x.c4[0] = 0; x.c4[1] = 1; x.c4[2] = 2; x.c4[3] = 3; }
对于在小端模式下新开发的程序,开发者应该直接使用默认的小端模式,即使用-qaltivec 或-maltivec 编译选项,其默认值就是符合小端模式,开发者不要使用以上提到的-qaltivec=be 进行任何转换,否则可能出现错误,参考例 6。
清单 6
#include <stdio.h> int main() { vector signed int a4; int c4[4] = {0,1,2,3}; int i; a4 = vec_xlw4(0, c4); //vec_xlw4 将会受到 -qaltivec=be|le 影响 printf("%i %i %i %i/n", a4[0], a4[1], a4[2], a4[3]); //在小端模式下,[]操作符不受-qaltivec=be|le 影响,向量元素的始终从高地址向低地址依次排列 for(i=0;i<4;i++) printf("%i ", vec_extract(a4,i)); //vec_extract 将会受到 -qaltivec=be|le 影响 printf("/n"); }
这里我们使用的是 POWER7 小端模式处理器,分别使用-qaltivec=be 和-qaltivec(等同于-qaltivec=le)进行编译,结果如下:
xlc altivec_1.c -qaltivec=be -qarch=pwr7 a.out 3 2 1 0 0 1 2 3 xlc altivec_1.c -qaltivec -qarch=pwr7 a.out 0 1 2 3 0 1 2 3
我们可以看到启用-qaltivec=be 后打印出的第一行的向量元素顺序被颠倒了。这是因为 vec_xlw4 受-qaltivec=be 影响,在把向量从内存加载到寄存器时逆转了向量元素顺序,按照大端模式进行存储,但是由于[]操作符不受-qaltivec=be 影响,始终按照小端模式计算向量元素位置,所以打印时向量元素顺序被颠倒了,与程序意图不符。
启用-qaltivec=be 后打印出的第二行的向量元素顺序正确,这是因为 vec_extract 和 vec_xlw4 一样受-qaltivec=be 影响,在计算向量元素位置时也是按照大端模式,相当于“负负得正”,也符合了程序的意图。
在编程时开发者可能不会意识到[]操作符和 vec_extract()的在启用-qaltivec=be 时存在的差别,最好的办法就是在小端模式下遵循-qaltivec 的默认值,始终可以得到正确的结果,如例 6 的编译结果所示。
对于 XL C/C++ for Linux (little-endian distributions), 13.1.1 版本,开发者只能使用-qaltivec 来开启 be 子选项,以后其 GCC 对应的选项-maltivec 也会新增 be 子选项,方便开发者使用。
衷心感谢 IBM 工程师郭久福(Jeff Guo)审阅本文并提出宝贵建议!