快好知 kuaihz

SSE指令集优化心得(一)

SSE指令优化心得(一)

背景

SIMD(single-instruction,multiple-data)是一种使用单道指令处理多道数据流的CPU执行模式,即在一个CPU指令执行周期内用一道指令完成处理多个数据的操作。

常见的指令

•       MMX(Multi-Media Extensions,多媒体扩展),主要问题是只对整数起作用,不支持浮点计算;

•       SSE(Streaming SIMDExtensions,单指令多数据流扩展),兼容MMX指令,可以提高浮点运算速度。

•       SSE2、SSE3、SSE4(是SSE的扩展技术)

•       3DNow!

•       X86

•       AVX(Advanced VectorExtensions)沿用了的MMX/SSE指令集,指令格式上有一些变化,增强了SIMD计算性能。

 

问题

最初,我们只能使用汇编语言来编写SIMD代码。不仅写起来很麻烦,而且易读性、可维护性、移植性都较差。不久,VC、GCC等编译器相继支持了Intrinsic函数,使我们可以摆脱汇编,利用C语言来调用SIMD指令集,大大提高了易读性和可维护。而且移植性也有提高,能在同一编译器上实现32位与64位的平滑过渡。但当代码在另一种编译器编译时,会遇到一些问题而无法编译。甚至在使用同一种编译器的不同版本时,也会遇到无法编译问题。

  ——首先是整数类型问题——

  传统C语言的short、int、long等整数类型是与平台相关的,不同平台上的位长是不同的(例如Windows是LLP64模型,Linux、Mac等Unix系统多采用LP64模型)。而使用SSE等SIMD指令集时需要精确计算数据的位数,不同位长的数据必须使用不同的指令来处理。有一个解决办法,就是使用C99标准中stdint.h所提供的指定位长的整数类型。GCC对C99标准支持性较好,而VC的步骤很慢,貌似直到VC2010才支持stdint.h。而很多时候我们为了兼容旧代码,不得不使用VC6等老版本的VC编译器。

 

1/9

——其次是Intrinsic函数的头文件问题——

不同编译器所使用的头文件不同——对于早期版本VC,需要根据具体的指令集需求,手动引入mmintrin.h、xmmintrin.h等头文件。对于VC2005或更高版本,引入intrin.h就行了,它会自动引入当前编译器所支持的所有Intrinsic头文件。对于早期版本GCC,也是手动引入mmintrin.h、xmmintrin.h等头文件。而对于高版本的GCC,引入x86intrin.h就行了,它会自动引入当前编译环境所允许的Intrinsic头文件。

 

  ——再次是当前编译环境下的Intrinsic函数集支持性问题——

  对于VC来说,VC6支持MMX、3DNow!、SSE、SSE2,然后更高版本的VC支持更多的指令集。但是,VC没有提供检测Intrinsic函数集支持性的办法。例如你在VC2010上编写了一段使用了AVX Intrinsic函数的代码,但拿到VC2005上就不能通过编译了。其次,VC不支持64位下的MMX,这让一些老程序迁徙到64位版时遭来了一些麻烦。

   而对于GCC来说,它使用-mmmx、-msse等编译器开关来启用各种指令集,同时定义了对应的__MMX__、__SSE__等宏,然后x86intrin.h会根据这些宏来声明相应的Intrinsic函数集。__MMX__、__SSE__等宏可以帮助我们判断Intrinsic函数集是否支持,但这只是GCC的专用功能。

   此外还有一些细节问题,例如某些Intrinsic函数仅在64下才能使用、有些老版本编译器的头文件缺少某个Intrinsic函数。所以我们希望有一种统一的方式来判断Intrinsic函数集的支持性。

 

  ——除了编译期间的问题外,还有运行期间的问题——

  在运行时,怎么检测当前处理器支持哪些指令集?

   虽然X86体系提供了用来检测处理器的CPUID指令,但它没有规范的Intrinsic函数,在不同的编译器上的用法不同。

   而且X86体系有很多种指令集,每种指令集具体的检测方法是略有区别的。尤其是SSE、AVX这样的SIMD指令集是需要操作系统配合才能正常使用的,所以在CPUID检查通过后,还需要进一步验证。

SSE介绍

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

2/9

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

intrinsic内联函数

在C/C++程序中使用SSE指令有两种方式:

•       直接嵌入汇编指令(内嵌式汇编语言);

•       使用编译器提供的支持SSE的intrinsics内联函数 (从代码可读和维护角度讲,通过intrinsics内联函数的形式来使用SSE更好)。

/**   内嵌式汇编语言使用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函数是对MMX、SSE等指令集的一种封装,以函数的形式提供,在编译的时候,这些函数会被内联为汇编,不会产生函数调用的开销。

头文件

Visual Studio使用SSE指令集需要添加对应的头文件:

intrin.h    --> All Architectures

mmintrin.h  --> MMX

xmmintrin.h -->  SSE

emmintrin.h--> SSE2

pmmintrin.h--> SSE3

smmintrin.h--> SSE4

immintrin.h --> AVX

3/9

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

SSE指令集支持的处理器有8个128位的寄存器( xmm0 -xmm7 ),每一个寄存器可以存放4个(32位)单精度的浮点数。SSE 的浮点数运算指令就是使用这些寄存器。下图是SSE 新增的寄存器的示意图:

 

 

__m128数据类型

SSE使用4个浮点数(4*32bit)组合成一个新的数据类型__m128 ,对应128位的寄存器。SSE指令的参数和返回结果的数据类型都是__m128。

比如:__m128 _mm_add_ps(__m128 a, __m128 b); //两个四维向量相加

 

SSE浮点运算指令分类

•       packed指令是一次对XMM寄存器中的四个浮点数(即DATA0 ~DATA3)均进行计算;

•       scalar只对XMM暂存器中的DATA0进行计算。

SSE指令格式 _mm__(参数表)

•       前缀_mm,表示是SSE指令集对应的Intrinsic函数;

4/9

•       表示指令的作用,比如加法add;

•       是ps或者ss,分别表示为packed或者scalar;

如 __m128 _mm_add_ps(__m128 a, __m128 b);//两个四维向量相加

 

内存对齐

•       SSE指令要求处理的数据16字节(128位二进制)对齐,也就是每16个字节分为一组。

•       静态数组(static array)可由__declspec(align(16))关键字声明:

__declspec(align(16))float m_fArray[ARRAY_SIZE];

•       在 xxmintrin.h中定义了一个宏__MM_ALIGN16,所以上面的程序也可以写成:

_MM_ALIGN16float m_fArray[ARRAY_SIZE];

•       动态数组(dynamic array)可由_aligned_malloc函数为其分配空间:

m_fArray =(float*) _aligned_malloc(ARRAY_SIZE * sizeof(float), 16);

•       由_aligned_malloc函数分配空间的动态数组可以由_aligned_free函数释放其占用的空间:

_aligned_free(m_fArray);

 

•       以_mm_load_ps函数为例,其使用示例如下:

 

 

这里加载正确的前提是:input这个浮点数阵列都是对齐在16 bytes的边上。如果没有对齐,就需要使用_mm_loadu_ps函数,这个函数用于处理没有对齐在16bytes上的数据,但是其速度会比较慢。

 

【注意】GCC编译器和VC编译器下字节对齐是不同的:

       GCC : __attribute__((aligned(16)))

       VC  : __declspec(align(16))

 

Intrinsic SSE相关指令

Load系列(用于加载数据,从内存到寄存器)

·           __m128 _mm_load_ss (float*p) 

·           __m128 _mm_load_ps (float*p) 

·           __m128 _mm_load1_ps (float*p) 

·           __m128 _mm_loadh_pi (__m128 a,__m64 *p) 

·           __m128 _mm_loadl_pi (__m128 a,__m64 *p) 

·           __m128 _mm_loadr_ps (float*p) 

·           __m128 _mm_loadu_ps (float*p)     // 不要求16字节对齐

 

Set系列(用于加载数据,从内存到寄存器,大部分需要多条指令完成,但是可能不需要16字节对

5/9

本站资源来自互联网,仅供学习,如有侵权,请通知删除,敬请谅解!
搜索建议:SSE指令集优化心得  指令  指令词条  优化  优化词条  心得  心得词条  SSE  SSE词条  
电脑

 wifi链接不上,怎么回事?

WiFi也称为移动热点,也可以理解为无线上网。下面,我们来看看wifi链接不上,怎么回事吧。操作方法01:连接点击右下角的【宽带连接】图标,然后找到WiFi,右...(展开)

电脑优化

 电脑关机很慢怎么办?

电脑关机很慢怎么办?有的小伙伴的电脑在点击电源关机后,屏幕一直显示"正在关机",有时候需要十几分钟才能真正关机,这个到底是怎么回事呢?今天小...(展开)