为采纳AltiVec而进行的向量化过程中的大多数任务—重构数据结构,设计并行算法,消除分支,等等—和Intel架构上需要执行的任务是一样的。如果您已经有了AltiVec代码,就已经完成了基础性的向量化工作,这些工作也是为Intel架构重写应用程序所需要的。在很多情况下,对AltiVec指令的翻译过程是平滑的,直接或者近乎直接将它代替为Intel的等价指令就可以了。
然而,在这两种架构之间存在很多重要的差别。在您开始为Intel指令集架构重写AltiVec指令之前,请阅读“指令集架构间的差别”部分的内容。如果这些差别对您的代码影响很大,则您可能可以考虑简单地重写代码,直接改用Accelerate框架。
如果您确定为Intel ISA重写代码是最好的选择,则需要阅读“编程模型”的内容,该文档讨论的是Intel架构上的向量代码的总体编程方法,并提供一些一般性的策略。在您已经做好连编代码的准备时,请参见“连编x86 ISA的代码”部分,以获得关于编译器和其它重要设定的信息。阅读“对齐数据”部分可以熟悉在Intel架构上是如何完成对齐的,并且可以知道应该对代码采取什么策略。
指令集架构间的差别
AltiVec指令集架构和Intel指令集架构之间的差别决定了将代码从一个架构移动到另一个架构需要付出多少努力。两个架构之间有如下这些关键的差别:
-
整数乘的算法不等价。
AltiVec有约13种不同风格的整数乘,不同风格之间有一些变化。x86架构则有3种乘法,并且几乎没有变化。在某些特定的情况下,为利用AltiVec multiply-accumulate(乘-累计)便利工具而设计的算法需要重写,才能利用到x86的乘法。
-
vec_perm
没有直接的等价翻译。在x86平台上无法执行互换操作,因为在编译时无法知道互换映射(permute map)。某些字节互换也是不可能进行的。诸如在一个SIMD寄存器上进行字节交换,将互换单元用作查找表,以及使用互换单元来处理对齐这些操作在x86平台上都不能工作,或者需要数量惊人的计算。
对于象小型查找表这样严重依赖于
vec_perm()
函数的AltiVec代码,向量化也许是不可能的。有一个和互换相类似的移动(shuffle)工具(SHUFPS
,SHUFPD
,PSHUFD
,PSHUFLW
,PSHUFHW
)。然而,交换映射必须在编译时确定,这意味着不能在运行时确定如何移动(shuffle)数据。 -
不对齐向量的装载在各个架构上的处理也有所不同。
AltiVec没有不对齐的向量装载和存储操作。x86架构上不存在可以在运行时使用的左移操作(有左移操作,但是左移的数值必须在编译时设定,这种操作不能用于使不对齐的向量装载操作对齐)。
这意味着如果您有操作不对齐的数据结构的AltiVec 代码,就必须修改。在SSE/SSE2中,没有可接受的方法能在运行时确定不对齐向量的128位左移或者右移应该移动多少。因此,即便是两种架构上都有16字节的对齐向量装载操作,在x86上您还是不能从两个相邻的经典AltiVec风格的装载操作中快速抽取出不对齐的向量。所有这样的移位指令都接受表示移动距离的直接参数(immediate arguments),并且必须在编译时提供。不对齐的向量装载和存储操作必须显式地通过
MOVDQU
,MOVUPS
,MOVUPD
,和MASKMOVDQU
来进行。在SSE3中,您可能需要使用LDDQU
来进行不对齐的整数向量装载。所有的其它装载和存储操作,包括立即内存操作,都必须是16字节对齐的。这也意味着需要对循环结构进行实质性的修改,以避免对长度不为16字节倍数的数组进行越限读取。如果需要更多的信息,请参见“对齐数据”部分。 -
当向量装载和存储操作发生的时候,整个向量需要以整个16字节互换的方式进行字节交换。
向量中元素的排列顺序相对于内存顺序是相反的,如图5-1所示。不仅字节顺序相反,元素的顺序也相反。在执行互换和128位移位操作的时候,需要考虑重新排列和字节交换。
图 5-1:向量元素内存中的顺序和寄存器顺序的比较
-
执行and-with-complement(增补与)操作的
ANDN
指令是vec_andc()
函数的前身。增补部分(complement)由第一个参数来表示,而不是第二个参数。这通常意味着您的掩码会被破坏。该操作实现下面的运算:
A = ~A & B
-
没有融合在一起的multiply-add(乘-加)操作。
-
在x86上没有和用于生成向量常数的
vec_splat_u8()
及vec_lvsl()
相对应的操作。大多数的向量常数必须从存储上装载。一部分常数,比如0
和–1
,可以通过灵活地应用XOR
和向量比较指令来创建。
编程模型
在您将代码迁移到Intel架构时,不需要用汇编语言来书写代码。Intel为MMX,SSE,SSE2,和SSE3提供一个C语言编程模型。编译器提供了相当于AltiVec内在函数(intrinsic functions)的x86内在函数。内在函数列表中以_mm_*
开头的函数类似于AltiVec中的vec_*
函数。
举例来说,下面所示的AltiVec加法操作:
|
在SSE中实现为:
|
即便是ADDPS
指令可能带来问题(从经验上看,b和a应该是位于同一个寄存器中),在C语言中也可以工作。编译器首先发出一个MOVAPS
指令进行寄存器到寄存器间的拷贝,以保留b变量的值,备将来之用。
在AltiVec中常见的操作数分类在很大程度上都是可用的。SSE和SSE2也有全精度除和平方根指令。SSE和SSE2缺少AltiVec向量复杂整数单元中具有的multiply-add(乘-加)融合内核及大多数特殊目的的multiply-add和multiply-sum(乘-求和)整数指令,也缺少与vec_sel
相类似的东西,但是您可以轻易地用Boolean操作符来代替,如下所示:
result = a & mask | b & ~mask
浮点代码翻译得特别好。如果您采用reroll long的方式,性能通常会好一些,unrolled AltiVec有一些循环。
在Intel内部用后缀来指示被操作的数据类型。如表5-1所示。
表5-1 : Intel内部函数的后缀及其对应的数据类型
后缀 | 数据类型 |
---|---|
| 有符号的8位整数 |
| 有符号的16位整数 |
| 有符号的32位整数 |
| 无符号的8位整数 |
| 无符号的16位整数 |
| 128位整数 |
| 包装型单精度浮点数 |
| 数量型单精度浮点数 |
| 包装型双精度浮点数 |
| 数量型双精度浮点数 |
和AltiVec类似,Intel内部有一个专用向量数据类型的列表,如表 5-2。所有的整数类型共用__m128i
类型。
表5-2 : 等价SSE2/SSE和AltiVec数据类型
SSE/SSE2 | AltiVec 的对应类型 |
---|---|
| 向量浮点数 |
| 向量双精度数(如果AltiVec有的话) |
| 向量(signed/unsigned/bool) (char/short/long) |
虽然您可以使用表5-2中列举的SSE/SSE2数据类型,但是最好还是使用表5-3的数据类型。您既可以在PowerPC也可以在x86上使用这些类型。使用这些类型可以编写同时工作在两种处理器上的代码。在AltiVec中没有64位和双精度类型的硬件等价类型。那些名称在AltiVec s中存在,您可以使用,但是不能对这些类型执行任何数学运算。
在使用表5-3列出的类型之前,必须包含Accelerate框架的框架头文件。您不必连接框架,而只是简单地在代码中包含下面的语句就可以:
#include <Accelerate/Accelerate.h>
您可以将表5-3中的数据类型和Intel风格的内部函数,比如_mm_slli_epi32( __m128i v, imm )
函数一起使用。如果您混用了浮点数类型,双精度类型,和整数类型,编译器就会产生相应的警告信息,但是如果只混用了表5-3中的带有“整型数性质”的类型—即只在整数类型之间混合使用,则编译器不会发出警告信息。
表5-3 : 向量数据类型的苹果名称和Intel名称
包装类型的向量 | 苹果名称 | Intel名称 |
---|---|---|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
参见“AltiVec指令的x86等价指令”部分的内容可以得到一个等价指令的扩展列表。在Intel架构软件开发者手册,卷2:指令集参考,附录C中可以找到Intel内部函数的完全列表。请注意,某些MMX和SSE2指令共用相同的名称。
连编x86 ISA的代码
您需要通过Xcode 2.1来为采用Intel微处理器的Macintosh连编x86 ISA的代码。编译器在缺省情况下支持使用MMX,SSE,和SSE2 指令。编译器也支持SSE3,如果要打开SSE3的支持,请向编译器传入-msse3
标志。如果要使用Intel SIMD C的内部函数,则必须同时包含恰当的头文件,如表 5-4所示。
在缺省情况下,Max OS X的AltiVec会将denormals清零。而x86架构上的denormals则不是这样。如果您的代码可能频繁地遇到denormals,则为了在采用Intel微处理器的Macintosh上得到最好的性能,您将需要将MXCSR
寄存器中的FZ
和DAZ
位关闭,如列表5-1所示。
一个典型的,输入经过正常化处理(normalized)的向量加(ADDPS
)操作需要开销3到5个时钟周期,取决于您采用的时钟周期的计算方法。对一个没有经过正常化处理(denormalized)输入执行一个ADDPS
操作则需要大约1500个时钟周期。将FZ
和DAZ
位打开,会使所有denormals在执行运算之前被转化为零。在运算中产生的Denormals结果会被清零。
列表5-1 : 打开FZ和DAZ位的代码
|
对齐数据
数据对齐对于能否使代码获得最好性能是相当重要的。不对齐的数据运算起来会比对齐了的数据慢ᾰ有时会超过十倍。AltiVec与x86架构的SIMD对齐技术使用非常不同的方式来解决对齐问题。通过阅读这个部分,您可以理解这些差别,并获得如何解决对齐问题的指导原则。
AltiVec只能执行对齐的装载操作,并且依赖于一个已申报专利的互换闩(permute crossbar)技术来从被括号包围着的两个对齐向量中抽取不对齐的向量。另一方面,x86架构上提供对齐向量的装载操作,没有互换操作。长向量的左移和右移需要一个直接参数,且该参数的值必须在编译时确定。移动指令也是采取同样的方式进行处理。因此,软件对齐是很难实现的。x86架构为不对齐的装载和存储操作提供了硬件支持,来作为代替的方法。
请注意:由malloc
函数分配的内存总是16字节对齐的,这和堆栈中及作为函数参数传递的__m128
,__m128i
,和__m128d
数据类型一样。
为不对齐的装载和存储操作提供的硬件支持并不能使对齐问题得到解脱,它引入了一些自身特有的困难。特别值得一提的是,您必须特别注意避免越过数组的边界对没有映射的内存进行读取。在编写AltiVec代码的时候,由于所有的向量都是16字节对齐的,如果您知道向量中至少有一个字节是正当的,已经被映射到内存空间,则整个向量就可以安全被装载了。
内存被映射为一些对齐的4KB页面。一个对齐的16字节向量永远不会跨越页面的边界。如果向量中的一个字节已经存在于某个页面,则整个16字节的对齐向量肯定也位于同样的页面上,因此也是存在的。这种推断对于不对齐的装载就不正确了。在不对齐的装载操作中,一个向量可能跨越页面的边界。在装载或者写入向量之前,您必须确保向量中所有的字节都是存在的。图5-2说明了不对齐数据的问题。请注意,图中的最后一个不对齐向量包含一些可能没有被映射到内存中的字节。如果某些未知字节还位于没有被映射的页面上,则对这个向量执行不对齐的装载或者存储操作会导致崩溃。
图 5-2 : 不对齐的数据

这个限制在数组的尾部会导致下面的问题:
-
如果不对齐数组的长度不是16字节的倍数,则在原始的实现中,最后一个向量的装载和存储操作将包含超出数组边界部分的字节。您可以通过下面的方法来解决这个问题:
-
在数量方式的代码中对最后几个字节进行计算。举例来说,图5-3中位于扩展到向量之外的数据可以通过数量代码来处理。
图 5-3:扩展到数组之外的字节
-
通过数量方式的装载和存储操作将数据载如寄存器,并在向量单元中进行计算。这可能会有一些微小的性能优势。
-
书写仅使用对齐装载操作的计算代码,即使数据是不对齐的,也用对齐装载操作处理,如图5-4所示。使用您接收到的数据,而不管其是否对齐,因为在寄存器中无法解决这个问题。这意味着某些从数组的结束部分装载的数据是没有意义的,您必须正确地丢弃这些数据,并使这些被丢弃的数据不对结果产生影响。在数组的边界上使用
MASKMOVDQU
指令,只对存在的字节进行存储(请参见图 5-8及相关的讨论)。对于AltiVec,请注意不要装载全部为空的向量。图 5-4: 两端都带有未知数据的非对齐数据
-
提前一个周期退出循环,然后将指针回退合适数目的字节,使循环中最后一个装载和存储操作终止于数组的最后一个字节,然后再进行最后一次循环。这种方法会在最后一次循环结果的开头部分和前一个循环结果的结尾部分产生一些重叠,如图 5-5所示。如果输入相同的计算总是产生同样的结果,则您会以同样的结果将那些值有效地覆盖了,而覆盖不应该是个问题。您需要提供代码来处理整个数组长度比一个循环所需长度还要小的特殊情况。
图 5-5 :处理不对齐数据的回退方法
-
-
您不能通过对整个向量执行不对齐装载操作来安全装载小于16字节的标量或者其它数量。如果您使用一个含有标量的对齐向量,可能可以被安全装载,但是一般情况下,没有好的方法可以将标量移动到一个已知且有意义的位置上。完成这个任务的最好方式是将标量装载到一个对齐方式已知的寄存器中。有几种方法可以将部分的变量转移到一个xmm寄存器上。
-
对于部分向量的存储操作,请使用
MASKMOVDQU
指令,如图5-8所示。这个指令根据一个掩码对不对齐的向量进行存储,且只存储那些不被掩盖住的字节。很快就可以为数组结束部分的不对齐数据区创建MASKMOVDQU
掩码,首先将一个全部字节都为零的向量对齐存储在一个字节都为0xFF
的向量的相邻位置,然后从中间区域对期望的映射执行一个不对齐的装载操作。下面的代码显示如何创建一个掩码,开头的N个字节的掩码为
0xFF
,接着是0x00
。__m128i CreateTrailingStoreMask( int N )
{
__m128i mask[2];
//Set second vector to 0
mask[1] = (__m128i) _mm_setzero_ps();
//Set first vector to ?1
mask[0] = _mm_cmpeq_epi8( mask[1], mask[1] );
return _mm_loadu_si128( (__m128i*) ((char*) &mask[1] ? N ));
}
如果数组的长度是16字节的倍数,则您可以使用这个掩码的补码来进行开头部分向量的存储。
图 5-8 : 使用MASKMOVDQU进行部分向量的存储
-
没有与8位及16位类型的
vec_splat
相对应的函数。需要先使用PUNPACK*
将8或16位的int
连接为32位的数量,然后用PSHUFD
/SHUFPS
/SHUFPD
指令来和向量的剩下部分相连。