SSE学习笔记

背景

什么是指令集?

指令集是为了增强CPU在某些方面(如多媒体)的功能而特意开发出的一组程序代码集合。

常见的指令集有哪些呢?

MMX(Multi-Media Extensions,多媒体扩展):Intel1996年推出的一项多媒体指令增强技术。共包含57条多媒体指令,这些指令一次可以处理多个数据。MMX的主要问题是,CPU无法同时处理浮点和SIMD数据,只对整数起作用(不支持浮点计算)。

SSE指令集(Streaming SIMD Extensions,单指令多数据留扩展)兼容MMX指令,它可以通过SIMD(单指令多数据技术)和单时钟周期并行处理多个浮点来有效地提高浮点运算速度。SSE数据集包含70条指令,其中有50条SIMD(单指令多数据技术)浮点运算指令,12条MMX整数运算增强指令,8条优化内存中连续数据块的传输指令。理论上这些指令对目前流行的图像处理、浮点处理、3D运算、视频处理、音频处理等多媒体应用起到全面强化的作用。  注意:SSE指令和3DNow!指令彼此互不兼容,但SSE包含了3DNow!的绝大多数功能。

SSE2SSE3SSE4是SSE的扩展技术。

3DNow!指令集。

X86指令集

AVX指令集(Advanced Vector Extensions),Intel AVX指令集在SIMD计算性能增强的同时也沿用了的MMX/SSE指令集。不过和MMX/SSE的不同点在于增强的AVX指令,从指令的格式上就发生了很大的变化。AVX 指令集架构的改进和增强的功能:

  • 128 位 SIMD 寄存器 xmm0 - xmm15 扩展为 256 位的 ymm0 - ymm15 寄存器 ;
  • 支持 256 位的矢量运算,由原来 128 位扩展为 256 位 ;
  • 指令可支持最多 4 个操作数,实现目标操作数无需损毁原来的内容 ;
  • 引进新的 AVX 指令,非 Legacy SSE 指令移植 ;
  • 新增 FMA 指令子集进行 fused multiply-add/subtract 类运算,用子式表达为:± (a * b) ± c ;
  • 引进新的指令编码模式,使用 VEX prefix 来设计指令编码 。

SSE介绍

SSE(为 Streaming SIMD Extensions 的缩写)是由Intel公司,在 1999 年推出 Pentium III 处理器时,同时推出的新指令集,它是SIMD指令集扩展。SIMD(single-instruction, multiple-data)是一种使用单道指令处理多道数据流的CPU执行模式,即在一个CPU指令执行周期内用一道指令完成处理多个数据的操作。 当对多个数据对象执行完全相同的操作时, SIMD 指令可以大大提高性能。典型的应用是数字信号处理和图形处理。

SSE 指令包括了四个主要的部份:单精度浮点数运算指令整数运算指令(此为 MMX 之延伸,并和 MMX 使用同样的缓存器)、Cache 控制指令、和状态控制指令。 这里主要是介绍浮点数运算指令和 Cache 控制指令。

SSE新增的寄存器(用于浮点运算指令)

SSE 新增了八个新的 128 位缓存器,xmm0 ~ xmm7。 这些 128 位的缓存器,可以用来存放四个 32 位的单精度浮点数。 SSE 的浮点数运算指令就是使用这些缓存器。 和之前的 MMX 或 3DNow! 不同,这些缓存器并不是原来己有的缓存器(MMX 和 3DNow! 均是使用 x87 浮点数缓存器),所以不需要像 MMX 或 3DNow! 一样,要使用 x87 指令之前,需要利用一个EMMS 指令来清除缓存器的状态。 因此,不像 MMX 或 3DNow! 指令,SSE 的浮点数运算指令,可以很自由地和 x87 指令,或是 MMX 指令共享。 但是,这样做的主要缺点是,因为多任务操作系统在进行 context switch 时,需要储存所有缓存器的内容。 而这些多出来的新缓存器,也是需要储存的。 因此,既存的操作系统需要修改,在 context switch 时,储存这八个新缓存器的内容,才能正确支持 SSE 浮点运算指令。

下图是 SSE 新增的缓存器的示意图:

SSE学习笔记_第1张图片

SSE新的数据类型

根据上面知道,SSE新增的寄存器是128bit的,那么SSE就需要使用128bit的数据类型 (也就是 __m128),SSE使用4个浮点数(4*32bit)组合成一个新的数据类型,用于表示128bit类型,SSE指令的返回结果也是128bit的。

__m128 是一个 16 bytes(128 bits)的数据型态,对应 SSE 的 128 位寄存器。 几乎所有的 SSE 浮点运算的 intrinsics 都是使用这个数据型态。 比如说,_mm_add_ps 这个 intrinsic 的函数声明为:
                 __m128 _mm_add_ps(__m128 a, __m128 b);
可以看到,它的参数和传回值的型态都是 __m128。 基本上,这个 intrinsic 的动作就是把两个参数相加,并把结果以传回值的方式传回。


SSE浮点运算指令分类

SSE的浮点运算指令分为两大类:packed 和 scalar。(有些地方翻译为“包裹指令和”“标量指令” :) )
packed指令是一次对XMM暂存器中的四个浮点数(即DATA0 ~ DATA3)均进行计算,而scalar则只对XMM暂存器中的DATA0进行计算。如下图所示:

SSE学习笔记_第2张图片

SSE指令格式

  • 第一部分表示指令的作用,比如加法add;
  • 第二部分是p或者s,分别表示为packed或者scalar;
  • 第三部分为s,表示单精度浮点数。

SSE学习笔记_第3张图片

SSE定址/寻址方式

SSE 指令和一般的x86 指令很类似,基本上包括两种定址方式:寄存器-寄存器方式(reg-reg)和寄存器-内存方式(reg-mem):

  • addps xmm0, xmm1 ;    reg-reg
  • addps xmm0, [ebx] ;      reg-mem

指令的运算结果会覆盖到第一个参数中。 例如,以上面的第一个例子来说,xmm0 缓存器会存放最后计算的结果。

另外,绝大部份需要存取内存的 SSE 指令,都要求地址是 16 的倍数(也就是对齐在 16 bytes 的边上)。 如果不是的话,就会导致 exception。 这是非常重要的。 因为,一般的 32 位浮点数只会对齐在 4 bytes 或 8 bytes 的边上(根据 compiler 的设定而不同)。 另外,若是处理数组中的数字,也需要特别注意这个问题。

支持SSE指令集的intrinsics内联函数

SSE指令的使用

要在 C 或 C++ 程序中使用SSE指令有两种方式:一是直接在C/C++中嵌入(汇编)指令(内嵌式汇编语言);二是使用Intel C++ Compiler或是Microsoft Visual C++中提供的支持SSE指令集的intrinsics内联函数。从代码可读和维护角度讲,推荐使用intrinsics内联函数的形式。以下是一些例子:

/**   内嵌式汇编语言使用SSE指令集   **/
_asm addps xmm0, xmm1 
__asm movaps [ebx], xmm0 
... 
__m128 data; 
... 
__asm 
{ 
    lea ebx, data 
    addps xmm0, xmm1 
    movaps [ebx], xmm0 
} 


/**   通过 intrinsics内联函数使用SSE指令集   **/
__m128 data1, data2; 
... 
__m128 out = _mm_add_ps(data1, data2); 
... 

什么是intrinsics?  

intrinsics函数是对MMX、SSE等指令集的一种封装,以函数的形式提供,使得程序员更容易编写和使用这些高级指令,在编译的时候,这些函数会被内联为汇编,不会产生函数调用的开销。SSE指令中intrinsics函数的数据类型为:__m128,如果使用sizeof(__m128)计算该类型大小,结果为16,即等于四个浮点数长度。

SSE的 intrinsics函数 一般格式为:_mm__

  • 前缀_mm,表示是SSE指令集对应的Intrinsic函数;
  • < opcode> 是对应的指令操作,如_add,_mul,_load等,有些操作可能会有修饰符,如loadu将未16位对齐的操作数加载到寄存器中。
  • 为操作的对象名及数据类型。 在 SSE 浮点运算指令中,只有两个种类:psss。 其中,ps 是指 Packed Single-precision,也就是这个指令对缓存器中的四个单精度浮点数进行运算;ss 则是指 Scalar Single-precision,也就是这个指令只对缓存器中的 DATA0 进行运算。

所以,像上面的 _mm_add_ps 指令,就是把两个四维向量相加的指令。

SSE指令中的intrinsics函数的数据类型为:__m128,正好对应了上面提到的SSE新的数据类型(128bit),当然,这种数据类型只是一种抽象表示,实际是要转换为基本的数据类型的。

头文件

有了头文件,在我们的代码中才能调用指令集函数(加入头文件可以将汇编形式的指令集封装成C语言形式,可增强可读性以及可维护性),Visual Studio使用SSE需要添加对应的头文件:

  • mmintrin.h------>MMX
  • xmmintrin.h------>SSE
  • emmintrin.h------>SSE2
  • pmmintrin.h------>SSE3

SSE指令的内存对齐要求

SSE中大部分指令要求地址16bytes对齐的,要理解这个问题,以_mm_load_ps函数来解释,这个函数对应于loadps的SSE指令。

其原型为:extern __m128 _mm_load_ps(float const*_A);

可以看到,它的输入是一个指向float的指针,返回的就是一个__m128类型的数据,从函数的角度理解,就是把一个float数组的四个元素依次读取,返回一个组合的__m128类型的SSE数据类型,从而可以使用这个返回的结果传递给其它的SSE指令进行运算,比如加法等;从汇编的角度理解,它对应的就是读取内存中连续四个地址的float数据,将其放入SSE新的暂存器(xmm0~8)中,从而给其他的指令准备好数据进行计算。其使用示例如下:

float input[4] = { 1.0f, 2.0f, 3.0f, 4.0f };
__m128 a = _mm_load_ps(input);

这里加载正确的前提是:input这个浮点数阵列都是对齐在16 bytes的边上。否则加载的结果和预期的不一样。如果没有对齐,就需要使用 _mm_loadu_ps 函数,这个函数用于处理没有对齐在16bytes上的数据,但是其速度会比较慢。关于内存对齐的问题,这里就不详细讨论什么是内存对齐了,以及如何指定内存对齐方式。这里主要提一下,SSE的intrinsics函数中的扩展的方式:

一般来说,宣告一个 float 的数组,并不会对齐在 16 bytes 的边上。 如果希望它会对齐在 16 bytes 的边上,以便利用 SSE 指令的话,Visual C++ 6.0 Processor Pack 和 Intel C++ compiler 都可以指定对齐方式。 指定的方式如下:

  • __declspec(align(16)) float input[4];

这样就可以直接用较快的 _mm_load_ps 来加载数据了。 因为 SSE 浮点数指令常常需要数据对齐在 16 bytes 的边上,所以 xmmintrin.h 也定义了一个宏 _MM_ALIGN16, 是同样的意义。 因此,上面的程序也可以写成:

  • _MM_ALIGN16 float input[4];

【注意】GCC编译器和VC编译器下字节对齐是不同的,例如创建此结构体实例时按16字节对齐:

  • gcc: __attribute__((aligned(16)))
  • vc:   __declspec(align(16))
     

大小端问题

这个只是使用SSE指令的时候要注意一下,我们知道,x86的little-endian特性,位址较低的byte会放在暂存器的右边。也就是说,若以上面的input为例,在载入到XMM暂存器后,暂存器中的DATA0会是1.0,而DATA1是2.0,DATA2是3.0,DATA3是4.0。如果需要以相反的顺序载入的话,可以用_mm_loadr_ps 这个intrinsic,根据需要进行选择。

 

【参考】

          http://dev.gameres.com/Program/Other/SSEjianjie.htm

          https://blog.csdn.net/gengshenghong/article/details/7008704

 

你可能感兴趣的:(SSE学习笔记)