SSE指令集学习:Compiler Intrinsiccss
https://blog.csdn.net/brookicv/article/details/52295043html
大多数的函数是在库中,Intrinsic Function却内嵌在编译器中(built in to the compiler)。nginx
Intrinsic Function做为内联函数,直接在调用的地方插入代码,即避免了函数调用的额外开销,又可以使用比较高效的机器指令对该函数进行优化。优化器(Optimizer)内置的一些Intrinsic Function行为信息,能够对Intrinsic进行一些不适用于内联汇编的优化,因此一般来讲Intrinsic Function要比等效的内联汇编(inline assembly)代码快。优化器可以根据不一样的上下文环境对Intrinsic Function进行调整,例如:以不一样的指令展开Intrinsic Function,将buffer存放在合适的寄存器等。
使用 Intrinsic Function对代码的移植性会有必定的影响,这是因为有些Intrinsic Function只适用于Visual C++,在其余编译器上是不适用的;更有些Intrinsic Function面向的是特定的CPU架构,不是全平台通用的。上面提到的这些因素对使用Intrinsic Function代码的移植性有一些很差的影响,可是和内联汇编相比,移植含有Intrinsic Function的代码无疑是方便了不少。另外,64位平台已经再也不支持内联汇编。算法
VS和GCC都支持SSE指令的Intrinsic,SSE有多个不一样的版本,其对应的Intrinsic也包含在不一样的头文件中,若是肯定只使用某个版本的SSE指令则只包含相应的头文件便可。数组
引用自:http://www.cnblogs.com/zyl910/archive/2012/02/28/vs_intrin_table.html架构
例如,要使用SSE3,则app
#include <tmmintrin.h>
若是不关心使用那个版本的SSE指令,则能够包含全部函数
#include <intrin.h>
Intrinsic使用的数据类型和其寄存器是想对应,有学习
甚至AVX-512指令集有512位的寄存器,那么相对应Intrinsic的数据也就有512位。
具体的数据类型及其说明以下:优化
256和512的数据类型和128位的相似,只是存放的个数不一样,这里再也不赘述。
知道了各类数据类型的长度以及其代码的意义,那么它的表现形式究竟是怎么样的呢?看下图
__m128i yy;
yy是__m128i型,从上图能够看出__m128i是一个联合体(union),根据不一样成员包含不一样的数据类型。看其具体的成员包含了8位、16位、32位和64位的有符号/无符号整数(这里__m128i是整型,故只有整型的成员,浮点数的使用__m128)。而每一个成员都是一个数组,数组中填充着相应的数据,而且根据数据长度的不一样数组的长度也不一样(数组长度 = 128 / 每一个数据的长度(位))。在使用的时候必定要特别的注意要操做数据的类型,也就是数据的长度,例如上图同一个变量yy看成4个32位有符号整型使用时其数据是:0,0,1024,1024;可是当作64位有符号整型时其数据为:0,4398046512128,大大的不一样。
在MSVC下可使用yy.m128i_i32[0]
取出第一个32位整型数据,原生的Intrinsic函数是没有提供该功能的,这是在MSVC的扩展,比较像Microsoft的风格,使用及其的方便可是效率不好,因此这种方法在GCC/Clang下面是不可用的。在MSVC下面能够根据须要使用不使用这种抽取数据的方法,可是这种功能在调试代码时是很是方便的,如上图能够很容易的看出128位的数据在不一样数据类型下其值的不一样。
Intrinsic函数的命名也是有必定的规律的,一个Intrinsic一般由3部分构成,这个三个部分的具体含义以下:
将这三部分组合到以其就是一个完整的Intrinsic函数,如_mm_mul_epi32 对参数中全部的32位有符号整数进行乘法运算。
SSE指令集对分支处理能力很是的差,并且从128位的数据中提取某些元素数据的代价又很是的大,所以不适合有复杂逻辑的运算。
在上一篇文章SSE指令集优化学习:双线性插值 使用SSE汇编指令对双线性插值算法进行了优化,这里将其改为为Intrinsic版的。
目的像素须要其映射到源像素周围最近的4个像素插值获得,这里同时计算源像素的最近的4个像素值的偏移量。
__m128i wwidth = _mm_set_epi32(0, width, 0, width);
__m128i yy = _mm_set_epi32(0, y2, 0, y1);
yy = _mm_mul_epi32(yy, wwidth); //y1 * width 0 y2 *width 0
yy = _mm_shuffle_epi32(yy, 0xd8); // y1 * width y2 * width 0 0
yy = _mm_unpacklo_epi32(yy, yy); // y1 * width y2 * width y1 * width y2 * width
yy = _mm_shuffle_epi32(yy, _MM_SHUFFLE(3, 1, 2, 0));
__m128i xx = _mm_set_epi32(x2, x2, x1, x1);
xx = _mm_add_epi32(xx, yy); // (x1,y1) (x1,y2) (x2,y1) (x2,y2)
__m128i x1x1 = _mm_shuffle_epi32(xx, 0x50); // (x1,y1) (x1,y2)
__m128i x2x2 = _mm_shuffle_epi32(xx, 0xfa); // (x2,y1) (x2,y2)
_MM_SHUFFLE
是一个宏,可以方便的生成shuffle中所须要的当即数。例如
_mm_shuffle_epi32(yy,_MM_SHUFFLE(3,1,2,0);
将yy中存放的第2和第3个32位整数交换顺序。
SSE汇编指令和其Intrinsic函数之间基本存在这一一对应的关系,有了汇编的实现再改成Intrinsic是挺简单的,再在这罗列代码也乜嘢什么意义了。这里就记录下使用的过程当中遇到的最大的问题:数据类型之间的转换。
作图像处理,因为像素通道值是8位的无符号整数,而与其运算的每每又是浮点数,这就须要将8位无符号整数转换为浮点数;运算完毕后,获得的结果又要写回图像通道,就要是8位无符号整数,还要涉及到超出8位的截断。开始不注意时吃了大亏....
类型转换主要如下几种:
上面的数据转换还少了一种,整数的饱和转换。什么是饱和转换呢,超过的最大值的以最大值来计算,例如8位无符号整数最大值为255,则转换为8位无符号时超过255的值视为255。
整数的饱和转换有两种:
有符号之间的 SSE的Intrinsic函数提供了两种
__m128i _mm_packs_epi32(__m128i a, __m128i b)
__m128i _mm_packs_epi16(__m128i a , __m128i b)
用于将16/32位的有符号整数饱和转换为8/16位有符号整数。有符号到无符号之间的
__m128i _mm_packus_epi32(__m128i a, __m128i b)
__m128i _mm_packus_epi16(__m128i a , __m128i b)
用于将16/32位的有符号整数饱和转换为8/16位无符号整数
这里只是作了一个粗略的对比,毕竟还只是个初学者。先说结果吧,在Debug下使用纯汇编的SSE代码会快很多,应该是因为没有编译器的优化,汇编代码的效率仍是有很大的优点的。可是在Release下面,前面也有提到过优化器内置了Intrinsic函数的行为信息,可以对Intrinsic函数提供很强大的优化,二者没有什么差异。PS:应该是因为选用数据的问题 ,普通的C++代码,SSE汇编代码以及Intrinsic函数三者在Release下的速度相差无几,编译器自己的优化功能是很强大的。
在对比时发现使用Intrinsic函数另外一个问题,就是数据的存取。使用SSE汇编时,能够将中间的计算结果保存到xmm寄存器中,在使用的时候直接取出便可。Intrinsic函数不能操做xmm寄存器,也就不能如此操做,它须要将每次的计算结果写回内存中,使用的时候再次读取到xmm寄存器中。
yy = _mm_mul_epi32(yy, wwidth);
上述代码是进行32位有符号整数乘法运算,计算的结果保存在yy中,反汇编后其对应的汇编代码:
000B0428 movaps xmm0,xmmword ptr [ebp-1B0h]
000B042F pmuldq xmm0,xmmword ptr [ebp-190h]
000B0438 movaps xmmword ptr [ebp-7A0h],xmm0
000B043F movaps xmm0,xmmword ptr [ebp-7A0h]
000B0446 movaps xmmword ptr [ebp-1B0h],xmm0
上述汇编代码中有屡次的movaps
操做。而上述操做在使用汇编时只需一条指令
pmuludq xmm0, xmm1;
在使用Intrinsic函数时,每个函数至少要进行一次内存的读取,将操做数从内存读入到xmm寄存器;一次内存的写操做,将计算结果从xmm寄存器写回内存,也就是保存到变量中去。因而可知,在只有很简单的计算中(例如:同时进行4个32位浮点数的乘法运算)和使用SSE汇编指令不会有很大的差异,可是若是逻辑稍微复杂些或者调用的Intrinsic函数较多,就会有不少的内存读写操做,这在效率上仍是有一部分损失的。
一个比较极端的例子,未通过优化的C++代码以下:
_MM_ALIGN16 float a[] = { 1.0f,2.0f,3.0f,4.0f };
_MM_ALIGN16 float b[] = { 5.0f,6.0f,7.0f,8.0f };
const int count = 1000000000;
float c[4] = { 0,0,0,0 };
cout << "Normal Time(ms):";
double tStart = static_cast<double>(clock());
for (int i = 0; i < count; i++)
for (int j = 0; j < 4; j++)
c[j] = a[j] + b[j];
double tEnd = static_cast<double>(clock());
对两个有4个单精度浮点数的数组作屡次加法运算,而且这种加法是重复进行,进行1次和进行1000次的结果是相同的。使用SSE汇编指令的代码以下:
for(int i = 0; i < count; i ++)
_asm
{
movaps xmm0, [a];
movaps xmm1, [b];
addps xmm0, xmm1;
}
使用Intrinsic函数的代码:
__m128 a1, b2;
__m128 c1;
for (int i = 0; i < count; i++)
{
a1 = _mm_load_ps(a);
b2 = _mm_load_ps(b);
c1 = _mm_add_ps(a1, b2);
}
在Debug下的运行
这个结果应该在乎料之中的,SSE汇编指令 < Intrinsic函数 < C++。SSE汇编指令比Intrinsic函数快了近1/3,下面是Intrinsic函数的反汇编代码
a1 = _mm_load_ps(a);
00FB2570 movaps xmm0,xmmword ptr [a]
00FB2574 movaps xmmword ptr [ebp-220h],xmm0
00FB257B movaps xmm0,xmmword ptr [ebp-220h]
00FB2582 movaps xmmword ptr [a1],xmm0
b2 = _mm_load_ps(b);
00FB2586 movaps xmm0,xmmword ptr [b]
00FB258A movaps xmmword ptr [ebp-240h],xmm0
00FB2591 movaps xmm0,xmmword ptr [ebp-240h]
00FB2598 movaps xmmword ptr [b2],xmm0
c1 = _mm_add_ps(a1, b2);
00FB259F movaps xmm0,xmmword ptr [a1]
00FB25A3 addps xmm0,xmmword ptr [b2]
00FB25AA movaps xmmword ptr [ebp-260h],xmm0
00FB25B1 movaps xmm0,xmmword ptr [ebp-260h]
00FB25B8 movaps xmmword ptr [c1],xmm0
能够看到共有12个movaps指令和1个addps指令。而SSE的汇编代码只有2个movaps指令和1个addps指令,可见其时间的差异应该主要是因为Intrinsic的内存读写形成的。
Debug下面的结果是没有出意料以外的,那么Release下的结果则真是出乎意料的
使用SSE汇编的最慢,C++实现都比起快很好,可见编译器的优化仍是很是给力的。而Intrinsic的时间则是0,是怎么回事。查看反汇编的代码发现,那个加法只执行了一次,而不是执行了不少次。应该是优化器根据Intrinsic行为作了预测,后面的屡次循环都是无心义的(一同窗告诉个人,他是作编译器生成代码优化的,作的是分支预测,不过也是在实现中,不知道他说的对不对)。
学习SSE指令将近两个周了,作了两篇学习笔记,差很少也算入门了吧。这段时间的学习总结以下: