背景
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
•
•
如 __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