SSE指令集学习之旅(一)

SSE指令集学习之旅(一)

文章目录

  • SSE指令集学习之旅(一)
    • 1、SSE介绍
    • 2、如何使用SSE指令
    • 3、SSE相关数据类型
    • 4、 Intrinsic 函数的命名
    • 5、常用的SSE指令
    • 6、SSE指令应用实例

1、SSE介绍

        SSE (为 Stream SIMD Extentions 的缩写)数据流单指令多数据扩展,是由 Intel 公司,在1999年推出 Pentinum III 处理器时,同时推出的新指令集,是继 MMX (为 Multi Media eXtension 的缩写)多媒体扩展指令集之后推出的新一代CPU指令集。如同其名称所表示的,SSE 是一种 SIMD 指令集。所谓的 SIMD (为 Single Instruction Multiple Data 的缩写)单指令多数据技术,其作用是通过一个指令同时对多个数据进行相同的计算操作,实现数据并行处理,提高程序的运算速度。較早的 MMXAMD 的 3DNow! 也都是 SIMD 指令集。

        SSE 本质上类似于一个向量处理器,包括了4个主要部分:单精确度浮点数运算指令、整数运算指令(为 MMX 的延伸,并与 MMX 使用同样的寄存器)、Cache控制指令、状态控制指令。

        如下图所示,SSE 新增了八个新的128位元寄存器(XMM0 ~ XMM7),这些128位元寄存器,可以用来存放四个32位元的单精度浮点数。也就是说,SSE 中所有计算都是一次性针对四个浮点数来完成的,这种批处理会带来显著的效率提升。使用 SSE 优化之后,我们的代码不一定会得到4倍速的提升,因为编译器可能已经自动对某些代码进行 SSE 优化了。

SSE指令集学习之旅(一)_第1张图片
        SSE 指令一次性能处理16个字节型数据,8个short类型数据,4个int类型数据或4个单精度浮点型数据,2个双精度浮点型数据。如下图所示:

SSE指令集学习之旅(一)_第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个结果取出写入内存
        {

2、如何使用SSE指令

        要在 CC++ 程序中使用 SSE 指令,一般有两种方式:一种是直接在 C/C++ 中嵌入(汇编)指令;二是使用 Intel C++ Compiler 或是 Microsoft Visual C++ 中提供的支持 SSE 指令集的 intrinsics 内联函数。从代码可读和维护角度讲,推荐使用 intrinsics 内联函数的形式。intrinsics 是对 MMXSSE 等指令集的一种封装,以函数的形式提供,使得程序员更容易编写和使用这些高级指令,在编译的时候,这些函数会被内联为汇编,不会产生函数调用的开销。

        举例如下:使用纯 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指令,则需要包含以下对应的头文件:

SSE指令集学习之旅(一)_第3张图片
        例如,要使用 SSE3,则需要导入:#include

    如果不关心使用那个版本的 SSE 指令,则可以包含所有版本:#include

3、SSE相关数据类型

         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。

4、 Intrinsic 函数的命名

        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进行计算。

SSE指令集学习之旅(一)_第4张图片
        第二个字母表示参与运算的数据类型,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函数。

5、常用的SSE指令

        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)

6、SSE指令应用实例

        一般而言,使用 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指令集学习之旅(一)_第5张图片

        例二:使用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指令集学习之旅(一)_第6张图片

        例三:使用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

你可能感兴趣的:(C++,学习)