SSE指令集学习笔记

Posted by 吴俊贤 on November 12, 2018

SSE

全称Streaming SIMD Extension,是x86上对SIMD指令集的一个扩展,主要用于处理单精度浮点数。Intel陆续推出SSE2、SSE3、SSE4版本。其中,SSE主要处理单精度浮点数,SSE2引入了整数的处理,

SSE指令集引入了8个128bit的寄存器,称为XMM0XMM7。正因为这些寄存器存储了多个数据,使用一条指令处理,因此称这项功能为SIMD。

处理器指令

(暂无)

C程序库

这里是Intel官方的指令集C库文档地址:https://software.intel.com/sites/landingpage/IntrinsicsGuide/

以及Intel的介绍性文档:https://software.intel.com/en-us/node/523354

头文件

适用于使用gcc以及Intel x64平台上的编程。

指令集 头文件
SSE xmmintrin.h
SSE2 emmintrin.h
SSE3 pmmintrin.h
SSSE3 tmmintrin.h
SSE4.1 smmintrin.h
SSE4.2 nmmintrin.h
SSE4A ammintrin.h

新数据结构

  • __m128:保存4个单精度浮点数
  • __m128d:保存2个双精度浮点数
  • __m128i:保存整数,如4个32bit、8个16bit和2个64bit整数

对于__m128d__m128i,编译器自动把局部和全局变量在栈上对齐16字节。若需要对齐整型、float和double数组,使用__declspec(align)

获取__m128i的数据

获取8bit整数使用:

#define _mm_extract_epi8(x, imm) \
((((imm) & 0x1) == 0) ?   \
_mm_extract_epi16((x), (imm) >> 1) & 0xff : \
_mm_extract_epi16(_mm_srli_epi16((x), 8), (imm) >> 1))

获取16bit整数使用:

int _mm_extract_epi16(__m128i a, int imm)

获取32bit整数使用:

#define _mm_extract_epi32(x, imm) \
_mm_cvtsi128_si32(_mm_srli_si128((x), 4 * (imm)))

获取64bit整数使用:

#define _mm_extract_epi64(x, imm) \
_mm_cvtsi128_si64(_mm_srli_si128((x), 8 * (imm)))

其中的imm,是数字的下标。

函数命名规定

一般情况下函数的命名格式如下

_<mm/mm256/mm512>_<intrin_op>_<suffix>

其中,<intrin_op>为固有指令的名称。<suffix>为操作数的类型,前一个或前两个字母代表这个操作数是packed(p),还是extended packed(ep),还是scaler(s),后面的字母代表数据类型。如下:

  • s:单精度浮点数
  • d:双精度浮点数
  • i128:有符号128bit整数
  • i64:有符号64bit整数
  • u64:无符号64bit整数
  • i32:有符号32bit整数
  • u32:无符号32bit整数
  • i16:有符号16bit整数
  • u16:无符号16bit整数
  • i8:有符号8bit整数
  • u8:无符号8bit整数

关于packed的解释,原意是完全的、满的,在这篇文章,Kittur解释了packed是指多个数据放到了一个向量里面,如4个单精度浮点数放到128bit寄存器,然后一条指令就操作这四个数。scaler类型的运算,一条指令只操作最低的某类型的数。extended packed则是用在SSE4引入的数据位数扩展的指令(看这里),可以将低位数的数据扩展到更高位数的数据,有有符号和无符号两种扩展方式。

变量后的数字,代表使用packed数据的第几个数据,如r0则使用r的最低位数据。

packed数据从右向左存储,也就是低位到高位存储,如

double a[2] = {1.0, 2.0};
__m128d t = _mm_load_pd(a);

那么在寄存器中,就是这样的

127----063----000
 |  2.0 |  1.0 |

下面两条语句的效果是一样的

__m128d t = _mm_set_pd(2.0, 1.0);
__m128d t = _mm_setr_pd(1.0, 2.0);

packed类型的内嵌原语操作,将会操作寄存器中的所有数字。而scaler类型的操作,只会操作寄存器中最低位的数字,而其他的数字,从第一个操作数传递到结果当中。 下面的表格中,就有这两种类型的内嵌原语,不再说明这一点。

内嵌原语列表

下面的所有的操作,有些可能包含多条指令(如composite),因此可能不会达到预期的效果,请留意。为阐述方便,规定第一个参数为a,第二个为b,特殊操作数imm等另外说明。

转换原语查询表

列表头为被转换类型,行表头为转换到的类型

矢量转换

  ps pd pi32 pi64 pi16 pu16 pi8 pu8
ps   1 1+t   1   1  
pd 1   1+t          
pi32 1 1            
pi64                
pi16 1              
pu16 1              
pi8 1              
pu8 1              

标量转换

  ss sd si32 si64 float32 float64
ss   1 1+t 1+t 1  
sd 1   1+t     1
si32 1 1        
si64 1          

表中没有包含:

  • 转换SPFP到float
  • 转换DPFP到double

算术型

加法

将操作数A与B加起来,并返回结果。

  SPFP DPFP i8 i16 i32 i64
packed _mm_add_ps _mm_add_pd _mm_add_epi8 _mm_add_epi16 _mm_add_epi32 _mm_add_epi64
scaler _mm_add_ss _mm_add_sd       _mm_add_si64[1]

[1]:操作数类型为__m64

adds 饱和(saturation)加

本加法使得A和B相加的结果不会超出范围。若结果超出整数范围上界,则返回上界,若超出下界,则返回下界。否则返回结果。

  i8 i16 u8 u16
packed _mm_adds_epi8 _mm_adds_epi16 _mm_adds_epu8 _mm_adds_epu16

减法

将操作数A减去操作数B,并返回结果。

  SPFP DPFP i8 i16 i32 i64
packed _mm_sub_ps _mm_sub_pd _mm_sub_epi8 _mm_sub_epi16 _mm_sub_epi32 _mm_sub_epi64
scaler _mm_sub_ss _mm_sub_sd NULL NULL NULL _mm_sub_si64[1]

[1]:操作数类型为__m64

subs 饱和减

将A与B相减。若结果超出整数范围上界,则返回上界,若超出下界,则返回下界,否则返回结果。

  i8 i16 u8 u16
packed _mm_subs_epi8 _mm_subs_epi16 _mm_subs_epu8 _mm_subs_epu16

乘法

操作数A乘以操作数B,并返回结果

  SPFP DPFP u32
packed _mm_mul_ps _mm_mul_pd _mm_mul_epu32
scaler _mm_mul_ss _mm_mul_sd _mm_mul_su32[1]

[1]:操作数为__m64,取A和B的最低数相乘,结果为64bit整数,保存在__m64

mulhi 乘法并取高位结果

将操作数A与B相乘,得到中间结果,该中间结果位长是乘数的位长的两倍。然后,取出中间结果高一半的比特,放入最终结果。这样做是在乘法溢出时,仍然能够得到更长位数的准确的结果。

  i16 u16
packed _mm_mulhi_epi16 _mm_mulhi_epu16, _mm_mulhi_pu16

mullo 乘法并取低位结果

将操作数A与B相乘,取得中间结果,中间结果的长度是A的两倍,然后取中间结果的低位数的结果,该结果长度与A等长。

  i16
packed _mm_mullo_epi16

除法

操作数A除以操作数B,并返回结果

  SPFP DPFP
packed _mm_div_ps _mm_div_pd
scaler _mm_div_ss _mm_div_sd

平方根

计算操作数A的平方根。

  SPFP DPFP
packed _mm_sqrt_ps _mm_sqrt_pd
scaler _mm_sqrt_ss _mm_sqrt_sd[1]

[1]:此函数输入两个参数A和B,结果的低位是B的平方根,高位是A的高位DPFP。

倒数

倒数

  SPFP
packed _mm_rcp_ps
scaler _mm_rcp_ss

平方根倒数

先求平方根,然后求其倒数,最后取近似值。

  SPFP
packed _mm_rsqrt_ps
scaler _mm_rsqrt_ss

最大值

A和B对应位置的数相比较,将较大的放到结果中,并返回

  SPFP DPFP u8 i16
packed _mm_max_ps _mm_max_pd _mm_max_epu8, _mm_max_pu8 _mm_max_epi16, _mm_max_pi16
scaler _mm_max_ss _mm_max_sd    

最小值

A和B对应位置的数相比较,将较小的放到结果中,并返回

  SPFP DPFP u8 i16
packed _mm_min_ps _mm_min_pd _mm_min_epu8, _mm_min_pu8 _mm_min_epi16, _mm_min_pi16
scaler _mm_min_ss _mm_min_sd    

逻辑运算型

运算的函数名遵循一个格式:_mm_<op>_<type>

op可以是:

  • and:与运算
  • or:或运算
  • andnot:与非,先非A,然后再与B
  • xor:异或运算

type可以是:

  • ps
  • pd
  • si128

数值比较型

比较1

_mm_cmp<比较运算>_<类型>

比较运算有:

  • eq:等于
  • lt:小于
  • le:小于等于
  • gt:大于
  • ge:大于等于
  • neq:不等于
  • ngt:不大于
  • nge:不大于等于
  • nlt:不小于
  • nle:不小于等于
  • ord:是否都不是NaN
  • uord:两个数中是否至少一个NaN

支持类型说明:ps,pd,ss,sd具有所有的比较运算。但是i8,i16,i32仅支持eq,lt,gt。

比较2

比较两个寄存器的最低数,返回布尔结果,1为真,0为假。

格式:_mm_comi<比较运算>_<类型>

比较运算有:

  • eq:等于
  • lt:小于
  • le:小于等于
  • gt:大于
  • ge:大于等于
  • neq:不等于

仅支持sd与ss。

比较3

格式:_mm_ucomi<比较运算>_<类型>

比较两个寄存器的最低数,返回布尔结果,1为真,0为假。与上面的运算不同的地方在,本运算在处理QNaNs时不会引发错误。其余都是一样的。

数据转换型

内嵌原语 操作 对应SSE指令
__m128 _mm_cvtpi32x2_ps(__m64 a, __m64 b); 转换a的2个32bit有符号整数与b的2个32bit有符号整数 composite

数据读取型

操作 含义 ss ps sd pd pi[1] pd1 si16 si32 si64 si128
load 读取,需对齐 1 1 1 1           1
loadu 读取,无需对齐   1   1     1 1 1 1
loadr 反向读取   1   1            
loadh 读取到高位数字       1 1          
loadl 读取到低位数字       1 1          
load1 读取并复制到所有位置   1   1            

[1]:本函数有两个操作数,A与B。将会复制B到结果的对应的位置(根据是loadh还是loadl),其余的位置从A中对应位置读取。

特殊函数

_mm_load_pd1_mm_load_ps1:本函数读取传入的地址的一个数,并放到目标的所有位置

数据设置型

内嵌原语 操作 对应SSE指令
__m128 _mm_set_ss(float w); 读取w到结果最低位,其余为0 Composite
__m128 _mm_set1_ps(float w); 读取w,并把所有位都设置为w Composite
__m128 _mm_set_ps(float z, float y, float x, float w); 读取z、y、x、w,从最高位到最低位设置数值 Composite
__m128 _mm_setr_ps(float z, float y, float x, float w); 读取z、y、x、w,从最低位到最高位设置数值 Composite
__m128 _mm_setzero_ps(void); 返回全为0的结果 Composite

set 设置全部数字

函数参数为所需要设置的所有数字,返回设置完参数的对应数据结构

格式为:_mm_set_<type>

type可以是:

  • epi16
  • epi32
  • epi64:接收__m64类型的参数
  • epi64x:接收__int64类型的参数
  • epi8
  • pd
  • pd1:将目标寄存器的所有位置都设置为一个数
  • ps
  • ps1
  • sd
  • ss

set1 设置所有位置为1个数字

格式为:_mm_set1_<type>

type可以是:

  • epi16
  • epi32
  • epi64
  • epi64x
  • epi8
  • pd
  • ps

setr 与set的设置方向相反

格式:_mm_setr_<type>

type为:

  • epi16
  • epi32
  • epi64
  • epi8
  • pd
  • ps

数据存储型

内嵌原语 操作 对应SSE指令
void _mm_storeh_pi(__m64 *p, __m128 a); 保存2个a的高位SPFP到p MOVHPS mem, reg
void _mm_storel_pi(__m64 *p, __m128 a); 保存2个a的低位SPFP到p MOVLPS mem, reg
void _mm_store_ss(float * p, __m128 a); 保存a的最低位SPFP到p MOVSS
void _mm_store1_ps(float * p, __m128 a); 保存a的最低位SPFP到p数组的前4个位置 Shuffling + MOVSS
void _mm_store_ps(float *p, __m128 a); 保存a的4个SPFP到p数组,p地址必须16字节对齐 MOVAPS
void _mm_storeu_ps(float *p, __m128 a); 保存a的4个SPFP到p数组,p地址不须16字节对齐 MOVUPS
void _mm_storer_ps(float * p, __m128 a); 以相反方向保存a的4个SPFP到p数组,p必须16字节对齐 MOVAPS + Shuffling

缓存操作型

内嵌原语 操作 对应SSE指令
void _mm_prefetch(char const*a, int sel); 读取1个缓存行到一个靠近处理器的地方,sel定义了预读取的类型 PREFETCH
void _mm_stream_pi(__m64 *p, __m64 a); 保存a的数据到p而不影响当前缓存数据,需要事先清空MMX寄存器的状态 MOVNTQ
void _mm_stream_ps(float *p, __m128 a); 保存a的数据到p而不影响当前缓存数据,p必须16字节对齐 MOVNTPS
void _mm256_stream_ps(float *p, __m256 a); 保存a的数据到p而不影响当前缓存数据,p必须是32字节对齐的 VMOVNTPS
void _mm_sfence(void); 确保后续的存储操作进行之前,前面所有的保存操作是全局可见的 SFENCE

预读取资料:https://software.intel.com/sites/default/files/article/326703/5.3-prefetching-on-mic-4.pdf

整数型

内嵌原语 操作 对应SSE指令
int _mm_extract_pi16(__m64 a, int n); 根据n的值提取16bit整数,0时取a0,以此类推 PEXTRW
__m64 _mm_insert_pi16(__m64 a, int d, int n); 根据n的值,插入d到a中,0时插入到a0位置,其余不变 PINSRW
__m64 _mm_max_pi16(__m64 a, __m64 b); 比较ab的每一位,取最大值放入结果 PMAXSW
__m64 _mm_max_pu8(__m64 a, __m64 b); 比较ab的每一位无符号8bit整数,取最大值放入结果 PMAXUB
__m64 _mm_min_pi16(__m64 a, __m64 b); 取ab中的最小值放入结果 PMINSW
__m64 _mm_min_pu8(__m64 a, __m64 b); 取ab中的最小值放入结果 PMINUB
__m64 _mm_movemask_pi8(__m64 b); 取b中所有的符号位,依次放入结果中,结果为8bit PMOVMSKB
__m64 _mm_mulhi_pu16(__m64 a, __m64 b); a和b的数字相乘,取32bit结果的高16bit放入最终结果中 PMULHUW
__m64 _mm_shuffle_pi16(__m64 a, int n); 移动a中数字位置,n0和n1比特确定结果最低位是a的哪个数字,n2和n3确定a1,依次类推 PSHUFW
void _mm_maskmove_si64(__m64 d, __m64 n, char *p); 根据n中每一个比特是0还是1,依次确定是否将d的8bit数字放入p的对应位置中,如n0是1,则放d0到p0,如果n1是0,则p1不变 MASKMOVQ
__m64 _mm_avg_pu8(__m64 a, __m64 b); 计算ab对应位置数字的平均值 PAVGB
__m64 _mm_avg_pu16(__m64 a, __m64 b); 计算ab对应位置数字的平均值(?) PAVGW
__m64 _mm_sad_pu8(__m64 a, __m64 b); 计算ab对应数值的差值绝对值的和 PSADBW

读写寄存器

内嵌原语 操作 对应SSE指令
unsigned int _mm_getcsr(void); 返回控制寄存器的内容 STMXCSR
void _mm_setcsr(unsigned int i); 设置控制寄存器的内容 LDMXCSR

杂项

内嵌原语 操作 对应SSE指令
__m128 _mm_shuffle_ps(__m128 a, __m128 b, unsigned int imm8); 分别选择ab中的两个64bit数,放入结果中。如imm8为10(2),将会选择a0放到最低位,b1放到最高位。 SHUFPS
__m128 _mm_unpackhi_ps(__m128 a, __m128 b); 将a2,b2,a3,b3放入结果 UNPCKHPS
__m128 _mm_unpacklo_ps(__m128 a, __m128 b); 将a0,b0,a1,b1放入结果 UNPCKLPS
__m128 _mm_move_ss( __m128 a, __m128 b); 设置最低位为b0,其余从a取出 MOVSS
__m128 _mm_movehl_ps(__m128 a, __m128 b); 将b2,b3,a2,a3放入结果 MOVHLPS
__m128 _mm_movelh_ps(__m128 a, __m128 b); 将a0,a1,b0,b1放入结果 MOVLHPS
int _mm_movemask_ps(__m128 a); 构建4bit数,每个bit是对应位置的数的符号位 MOVMSKPS