使用SSE、AVX指令集 处理 单精度浮点数组求和(支持vc、gcc,兼

作者:zyl910。

  本文面对对SSE等SIMD指令集有一定基础的读者,以单精度浮点数组求和为例演示了如何跨平台使用SSE、AVX指令集。因使用了stdint、zintrin、ccpuid这三个模块,可以完全避免手工编写汇编代码,具有很高可移植性。支持vc、gcc编译器,香港空间,在Windows、Linux、Mac这三大平台上成功运行。

一、问题背景

  最初,我们只能使用汇编语言来编写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编译器。

  其次是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检查通过后,还需要进一步验证。

二、范例讲解2.1 事先准备

  为了解决上面提到的问题,我编写了三个模块——stdint:智能支持C99的stdint.h,解决整数类型问题。最新版的地址是 。zintrin:在编译时检测Intrinsic函数集支持性,并自动引入相关头文件、修正细节问题。最新版的地址是 。ccpuid:在编译时检测指令集的支持性。最新版的地址是 。

  这三个模块的纯C版就是一个头文件,用起来很方便,将它们放在项目中,直接#include就行了。例如——

#define __STDC_LIMIT_MACROS 1 // C99整数范围常量. [纯C程序可以不用, 而C++程序必须定义该宏.]#include #include

  因为stdint.h会被zintrin.h或ccpuid.h引用,所以不需要手动引入它。  因为它们用到了C99整数范围常量,所以应该在程序的最前面定义__STDC_LIMIT_MACROS宏(或者可以在项目配置、编译器命令行等位置进行配置)。根据C99规范,纯C程序可以不用, 而C++程序必须定义该宏。本文为了演示,定义了该宏。

2.2 C语言版

  我们先用C语言编写一个基本的单精度浮点数组求和函数——

// 单精度浮点数组求和_基本版.//// result: 返回数组求和结果.// pbuf: 数组的首地址.sumfloat_base(const float* pbuf, size_t cntbuf){float s = 0; // 求和变量. size_t i;for(i=0; i<cntbuf; ++i){s += pbuf[i];}return s;}

  该函数很容易理解——先将返回值赋初值0,然后循环加上数组中每一项的值。

2.3 SSE版2.3.1 SSE普通版

  SSE寄存器是128位的,对应__m128类型,它能一次能处理4个单精度浮点数。  很多SSE指令要求内存地址按16字节对齐。本文为了简化,假定浮点数组的首地址是总是16字节对齐的,仅需要考虑数组长度不是4的整数倍问题。  因使用了SSE Intrinsic函数,我们可以根据zintrin.h所提供的INTRIN_SSE宏进行条件编译。  代码如下——

#ifdef INTRIN_SSEsumfloat_sse(const float* pbuf, size_t cntbuf){float s = 0; // 求和变量. size_t i;size_t nBlockWidth = 4; // 块宽. SSE寄存器能一次处理4个float.size_t cntBlock = cntbuf / nBlockWidth; // 块数.size_t cntRem = cntbuf % nBlockWidth; // 剩余数量.__m128 xfsSum = _mm_setzero_ps(); // 求和变量。[SSE] 赋初值0__m128 xfsLoad; * p = pbuf; * q; // 将SSE变量上的多个数值合并时所用指针.(i=0; i<cntBlock; ++i){xfsLoad = _mm_load_ps(p); // [SSE] 加载xfsSum = _mm_add_ps(xfsSum, xfsLoad); // [SSE] 单精浮点紧缩加法p += nBlockWidth;}// 合并.q = (const float*)&xfsSum;s = q[0] + q[1] + q[2] + q[3];(i=0; i<cntRem; ++i){s += p[i];}return s;}

教育人的,激励人的,安慰人不开心的. 或者是 诗词 诗经里的..

使用SSE、AVX指令集 处理 单精度浮点数组求和(支持vc、gcc,兼

相关文章:

你感兴趣的文章:

标签云: