SSE (为 Stream SIMD Extentions 的缩写)数据流单指令多数据扩展,是由
Intel
公司,在1999年推出 Pentinum III 处理器时,同时推出的新指令集,是继 MMX (为 Multi Media eXtension 的缩写)多媒体扩展指令集之后推出的新一代CPU指令集。如同其名称所表示的,SSE 是一种 SIMD 指令集。所谓的 SIMD (为 Single Instruction Multiple Data 的缩写)单指令多数据技术,其作用是通过一个指令同时对多个数据进行相同的计算操作,实现数据并行处理,提高程序的运算速度。較早的 MMX 和AMD
的 3DNow! 也都是 SIMD 指令集。
SSE 本质上类似于一个向量处理器,包括了4个主要部分:单精确度浮点数运算指令、整数运算指令(为 MMX 的延伸,并与 MMX 使用同样的寄存器)、Cache控制指令、状态控制指令。
如下图所示,SSE 新增了八个新的128位元寄存器(XMM0 ~ XMM7),这些128位元寄存器,可以用来存放四个32位元的单精度浮点数。也就是说,SSE 中所有计算都是一次性针对四个浮点数来完成的,这种批处理会带来显著的效率提升。使用 SSE 优化之后,我们的代码不一定会得到4倍速的提升,因为编译器可能已经自动对某些代码进行 SSE 优化了。
SSE 指令一次性能处理16个字节型数据,8个short类型数据,4个int类型数据或4个单精度浮点型数据,2个双精度浮点型数据。如下图所示:
举例说明:计算一个很长的单精度浮点型数组中每个元素的平方根。在纯C++下面实现可以这样写:
for each f in array
{
f = sqrt(f)
}
其底层实现如下:
for each f in array
{
把f从内存加载到浮点寄存器中
计算平方根
再把计算结果从寄存器中取出放入内存
}
采用SSE指令集后实现如下:
for each 4 members in array
{
把数组中的这4个数加载到一个128位的SSE寄存器中
在一个CPU指令执行周期中完成计算这4个数的平方根的操作
把所得的4个结果取出写入内存
{
要在 C 或 C++ 程序中使用 SSE 指令,一般有两种方式:一种是直接在 C/C++ 中嵌入(汇编)指令;二是使用 Intel C++ Compiler 或是 Microsoft Visual C++ 中提供的支持 SSE 指令集的 intrinsics 内联函数。从代码可读和维护角度讲,推荐使用 intrinsics 内联函数的形式。intrinsics 是对 MMX 、SSE 等指令集的一种封装,以函数的形式提供,使得程序员更容易编写和使用这些高级指令,在编译的时候,这些函数会被内联为汇编,不会产生函数调用的开销。
举例如下:使用纯 C++
#include
#include
void computeArrayCPlusPlus(float* pArray1, float* pArray2, float* pResult, int nSize)
{
float* pSource1 = pArray1;
float* pSource2 = pArray2;
float* pDest = pResult;
for (int i = 0; i < nSize; i++)
{
*pDest = (float)sqrt((*pSource1) * (*pSource1) + (*pSource2)
* (*pSource2)) + 0.5f;
pSource1++;
pSource2++;
pDest++;
}
}
int main()
{
float pArray1[50000] = { 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9 };
float pArray2[50000] = { 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9 };
float pResult[50000];
auto start = std::chrono::steady_clock::now();
computeArrayCPlusPlus(pArray1, pArray2, pResult, 50000);
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double, std::micro> elapsed = end - start;
for (int i = 0; i < 10; i++)
{
if (i != 9)
{
std::cout << pResult[i] << ", ";
}
else
{
std::cout << pResult[i] << std::endl;
}
}
std::cout << "纯C++运行时间: " << elapsed.count() << "微秒" << std::endl;
return 0;
}
使用 SSE 指令集的 intrinsics 内联函数
#include
#include //SSE3指令集
#include
void ComputeArrayCPlusPlusSSE(float* pArray1, float* pArray2, float* pResult, int nSize)
{
int nLoop = nSize / 4;
//__m128属于单精度浮点型
__m128 m1, m2, m3, m4;
__m128* pSrc1 = (__m128*) pArray1;
__m128* pSrc2 = (__m128*) pArray2;
__m128* pDest = (__m128*) pResult;
//m0_5[0, 1, 2, 3] = [0.5, 0.5, 0.5, 0.5]
__m128 m0_5 = _mm_set_ps1(0.5f);
for (int i = 0; i < nLoop; i++)
{
//m1 = *pSrc1 * *pSrc1
m1 = _mm_mul_ps(*pSrc1, *pSrc1);
//m2 = *pSrc2 * *pSrc2
m2 = _mm_mul_ps(*pSrc2, *pSrc2);
//m3 = m1 + m2
m3 = _mm_add_ps(m1, m2);
//m4 = sqrt(m3)
m4 = _mm_sqrt_ps(m3);
//*pDest = m4 + 0.5
*pDest = _mm_add_ps(m4, m0_5);
pSrc1++;
pSrc2++;
pDest++;
}
}
int main()
{
float pArray1[50000] = { 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9 };
float pArray2[50000] = { 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9 };
float pResult[50000];
auto start = std::chrono::steady_clock::now();
ComputeArrayCPlusPlusSSE(pArray1, pArray2, pResult, 50000);
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double, std::micro> elapsed = end - start;
for (int i = 0; i < 10; i++)
{
if (i != 9)
{
std::cout << pResult[i] << ", ";
}
else
{
std::cout << pResult[i] << std::endl;
}
}
std::cout << "intrinsics 内联函数的运行时间: " << elapsed.count() << "微秒" << std::endl;
return 0;
}
使用 SSE 嵌入汇编
#include
#include
void ComputeArrayAssemblySSE(float* pArray1, float* pArray2, float* pResult, int nSize)
{
int nLoop = nSize / 4;
float f = 0.5f;
_asm
{
//xmm2[0] = 0.5
movss xmm2,f
//xmm2[1, 2, 3] = [xmm2[0], xmm2[0], xmm2[0]]
shufps xmm2,xmm2,0
//输入的源数组1的地址送往esi
mov esi,pArray1
//输入的源数组2的地址送往edx
mov edx,pArray2
//输入的结果数组的地址送往edi
mov edi,pResult
//循环次数送往ecx
mov ecx,nLoop
start_loop : //开始循环
movaps xmm0,[esi] //xmm0 = [esi]
mulps xmm0,xmm0 //xmm0 = xmm0 * xmm0
movaps xmm1,[edx] //xmm1 = [edx]
mulps xmm1,xmm1 //xmm1 = xmm1 * xmm1
addps xmm0,xmm1 //xmm0 = xmm0 + xmm1
sqrtps xmm0,xmm0 //xmm0 = sqrt(xmm0)
addps xmm0,xmm2 //xmm0 = xmm0 + xmm2
movaps [edi],xmm0 //[edi] = xmm0
add esi,16 //esi += 16 移动16个字节
add edx,16 //edx += 16 移动16个字节
add edi,16 //edi += 16 移动16个字节
dec ecx //ecx-- ecx -= 1
jnz start_loop //如果ecx不为零转向start_Loop
}
}
int main()
{
float pArray1[50000] = { 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9 };
float pArray2[50000] = { 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9 };
float pResult[50000];
auto start = std::chrono::steady_clock::now();
ComputeArrayAssemblySSE(pArray1, pArray2, pResult, 50000);
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double, std::micro> elapsed = end - start;
for (int i = 0; i < 10; i++)
{
if (i != 9)
{
std::cout << pResult[i] << ", ";
}
else
{
std::cout << pResult[i] << std::endl;
}
}
std::cout << "嵌入汇编的运行时间: " << elapsed.count() << "微秒" << std::endl;
return 0;
}
由于Vs x64平台下不支持内联汇编,所以在此就无法运行了。如果对此感兴趣的话,可以参考以下博客进行配置。https://www.cnblogs.com/betterwgo/p/8145746.html
在此,我只针对使用 SSE 指令集的 intrinsics 内联函数进行学习。
在C++中,想要使用SSE指令,则需要包含以下对应的头文件:
例如,要使用 SSE3,则需要导入:#include
如果不关心使用那个版本的 SSE 指令,则可以包含所有版本:#include
Intrinsic 使用的数据类型与其寄存器是相对应,如下:
·64位 MMX 指令集使用
·128位 SSE 指令集使用
·256位 AVX 指令集使用
SSE 指令中intrinsics函数的数据类型为:__m128(单精度浮点数),如果使用sizeof(__m128)计算该类型大小,结果为16,即等于四个浮点数长度。__declspec(align(16))做为数组定义的修释符,表示该数组是以16字节为边界对齐的,因为SSE指令大部分支持这种格式的内存数据。其__m128数组定义如下:
typedef struct __declspec(intrin_type) __declspec(align(16)) __m128
{
float m128_f32[4];
} __m128;
SSE 指令处理的每一个浮点数数组必须把其中需要处理的数每16个字节(128位二进制)分为一组。一个静态数组(static array)可由__declspec(align(16))关键字声明:
__declspec(align(16)) float m_fArray[ARRAY_SIZE];
动态数组(dynamic array)可由_aligned_malloc函数为其分配空间:
float* m_fArray;
m_fArray = (float*) _aligned_malloc(ARRAY_SIZE * sizeof(float), 16);
由_aligned_malloc函数分配空间的动态数组可以由_aligned_free函数释放其占用的空间:
_aligned_free(m_fArray);
除__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;
对于__m128i联合体,不同成员包含不同的数据类型。每个成员都是一个数组,数组中填充这相应的数据,并且根据数据长度的不同数组的长度也不同(数组长度 = 128 / 每个数据的长度(位))。在使用的时候要特别注意操作数据的类型,也就是数组长度。例如:定义了一个_m128i yy;当变量yy去存储32位的有符号整型时使用的数据为:0, 0, 1024, 1024;但当yy用于存储64位的有符号整型时使用的数据是:0, 4398046512128。
Intrinsic函数的命名也是有一定的规律,一个Intrinsic通常由3部分构成,这个三个部分的具体含义如下:
第一部分为前缀_mm,表示是SSE指令集对应的Intrinsic函数。_mm256是AVX指令集的Intrinsic函数前缀。
第二部分为对应的指令的操作,如_add,_mul,_load等,有些操作可能会有修饰符,如loadu将未16位对齐的操作数加载到寄存器中。
第三部分通常由两个字母组成。第一个字母表示对结果变量的影响方式,为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位的无符号整数。
将以上三部分组合到一起就是一个完整的Intrnsic函数。
load系列,用于加载数据,从内存到寄存器,大部分需要16字节对齐
//_mm_load_ss用于scalar的加载。加载一个单精度浮点数到寄存器的第一字节,其它三个字节清0,(r0 := *p, r1 := r2 := r3 := 0.0)。
__m128 _mm_load_ss (float *p)
//_mm_load_ps用于packed的加载(下面的都是用于packed的),要求p的地址是16字节对齐,否则读取的结果会出错,(r0 := p[0], r1 := p[1], r2 := p[2], r3 := p[3])。
__m128 _mm_load_ps (float *p)
//_mm_load1_ps表示将p地址的值加载到寄存器的四个字节,需要多条指令完成,所以,从性能考虑,在内层循环不要使用这类指令。(r0 := r1 := r2 := r3 := *p)。
__m128 _mm_load1_ps (float *p)
//_mm_loadh_pi和_mm_loadl_pi分别用于从两个参数高底字节等组合加载。具体参考手册。
__m128 _mm_loadh_pi (__m128 a, __m64 *p)
__m128 _mm_loadl_pi (__m128 a, __m64 *p)
//_mm_loadr_ps表示以_mm_load_ps反向的顺序加载,需要多条指令完成,当然,也要求地址是16字节对齐。(r0 := p[3], r1 := p[2], r2 := p[1], r3 := p[0])。
__m128 _mm_loadr_ps (float *p)
//_mm_loadu_ps和_mm_load_ps一样的加载,但是不要求地址是16字节对齐,对应指令为movups。
__m128 _mm_loadu_ps (float *p)
set系列,用于加载数据,类似于load操作,但是大部分需要多条指令完成,可能不需要16字节对齐
//_mm_set_ss对应于_mm_load_ss的功能,不需要字节对齐,需要多条指令。(r0 = w, r1 = r2 = r3 = 0.0)
__m128 _mm_set_ss (float w)
//_mm_set_ps对应于_mm_load_ps的功能,参数是四个单独的单精度浮点数,所以也不需要字节对齐,需要多条指令。(r0=w, r1 = x, r2 = y, r3 = z,注意顺序)
__m128 _mm_set_ps (float z, float y, float x, float w)
//_mm_set1_ps对应于_mm_load1_ps的功能,不需要字节对齐,需要多条指令。(r0 = r1 = r2 = r3 = w)
__m128 _mm_set1_ps (float w)
//_mm_setr_ps对应于_mm_loadr_ps功能,不需要字节对齐,需要多条指令。(r0=z, r1 = y, r2 = x, r3 = w,注意顺序)
__m128 _mm_setr_ps (float z, float y, float x, float w)
//_mm_setzero_ps是清0操作,只需要一条指令。(r0 = r1 = r2 = r3 = 0.0)
__m128 _mm_setzero_ps ()
store系列,将计算结果等SSE寄存器的数据保存到内存中,与load系列函数的功能对应,基本上都是一个反向的过程。
//_mm_store_ss:一条指令,*p = a0
void _mm_store_ss (float *p, __m128 a)
//_mm_store_ps:一条指令,p[i] = a[i]。
void _mm_store_ps (float *p, __m128 a)
//_mm_store1_ps:多条指令,p[i] = a0。
void _mm_store1_ps (float *p, __m128 a)
//_mm_storeh_pi,_mm_storel_pi:值保存其高位或低位。
void _mm_storeh_pi (__m64 *p, __m128 a)
void _mm_storel_pi (__m64 *p, __m128 a)
//_mm_storer_ps:反向,多条指令。
void _mm_storer_ps (float *p, __m128 a)
//_mm_storeu_ps:一条指令,p[i] = a[i],不要求16字节对齐。
void _mm_storeu_ps (float *p, __m128 a)
//_mm_stream_ps:直接写入内存,不改变cache的数据。
void _mm_stream_ps (float *p, __m128 a)
算数指令系列,SSE 提供了大量的浮点运算指令,包括加法、减法、乘法、除法、开方、最大值、最小值等等。
//返回一个__m128的寄存器,r0=_A0+_B0, r1 = A1, r2 = A2, r3 = A3
extern __m128 _mm_add_ss (__m128 _A, __m128 _B)
//返回一个__m128的寄存器,r0=_A0+_B0, r1=_A1+_B1, r2=_A2+_B2, r3=_A3+_B3,
extern __m128 _mm_add_ps (__m128 _A, __m128 _B)
//返回一个__m128的寄存器,r0=_A0-_B0, r1 = A1, r2 = A2, r3 = A3
extern __m128 _mm_sub_ss (__m128 _A, __m128 _B)
//返回一个__m128的寄存器,r0=_A0-_B0, r1=_A1-_B1, r2=_A2-_B2, r3=_A3-_B3,
extern __m128 _mm_sub_ps (__m128 _A, __m128 _B)
//返回一个__m128的寄存器,r0=_A0*_B0, r1 = A1, r2 = A2, r3 = A3
extern __m128 _mm_mul_ss (__m128 _A, __m128 _B)
//返回一个__m128的寄存器,r0=_A0*_B0, r1=_A1*_B1, r2=_A2*_B2, r3=_A3*_B3,
extern __m128 _mm_mul_ps (__m128 _A, __m128 _B)
//返回一个__m128的寄存器,r0=_A0/_B0, r1 = A1, r2 = A2, r3 = A3
extern __m128 _mm_div_ss (__m128 _A, __m128 _B)
//返回一个__m128的寄存器,r0=_A0/_B0, r1=_A1/_B1, r2=_A2/_B2, r3=_A3/_B3,
extern __m128 _mm_div_ps (__m128 _A, __m128 _B)
//返回一个__m128的寄存器,r0=_sqrt(A0), r1 = A1, r2 = A2, r3 = A3
extern __m128 _mm_sqrt_ss (__m128 _A, __m128 _B)
//返回一个__m128的寄存器,r0=_sqrt(A0), r1=_sqrt(A1), r2=sqrt(A2), r3=sqrt(A3),
extern __m128 _mm_sqrt_ps (__m128 _A)
//返回一个__m128的寄存器,r0=_max(_A0,_B0), r1 = A1, r2 = A2, r3 = A3
extern __m128 _mm_max_ss (__m128 _A, __m128 _B)
//返回一个__m128的寄存器,r0=_max(_A0,_B0), r1=_max(_A1,_B1), r2=_max(_A2,_B2), r3=_max(_A3,_B3),
extern __m128 _mm_max_ps (__m128 _A)
//返回一个__m128的寄存器,r0=_min(_A0,_B0), r1 = A1, r2 = A2, r3 = A3
extern __m128 _mm_min_ss (__m128 _A, __m128 _B)
//返回一个__m128的寄存器,r0=_min(_A0,_B0), r1=_min(_A1,_B1), r2=_min(_A2,_B2), r3=_min(_A3,_B3),
extern __m128 _mm_min_ps (__m128 _A)
//将128位值都赋值为0
extern _mm_setzero_si128()
//返回一个_m128i的寄存器,将S0和S1的低64位的数以8位为单位进行交错
//S0:A15 A14 A13 A12 A11 A10 A9 A8 A7 A6 A5 A4 A3 A2 A1 A0
//S1:B15 B14 B13 B12 B11 B10 B9 B8 B7 B6 B5 B4 B3 B2 B1 B0
//_mm_unpacklo_epi8(S0,S1):B7 A7 B6 A6 B5 A5 B4 A4 B3 A3 B2 A2 B1 A1 B0 A0
extern _mm_unpacklo_epi8(_m128i S0,_m128i S1)
//返回一个_m128i的寄存器,将S0和S1的高64位的数以8位为单位进行交错
extern _mm_unpackhi_epi8(_m128i S0,_m128i S1)
//返回一个_m128i的寄存器,将S0和S1中对应位置的16bit整数分别相减
extern _mm_sub_epi16(_m128i S0,_m128i S1)
//返回一个_m128i的寄存器,它含有8个16位的整数,分别为S0和S1对应位置的16位的整数相乘结果的低16位数据
extern _mm_mullo_epi16(_m128i S0,_m128i S1)
//返回一个_m128i的寄存器,将寄存器S0中的8个16位整数按照计数进行算术右移
extern _mm_srai_epi16(_m128i S0,int count)
//返回一个_m128i的寄存器,将S0和S1中共16个16位数,放入存8位数的数组里,并进行饱和处理
extern _mm_packs_epi16(_m128i S0,_m128i S1)
//返回一个_m128i的寄存器,将S0和S1中对应位置的8bit整数分别相加;
extern _mm_add_epi8(_m128i S0,_m128i S1)
//将存储在缓存器S0中的数据存在指针p指向的地址
extern _mm_stream_si128(_m128i *p,_m128i S0)
//更多可参考博客:https://blog.csdn.net/weixin_44470443/article/details/99819791
数据类型转换系列。在做图像处理时,由于像素通道值是8位的无符号整数,而与其运算的往往又是浮点数,这就需要将8位无符号整数转换为浮点数;运算完毕后,得到的结果要写回图像通道,就需要将浮点数转换回来。但有时计算要考虑超出8位的截断,即大于255的数据。
类型转换主要一下几种:
1、浮点数和整数的转换及32位浮点数和64位浮点数之间的转换。这种转换简单直接,只需要调用相应的函数指令即可。
2、有符号整数的高位扩展。将8位、16位、32位有符号整数扩展为16位、32位、64位。
3、有符号整数的截断。将16位、32位、64位有符号压缩为8位、16位、32位。
4、无符号整数到有符号整数的扩展
//(xx是位数8/16/32/64)这是有符号整数之间的转换。
_mm_cvtepixx_epixx
//整数到单精度浮点数之间的转换。
mm_cvtepixx_ps
//整数到双精度浮点数之间的转换。
_mm_cvtepixx_pd
//单精度浮点数转换为有符号32位整数(带截断操作)
__mm_cvttps_si32
//无符号整数向有符号整数的扩展,采用高位0扩展的方式,这些函数是对无符号高位0扩展变成相应位数的有符号整数。没有32位无符号整数转换为16位有符号整数这样的操作。
_mm_cvtepuxx_epixx
//无符号整数转换为单精度浮点数。
_mm_cvtepuxx_ps
// 无符号整数转换为双精度浮点数。
_mm_cvtepuxx_pd
上面的数据转换还少了一种,整数的饱和转换。即超过最大值的以最大值来计算,例如8位无符号整数最大值为255,则转换为8位无符号时超过255的值视为255。
整数的饱和转换有两种:
有符号之间的 SSE 的Intrinsic函数提供了两种
//用于将16/32位的有符号整数饱和转换为8/16位有符号整数。
__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)
一般而言,使用 SSE 指令写程序,步骤如下:
1、使用load/set函数将数据从内存加载到SSE寄存器中
2、使用相关SSE指令完成计算
3、使用store系列函数将结果从寄存器保存到内存,供后面使用
例一:使用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等同于__declspec(align(16))
_MM_ALIGN16 float result[4];
__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) << "毫秒" << endl;
// SSE指令加法
timeStart = GetTickCount();
for (int i = 0; i < count; i++)
{
sse_add(srcA, srcB, dest, size);
}
timeEnd = GetTickCount();
cout << "SSE加法" << (timeEnd - timeStart) << "毫秒" << endl;
// 释放内存
_mm_free(srcA);
_mm_free(srcB);
_mm_free(dest);
system("pause");
return 0;
}
上述内容参考以下博客:(本人也是初学者,如果有问题,请及时在评论区指出,感谢)
https://blog.csdn.net/jgj123321/article/details/95633431
https://www.cnblogs.com/wangguchangqing/p/5466301.html
https://www.cnblogs.com/zyl910/archive/2012/04/26/md00.html