本文参考:https://blog.csdn.net/grafx/article/details/20001589
https://blog.csdn.net/gengshenghong/article/details/7010615
SIMD(Single Instruction Multiple Data)是单指令多数据技术,目前Intel处理器支持的SIMD技术包括MMX,SSE,AVX。
SSE(Stream SIMD Extentions,数据流单指令多数据扩展)是英特尔继MMX(Multi Media eXtension,多媒体扩展指令集)之后推出的新一代CPU指令集。MMX提供了8个64bit的寄存器进行SIMD操作,SSE系列提供了8个128bit的寄存器进行SIMD操作。而最新的AVX指令则支持256bit的SIMD操作。
如下图所示,SSE新增的8个128位寄存器(xmm0 ~ xmm7),每个寄存器可以用来存放4个32位单精度浮点数,8个16位整型数。也就是说,SSE中的所有计算都是一次性针对4个浮点数来完成的,这种批处理会带来显著的效率提升。使用SSE优化之后,我们的代码不一定会得到4倍速的提升,因为编译器可能已经自动对某些代码进行SSE优化了。
使用SSE指令有两种方式:一是直接在C/C++中嵌入(汇编)指令;二是使用Intel C++ Compiler或是Microsoft Visual C++中提供的支持SSE指令集的intrinsics内联函数。从代码可读和维护角度讲,推荐使用intrinsics内联函数的形式。intrinsics是对MMX、SSE等指令集的一种封装,以函数的形式提供,使得程序员更容易编写和使用这些高级指令,在编译的时候,这些函数会被内联为汇编,不会产生函数调用的开销。想要使用SSE指令,则需要包含对应的头文件:
#include //mmx
#include //sse
#include //sse2
#include //sse3
SSE指令通常由三部分构成:
第一部分为前缀_mm(多媒体扩展指令集),表示该函数属于SSE指令集
第二部分为指令的操作类型,如_add、_mul等
第三部分通常由两个字母组成。第一个字母表示对结果变量的影响方式,为p或s。 p(packed:包裹指令) :该指令对xmm寄存器中的每个元素进行运算,即一次对四个浮点数(data0~data3)均进行计算;
s(scalar:标量指令):该指令对寄存器中的第一个元素进行运算,即一次只对xmm寄存器中的data0进行计算。
第二个字母表示参与运算的数据类型,s表示32位浮点数,d表示64位浮点数,i32表示带符号32位整型,i64表示带符号64位整型,u32表示无符号32位整型,以此类推。由于SSE只支持32位浮点数的运算,所以你可能会在这些指令封装函数中找不到包含非s修饰符的,但你可以在MMX和SSE2的指令集中去认识它们。
_pixx(xx为长度,可以是8,16,32,64)packed操作所有的xx位有符号整数,使用的寄存器长度为64位;_epixx(xx为长度)packed操作所有的xx位的有符号整数,使用的寄存器长度为128位;_epuxx packed操作所有的xx位的无符号整数;
SSE指令中intrinsics函数的数据类型为:__m128(单精度浮点数),如果使用sizeof(__m128)计算该类型大小,结果为16,即等于四个浮点数长度。__declspec(align(16))做为数组定义的修释符,表示该数组是以16字节为边界对齐的,因为SSE指令大部分支持这种格式的内存数据。他的定义如下:
typedef struct __declspec(intrin_type) __declspec(align(16)) __m128 {
float m128_f32[4];
} __m128;
除__m128外、还包括__m128d(双精度浮点数)和__m128i(整型)。其中__m128i是一个共用体类型,其定义如下 :
typedef union __declspec(intrin_type)_CRT_ALIGN(16)__m128i {
__int8 m128i_i8[16]; //char
__int16 m128i_i16[8]; //short
__int32 m128i_i32[4]; //int
__int64 m128i_i64[2]; //long long
unsigned __int8 m128i_u8[16]; //uchar
unsigned __int16 m128i_u16[8]; //ushort
unsigned __int32 m128i_u32[4]; //uint
unsigned __int64 m128i_u64[2]; //ulonglong
}__m128i;
load系列,用于加载数据(从内存到暂存器),大部分需要16字节对齐
__m128 _mm_load_ss(float *p) //将一个单精度浮点数加载到寄存器的第一个字节,其它三个字节清零(r0 := *p, r1 := r2 := r3 := 0.0)
__m128 _mm_load_ps(float *p) //将四个单精度浮点数加载到寄存器(r0 := p[0], r1 := p[1], r2 := p[2], r3 := p[3])
__m128 _mm_load1_ps(float *p)//将p地址的值加载到暂存器的四个字节,需要多条指令完成。从性能考虑,在内层循环不要使用这类指令(r0 := r1 := r2 := r3 := *p)
__m128 _mm_loadh_pi(__m128 a, __m64 *p)//
__m128 _mm_loadl_pi(__m128 a, __m64 *p)//
__m128 _mm_loadr_ps(float *p)//以_mm_load_ps反向的顺序加载,需要多条指令完成。(r0 := p[3], r1 := p[2], r2 := p[1], r3 := p[0])
__m128 _mm_loadu_ps(float *p)//_mm_load_ps一样的加载,但是不要求地址是16字节对齐
set系列,用于加载数据,类似于load操作,但是大部分需要多条指令完成,可能不需要16字节对齐
__m128 _mm_set_ss(float w)//对应于_mm_load_ss的功能,不需要字节对齐,需要多条指令(r0 = w, r1 = r2 = r3 = 0.0)
__m128 _mm_set_ps(float z, float y, float x, float w)//对应于_mm_load_ps的功能,参数是四个单独的单精度浮点数,所以也不需要字节对齐,需要多条指令。(r0=w, r1 = x, r2 = y, r3 = z,注意顺序)
__m128 _mm_set1_ps(float w)//对应于_mm_load1_ps的功能,不需要字节对齐,需要多条指令。(r0 = r1 = r2 = r3 = w)
__m128 _mm_setr_ps(float z, float y, float x, float w)//对应于_mm_loadr_ps功能,不需要字节对齐,需要多条指令。(r0=z, r1 = y, r2 = x, r3 = w,注意顺序)
__m128 _mm_setzero_ps()//清0操作,只需要一条指令。(r0 = r1 = r2 = r3 = 0.0)
store系列,将计算结果等SSE暂存器的数据保存到内存中,与load系列函数的功能对应,基本上都是一个反向的过程。
void _mm_store_ss(float *p, __m128 a) //一条指令,*p = a0
void _mm_store_ps(float *p, __m128 a) //一条指令,p[i] = a[i]
void _mm_store1_ps(float *p, __m128 a) //多条指令,p[i] = a0
void _mm_storeh_pi(__m64 *p, __m128 a) //
void _mm_storel_pi(__m64 *p, __m128 a) //
void _mm_storer_ps(float *p, __m128 a) //反向,多条指令
void _mm_storeu_ps(float *p, __m128 a) //一条指令,p[i] = a[i],不要求16字节对齐
void _mm_stream_ps(float *p, __m128 a) //直接写入内存,不改变cache的数据
算数指令系列,SSE提供了大量的浮点运算指令,包括加法、减法、乘法、除法、开方、最大值、最小值等等
__m128 _mm_add_ss (__m128 a, __m128 b)
__m128 _mm_add_ps (__m128 a, __m128 b)
数据类型转换系列
__mm_cvtss_si32 //单精度浮点数转换为有符号32位整数
__mm_cvttss_si32 //单精度浮点数转换为有符号32位整数(带截断操作)
__mm_cvtpi16_ps //16位有符号整数转换为单精度浮点数
对第一个指令进行详细说明,如下图所示:
一般而言,使用SSE指令写代码,步骤如下:
例一:使用SSE指令完成加法运算(不要求字节对齐)
#include
#include
using namespace std;
int main(int argc, char* argv[]){
float op1[4] = { 1.0, 2.0, 3.0, 4.0 };
float op2[4] = { 1.0, 2.0, 3.0, 4.0 };
float result[4];
__m128 a;
__m128 b;
__m128 c;
// Load
a = _mm_loadu_ps(op1);
b = _mm_loadu_ps(op2);
// Calculate
c = _mm_add_ps(a, b); // c = a + b
// Store
_mm_storeu_ps(result, c);
cout << result[0] << endl;
cout << result[1] << endl;
cout << result[2] << endl;
cout << result[3] << endl;
system("pause");
return 0;
}
例二:使用SSE指令完成加法运算(要求字节对齐)
#include
#include
using namespace std;
int main(int argc, char* argv[]){
__declspec(align(16)) float op1[4] = { 1.0, 2.0, 3.0, 4.0 };
__declspec(align(16)) float op2[4] = { 1.0, 2.0, 3.0, 4.0 };
_MM_ALIGN16 float result[4]; // _MM_ALIGN16等同于__declspec(align(16))
__m128 a;
__m128 b;
__m128 c;
// Load
a = _mm_load_ps(op1);
b = _mm_load_ps(op2);
// Calculate
c = _mm_add_ps(a, b); // c = a + b
// Store
_mm_store_ps(result, c);
cout << result[0] << endl;
cout << result[1] << endl;
cout << result[2] << endl;
cout << result[3] << endl;
system("pause");
return 0;
}
例三:使用SSE指令完成多个数据的加法运算(要求字节对齐)
如果想使用SSE计算一个浮点型数组中每个元素的平方根,我们不必去声明__m128类型的数组,可以直接将你的数组强制类型转换成__m128*,然后使用SSE的命令操作这个数组。
__declspec(align(16)) float array[] = { 1.0, 2.0, 3.0, 4.0 };
__m128* ptr = (__m128*)array;
__m128 t = _mm_sqrt_ps(*ptr);
#include
#include
#include
using namespace std;
void sse_add(float *srcA, float *srcB, float *dest, int n){
int len = n >> 2;
for (int i = 0; i < len; i++){
*(__m128*)(dest + i * 4) = _mm_add_ps(*(__m128*)(srcA + i * 4), *(__m128*)(srcB + i * 4));
}
}
void normal_add(float *srcA, float *srcB, float *dest, int n){
for (int i = 0; i < n; i++){
dest[i] = srcA[i] + srcB[i];
}
}
int main(){
DWORD timeStart = 0, timeEnd = 0;
const int size = 10000; //申请的内存中存放的数据个数
const int count = 10000;//循环计算的次数,便于观察执行效率
// 分配16字节对齐的内存
_MM_ALIGN16 float *srcA = (_MM_ALIGN16 float*)_mm_malloc(sizeof(float)*size, 16);
_MM_ALIGN16 float *srcB = (_MM_ALIGN16 float*)_mm_malloc(sizeof(float)*size, 16);
_MM_ALIGN16 float *dest = (_MM_ALIGN16 float*)_mm_malloc(sizeof(float)*size, 16);
// 初始化
for (int i = 0; i < size; i++){
srcA[i] = (float)i;
}
memcpy_s(srcB, sizeof(float) * size, srcA, sizeof(float) * size);
// 标准加法
timeStart = GetTickCount();
for (int i = 0; i < count; i++){
normal_add(srcA, srcB, dest, size);
}
timeEnd = GetTickCount();
cout<<"标准加法"<<(timeEnd - timeStart)<<"毫秒"<