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!的绝大多数功能。
SSE2、SSE3、SSE4是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新的数据类型
根据上面知道,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指令格式
- 第一部分表示指令的作用,比如加法add;
- 第二部分是p或者s,分别表示为packed或者scalar;
- 第三部分为s,表示单精度浮点数。
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_<opcode>_<suffix>
- 前缀_mm,表示是SSE指令集对应的Intrinsic函数;
- < opcode> 是对应的指令操作,如_add,_mul,_load等,有些操作可能会有修饰符,如loadu将未16位对齐的操作数加载到寄存器中。
- <suffix> 为操作的对象名及数据类型。 在 SSE 浮点运算指令中,只有两个种类:ps 和 ss。 其中,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
相关阅读
安装python时报错 An error occurred during the inst
安装Python时报错,详细错误信息如图1. 图1在我的windows 7系统中找到 控制面板->程序或功能->打开或关闭Windows功能 在弹框
assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行,原型定义: #include <assert.h> void assert(
assert.h 简介 assert.h 常用于防御式编程。防御式编程是提高软件质量技术的有益辅助手段。防御式编程的主要思想是:子程序应
在编程中,使用反射(IoC)是一个很好的架构。在.Net中,System.Reflection命名空间提供了对反射的支持。然而,很多朋友在使用Assembly.Loa
void abort(void);终止程序执行,直接从调用的地方跳出。头文件#include <stdlib.h>#include <stdio.h> #include <stdlib.h> in