社区应用 最新帖子 精华区 社区服务 会员列表 统计排行 银行

  • 22573阅读
  • 14回复

用SSE指令集增强程序的性能

级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 10楼 发表于: 2013-08-11
在不编写 AVX 代码的情况下使用 AVX
摘要

英特尔® 高级矢量扩展指令集(AVX)是一套针对英特尔® SIMD 流指令扩展(英特尔® SSE)的 256 位扩展指令集,专为浮点密集型应用而设计。英特尔® SSE 和英特尔® AVX 均为单指令多数据指令集的示例。英特尔® AVX 作为第二代英特尔® 酷睿™ 处理器家族的一部分发布。英特尔® AVX 采用更宽的 256 位矢量 - 一种全新的扩展指令格式(矢量扩展指令集或 VEX)并具备丰富的功能,使系统性能得到显著提升。
该指令集架构支持三种操作数,可提升指令编程灵活性,并支持非破坏性的源操作数。传统的 128 位 SIMD 指令也经过了扩展,支持三种操作数和新的指令加密格式 (VEX)。指令加密格式介绍了使用操作码和前缀,以处理器能够理解的格式来表达更高级别的指令的方式。这有助于实现对数据和一般应用的更好的管理,例如图像、音视频处理、科研模拟、金融分析和三维建模与分析。
本文讨论了开发人员可通过哪些方式将英特尔® AVX 集成到应用中,且无需在低级别汇编语言中进行明确地编码。对于 C/C++ 开发人员来说,访问英特尔® AVX 的最直接方式是使用兼容 C 的内部指令。这些内部函数提供了到英特尔® AVX 指令集的访问,以及英特尔® 短矢量数学库 (SVML) 中的更高级别的数学函数。这些函数分别在 immintrin.h 和 ia32intrin.h 头文件中进行声明。应用编程人员还可通过其它方法来使用英特尔® AVX,而且无需将英特尔® AVX 指令添加至其源代码。本文针对这些方法进行了调查(使用英特尔® C++ Composer XE 2011,定位于在 Sandy Bridge 系统上执行)。Linux*、Windows* 和 Mac OS* X 平台均支持英特尔® C++ Composer XE。本文将使用面向 Windows* 平台的命令行开关。
本文是《英特尔® 多线程应用开发指南》系列文章中的一篇,旨在为开发人员开发适用于英特尔® 平台的高效多线程应用提供指导。
背景
支持矢量或 SIMD 的处理器能够在一次指令中,同时在多个数据操作数上执行一个操作。在一个数字上由另外一个数字执行的操作以生成单个结果的流程被称作标量流程。在 N 个数字上同时执行的操作以生成 N 个结果的流程被称作矢量流程 (N > 1)。英特尔处理器或支持 SIMD 或 AVX 指令的兼容的非英特尔处理器均支持该技术。将算法从标量转化为矢量的流程被称作矢量化。
建议
面向英特尔® AVX 的重新编译
第一个方法是使用 /QaxAVX 编译器开关进行重新编译。无需对源代码进行修改。英特尔® 编译器将生成相应的 128 和 256 位英特尔® AVX VEX 加密指令。当有助于提高性能时,英特尔® 编译器将针对英特尔处理器生成多个特定处理器,且具备自动分布功能的代码路径。最合适的代码将在运行时执行。
编译器自动矢量化
借助合适的架构开关来编译应用,是构建英特尔® AVX 就绪型应用的第一步。借助自动矢量化功能,编译器可代表软件开发人员执行大部分矢量化工作。自动矢量化是满足特定条件时编译器执行的优化。英特尔® C++ 编译器可在生成代码期间自动执行相应的矢量化操作。英特尔® C++ 编译器矢量化指南详细介绍了矢量化。当优化级别为 /O2 或更高时,英特尔编译器将寻找矢量化机遇。
让我们来考虑一个简单的矩阵矢量乘法示例,该示例随英特尔® C++ ComposeXE 提供,详细阐释了矢量化的概念。下列代码片段来自 vec_samples 归档的 Multiply.c 中的 matvec 函数:

如果没有矢量化,外层循环将执行 size1 时间,内层循环将执行 size1*size2 时间。借助 /QaxAVX 开关实现矢量化以后,内层循环可以展开(unrolled),这是因为可在每次操作的单个指令中执行四次乘法和四次加法。矢量化循环的效率比标量循环高得多。英特尔® AVX 的优势还适用于单精度浮点数字,因为 8 个单精度浮点操作数可以存于 ymm 寄存器中。
循环必须满足特定的标准才能实现矢量化。在运行时进入循环时,必须要知道循环运行次数。运行次数可以是变量,但在执行循环时必须是常量。循环必须具备单进和单出能力,而且退出不能依赖于输入数据。此外还存在一些分支标准,例如不允许开关语句(switch statement)。如果 If 语句可作为隐蔽任务实施,则可允许这种类型的语句。最内层的循环最有可能是矢量化的对象,而且在循环内部使用函数调用可能会影响矢量化。内联函数和固有的 SVML 函数可增加矢量化机遇。
在应用开发的实施和调试阶段,建议对矢量化信息进行检查。英特尔® 编译器提供了矢量化报告,可帮助你了解被矢量化以及未被矢量化的元素。该报告可通过 /Qvec-report= 命令行选项提供,其中 n 指定了报告的详细级别。详细级别随 n 数值的增加而增加。如果 n=3,则可以提供相关性信息、被矢量化的循环和未被矢量化的循环。开发人员可根据报告中的信息来修改实施,循环未被矢量化的原因提供了非常有帮助的信息。
开发人员在其具体应用方面具有深入的专业知识,因此有时可以忽略自动矢量化行为。编译指示提供了额外的信息,以便为自动矢量化流程提供帮助。部分示例包括:一直对循环进行矢量化操作、确定循环内的数据保持一致、忽略潜在的数据相关性等。addFloats 示例对部分重要点进行了说明。你需要检查生成的汇编语言指令,以了解所生成的编译器。当指定 /S 命令行选项时,英特尔编译器将在当前的工作目录中生成汇编文件。

请注意 simd 和矢量编译指示的使用。它们在实现所期望的英特尔® AVX 256 位矢量化方面起着重要作用。向代码添加 "#pragma simd" 有助于生成英特尔® 128 位 AVX 指令的打包版本。此外,编译器还将展开循环,从而减少与循环测试结束相关的执行指令数量。指定 "pragma vector aligned" 有助于编译器针对所有阵列参考使用一致的数据移动指令。使用 "pragma simd" 和 "pragma vector aligned." 可生成期望的 256 位英特尔® AVX 指令。英特尔® 编译器选择 vmovups,这是因为当访问第二代英特尔®酷睿TM处理器上的一致内存时,使用不一致的转移指令不会出现任何问题。
使 #pragma simd 和 #pragma 矢量保持一致

这展示了英特尔® 编译器的部分自动矢量化能力。矢量化可通过矢量报告确认(simd 声称编译指令),或者通过检查生成的汇编语言指令来确认。如果开发人员对其应用有着深刻的了解,那么编译指令能够为编译器提供进一步的帮助。请参考英特尔® C++ 编译器矢量化指南了解关于英特尔编译器中的矢量化的更多信息。英特尔® C++ 编译器 XE 12.0 用户和参考指南提供了关于使用矢量化、编译指令和编译器开关的额外信息。英特尔编译器可为您完成大部分的矢量化工作,因此您的应用可以随时使用英特尔® AVX。
面向阵列符号(Notation)的英特尔® Cilk™ Plus C/C++ 扩展
面向阵列符号的英特尔® Cilk™ Plus C/C++ 语言扩展是专用于英特尔的语言扩展,适用于算法在阵列上运行的情况,不需要阵列元素之间的特定操作顺序。如果使用阵列符号来表达算法并通过 AVX 开关进行编译,英特尔® 编译器将生成英特尔® AVX 指令。面向阵列符号的 C/C++ 语言扩展旨在帮助用户在其程序中直接表达高级并行矢量阵列操作。这可帮助编译器执行数据相关性分析、矢量化和自动并行化。从开发人员的角度来看,他们将获得更加可预测的矢量化、改进的性能和更高的硬件资源利用率。通过结合使用面向阵列符号的 C/C++ 语言扩展和其它英特尔® CilkTM Plus 语言扩展,有助于简化并行和矢量化应用开发流程。
要实现上述优势,开发人员可以编写标准的 C/C++ 基本函数,以便通过标量句法来表示操作。在不使用面向阵列符号的 C/C++ 语言扩展的情况下调用时,该基本函数可用于在一个元素上进行操作,必须使用“__declspec(vector)”对该基础函数进行声明,以便用户能够通过面向阵列符号的 C/C++ 语言扩展来调用。
multiplyValues 示例作为一个基础函数来展示:

该标量调用通过该简单的示例进行说明:

此外,借助面向阵列符号的 C/C++ 语言扩展,该函数还可在整个阵列或阵列的一部分上来操作。片段操作符(section operator)可用于要在其上进行操作的阵列部分。句法: [ : : ]
下限是源阵列的开始索引、长度是结果阵列的长度,跨度表示的是整个源阵列的跨度。跨度是可选的,默认是一个。
这些阵列部分示例有助于阐释具体的使用方式:

此外,符号还支持多维阵列。

借助阵列符号,用户可以轻松地调用使用阵列的 multiplyValues。英特尔® 编译器提供了矢量化版本,可以分别执行相应的操作。以下为您列举了部分实例:第一个示例在整个阵列上操作,第二个则在阵列的一个子集或部分上操作。
该示例调用了整个阵列的函数:
a[:] = multiplyValues(b[:], c[:]);
该示例调用了阵列的一个子集的函数:
a[0:5] = multiplyValues(b[0:5], c[0:5]);
这些简单的示例显示了,面向阵列标记的 C/C++ 语言扩展如何使用英特尔® AVX 的特性,而且不需要开发人员明确地使用任何英特尔® AVX 指令。无论是否使用基础函数,都可以使用面向阵列标记的 C/C++ 语言扩展。该技术使用最新的英特尔® AVX 指令集架构,为开发人员提供了更高的灵活性和更多的选择。请参考英特尔® C++ 编译器 XE 12.0 用户和参考指南,了解面向阵列标记的英特尔® Cilk™ Plus C/C++ 语言扩展的更多信息。
使用英特尔® IPP 和英特尔® MKL 库
借助英特尔® 集成性能基元库和英特尔® 数学核心函数库,英特尔针对多媒体、数据处理、加密和通信应用提供了数千个高度优化的软件函数。这些线程安全库支持多种操作系统,最快的代码将在指定平台上运行。通过这种方式,用户可以轻松地向应用添加多核并行化和矢量化能力,并利用最新的处理器指令来执行代码。英特尔® 集成性能基元库 7.0 包括大约 175 个针对英特尔® AVX 而优化的函数。这些函数可用于执行 FFT、过滤、卷积、重新调整大小等操作。英特尔® 数学核心函数库 10.2 支持面向 BLASS (dgemm)、FFT 和 VML (exp, log, pow) 的英特尔® AVX。实施过程在英特尔® MKL 10.3 中得到了简化,因为开始不再需要调用 mkl_enable_instructions。英特尔® MKL 10.3 可扩展英特尔® AVX,以便支持 DGMM/SGEMM、radix-2 Complex FFT、最真实的 VML 函数以及 VSL 分布生成器。
如果您已经在使用,或者考虑使用这些版本的库,那么您的应用将能够使用英特尔® AVX 指令集。在 Sandy Bridge 平台上运行时,库将执行英特尔® AVX 指令,并且支持 Linux*、Windows* 和 Mac OS* X 平台。
如欲了解关于针对英特尔® AVX 而优化的英特尔® IPP 函数的更多信息,请访问:/en-us/articles/intel-ipp-functions-optimized-for-intel-avx-intel-advanced-vector-extensions。如欲了解关于英特尔® MKL AVX 支持的更多信息,请访问:Intel® AVX Optimization in Intel® MKL V10.3
使用准则
人们对更高计算性能的需求促使英特尔在微架构和指令集领域不断进行创新。应用开发人员希望确保他们的产品能够利用技术上的进步,且无需投入更多的开发资源。本文介绍的方法、工具和库可帮助开发人员从英特尔® 高级矢量扩展指令集的发展上获益,而且无需编写英特尔® AVX 汇编语言。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 11楼 发表于: 2013-08-11
使用SSE、AVX指令集处理单精度浮点数组求和

本文面对对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,解决整数类型问题。最新版的地址是http://www.cnblogs.com/zyl910/archive/2012/08/08/c99int.html 。
zintrin:在编译时检测Intrinsic函数集支持性,并自动引入相关头文件、修正细节问题。最新版的地址是http://www.cnblogs.com/zyl910/archive/2012/10/01/zintrin_v101.html 。
ccpuid:在编译时检测指令集的支持性。最新版的地址是 http://www.cnblogs.com/zyl910/archive/2012/10/13/ccpuid_v103.html 。

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

复制代码
#define __STDC_LIMIT_MACROS1// C99整数范围常量. [纯C程序可以不用, 而C++程序必须定义该宏.]#include "zintrin.h"#include "ccpuid.h"
复制代码

 

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


2.2 C语言版

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

复制代码
// 单精度浮点数组求和_基本版.//// result: 返回数组求和结果.// pbuf: 数组的首地址.// cntbuf: 数组长度.float sumfloat_base(const float* pbuf, size_t cntbuf){float s = 0;// 求和变量.size_t i;for(i=0; i<cntbuf; ++i){s += pbuf;}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_SSE// 单精度浮点数组求和_SSE版.float sumfloat_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;// 加载.const float* p = pbuf;// SSE批量处理时所用的指针.const float* q;// 将SSE变量上的多个数值合并时所用指针.// SSE批量处理.for(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];// 处理剩下的.for(i=0; i<cntRem; ++i){s += p;}return s;}#endif// #ifdef INTRIN_SSE
复制代码

 

  上述代码大致可分为四个部分——
1. 变量定义与初始化。
2. SSE批量处理。即对前面能凑成4个一组的数据,利用SSE的128位宽度同时对4个数累加。
3. 合并。将__m128上的多个数值合并到求和变量。因考虑某些编译器不能直接使用“.”来访问__m128变量中的数据,于是利用指针q来访问xfsSum中的数据。
4. 处理剩下的。即对尾部不能凑成4个一组的数据,采用基本的逐项相加算法。

  上述代码总共用到了3个SSE Intrinsic函数——
_mm_setzero_ps:对应XORPS指令。将__m128上的每一个单精度浮点数均赋0值,伪代码:for(i=0;i<4;++i) C=0.0f。
_mm_load_ps:对应MOVPS指令。从内存中对齐加载4个单精度浮点数到__m128变量,伪代码:for(i=0;i<4;++i) C=_A
_mm_add_ps:对应ADDPS指令。相加,即对2个__m128变量的4个单精度浮点数进行垂直相加,伪代码:for(i=0;i<4;++i) C=A+B


2.3.2 SSE四路循环展开版

  循环展开可以降低循环开销,提高指令级并行性能。
  一般来说,四路循环展开就差不多够了。我们可以很方便的将上一节的代码改造为四路循环展开版——

复制代码
// 单精度浮点数组求和_SSE四路循环展开版.float sumfloat_sse_4loop(const float* pbuf, size_t cntbuf){float s = 0;// 返回值.size_t i;size_t nBlockWidth = 4*4;// 块宽. SSE寄存器能一次处理4个float,然后循环展开4次.size_t cntBlock = cntbuf / nBlockWidth;// 块数.size_t cntRem = cntbuf % nBlockWidth;// 剩余数量.__m128 xfsSum = _mm_setzero_ps();// 求和变量。[SSE] 赋初值0__m128 xfsSum1 = _mm_setzero_ps();__m128 xfsSum2 = _mm_setzero_ps();__m128 xfsSum3 = _mm_setzero_ps();__m128 xfsLoad;// 加载.__m128 xfsLoad1;__m128 xfsLoad2;__m128 xfsLoad3;const float* p = pbuf;// SSE批量处理时所用的指针.const float* q;// 将SSE变量上的多个数值合并时所用指针.// SSE批量处理.for(i=0; i<cntBlock; ++i){xfsLoad = _mm_load_ps(p);// [SSE] 加载.xfsLoad1 = _mm_load_ps(p+4);xfsLoad2 = _mm_load_ps(p+8);xfsLoad3 = _mm_load_ps(p+12);xfsSum = _mm_add_ps(xfsSum, xfsLoad);// [SSE] 单精浮点紧缩加法xfsSum1 = _mm_add_ps(xfsSum1, xfsLoad1);xfsSum2 = _mm_add_ps(xfsSum2, xfsLoad2);xfsSum3 = _mm_add_ps(xfsSum3, xfsLoad3);p += nBlockWidth;}// 合并.xfsSum = _mm_add_ps(xfsSum, xfsSum1);// 两两合并(0~1).xfsSum2 = _mm_add_ps(xfsSum2, xfsSum3);// 两两合并(2~3).xfsSum = _mm_add_ps(xfsSum, xfsSum2);// 两两合并(0~3).q = (const float*)&xfsSum;s = q[0] + q[1] + q[2] + q[3];// 处理剩下的.for(i=0; i<cntRem; ++i){s += p;}return s;}
复制代码

 


2.4 AVX版

2.4.1 AVX普通版

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

  代码如下——

复制代码
#ifdef INTRIN_AVX// 单精度浮点数组求和_AVX版.float sumfloat_avx(const float* pbuf, size_t cntbuf){float s = 0;// 求和变量.size_t i;size_t nBlockWidth = 8;// 块宽. AVX寄存器能一次处理8个float.size_t cntBlock = cntbuf / nBlockWidth;// 块数.size_t cntRem = cntbuf % nBlockWidth;// 剩余数量.__m256 yfsSum = _mm256_setzero_ps();// 求和变量。[AVX] 赋初值0__m256 yfsLoad;// 加载.const float* p = pbuf;// AVX批量处理时所用的指针.const float* q;// 将AVX变量上的多个数值合并时所用指针.// AVX批量处理.for(i=0; i<cntBlock; ++i){yfsLoad = _mm256_load_ps(p);// [AVX] 加载yfsSum = _mm256_add_ps(yfsSum, yfsLoad);// [AVX] 单精浮点紧缩加法p += nBlockWidth;}// 合并.q = (const float*)&yfsSum;s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7];// 处理剩下的.for(i=0; i<cntRem; ++i){s += p;}return s;}#endif// #ifdef INTRIN_AVX
复制代码

 

  由上可见,将SSE Intrinsic代码(sumfloat_sse)升级为 AVX Intrinsic代码(sumfloat_avx)是很容易的——
1. 升级数据类型,将__m128升级成了__m256。
2. 升级Intrinsic函数,在函数名中加入255。例如_mm_setzero_ps、_mm_load_ps、_mm_add_ps,对应的AVX版函数是 _mm256_setzero_ps、_mm256_load_ps、_mm256_add_ps。
3. 因位宽翻倍,地址计算与数据合并的代码需稍加改动。

  当使用VC2010编译含有AVX的代码时,VC会提醒你——
warning C4752: 发现 Intel(R) 高级矢量扩展;请考虑使用 /arch:AVX

  目前“/arch:AVX”尚未整合到项目属性的“C++\代码生成\启用增强指令集”中,需要手动在项目属性的“C++\命令行”的附加选项中加上“/arch:AVX”——

详见MSDN——
http://msdn.microsoft.com/zh-cn/library/7t5yh4fd(v=vs.100).aspx
在 Visual Studio 中设置 /arch:AVX 编译器选项
1.打开项目的“属性页”对话框。 有关更多信息,请参见 如何:打开项目属性页。 
2.单击“C/C++”文件夹。
3.单击“命令行”属性页。
4.在“附加选项”框中添加 /arch:AVX。


2.4.2 AVX四路循环展开版

  同样的,我们可以编写AVX四路循环展开版——

复制代码
// 单精度浮点数组求和_AVX四路循环展开版.float sumfloat_avx_4loop(const float* pbuf, size_t cntbuf){float s = 0;// 求和变量.size_t i;size_t nBlockWidth = 8*4;// 块宽. AVX寄存器能一次处理8个float,然后循环展开4次.size_t cntBlock = cntbuf / nBlockWidth;// 块数.size_t cntRem = cntbuf % nBlockWidth;// 剩余数量.__m256 yfsSum = _mm256_setzero_ps();// 求和变量。[AVX] 赋初值0__m256 yfsSum1 = _mm256_setzero_ps();__m256 yfsSum2 = _mm256_setzero_ps();__m256 yfsSum3 = _mm256_setzero_ps();__m256 yfsLoad;// 加载.__m256 yfsLoad1;__m256 yfsLoad2;__m256 yfsLoad3;const float* p = pbuf;// AVX批量处理时所用的指针.const float* q;// 将AVX变量上的多个数值合并时所用指针.// AVX批量处理.for(i=0; i<cntBlock; ++i){yfsLoad = _mm256_load_ps(p);// [AVX] 加载.yfsLoad1 = _mm256_load_ps(p+8);yfsLoad2 = _mm256_load_ps(p+16);yfsLoad3 = _mm256_load_ps(p+24);yfsSum = _mm256_add_ps(yfsSum, yfsLoad);// [AVX] 单精浮点紧缩加法yfsSum1 = _mm256_add_ps(yfsSum1, yfsLoad1);yfsSum2 = _mm256_add_ps(yfsSum2, yfsLoad2);yfsSum3 = _mm256_add_ps(yfsSum3, yfsLoad3);p += nBlockWidth;}// 合并.yfsSum = _mm256_add_ps(yfsSum, yfsSum1);// 两两合并(0~1).yfsSum2 = _mm256_add_ps(yfsSum2, yfsSum3);// 两两合并(2~3).yfsSum = _mm256_add_ps(yfsSum, yfsSum2);// 两两合并(0~3).q = (const float*)&yfsSum;s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7];// 处理剩下的.for(i=0; i<cntRem; ++i){s += p;}return s;}
复制代码

 

2.5 测试框架

2.5.1 测试所用的数组

  首先考虑一下测试所用的数组的长度应该是多少比较好。
  为了避免内存带宽问题,这个数组最好能放在L1 Data Cache中。现在的处理器的L1 Data Cache一般是32KB,为了保险最好再除以2,那么数组的长度应该是 32KB/(2*sizeof(float))=4096。
  其次考虑内存对齐问题,avx要求32字节对齐。我们可以定义一个ATTR_ALIGN宏来统一处理变量的内存对齐问题。
  该数组定义如下——

复制代码
// 变量对齐.#ifndef ATTR_ALIGN#  if defined(__GNUC__)// GCC#define ATTR_ALIGN(n)__attribute__((aligned(n)))#  else// 否则使用VC格式.#define ATTR_ALIGN(n)__declspec(align(n))#  endif#endif// #ifndef ATTR_ALIGN#define BUFSIZE4096// = 32KB{L1 Cache} / (2 * sizeof(float))ATTR_ALIGN(32) float buf[BUFSIZE];
复制代码

 


2.5.2 测试函数

  如果为每一个函数都编写一套测试代码,那不仅代码量大,而且不易维护。
  可以考虑利用函数指针来实现一套测试框架。
  因sumfloat_base等函数的签名是一致的,于是可以定义这样的一种函数指针——
// 测试时的函数类型
typedef float (*TESTPROC)(const float* pbuf, size_t cntbuf);

  然后再编写一个对TESTPROC函数指针进行测试的函数——

复制代码
// 进行测试void runTest(const char* szname, TESTPROC proc){const int testloop = 4000;// 重复运算几次延长时间,避免计时精度问题.const clock_t TIMEOUT = CLOCKS_PER_SEC/2;// 最短测试时间.int i,j,k;clock_ttm0, dt;// 存储时间.double mps;// M/s.double mps_good = 0;// 最佳M/s. 因线程切换会导致的数值波动, 于是选取最佳值.volatile float n=0;// 避免内循环被优化.for(i=1; i<=3; ++i)// 多次测试.{tm0 = clock();// maink=0;do{for(j=1; j<=testloop; ++j)// 重复运算几次延长时间,避免计时开销带来的影响.{n = proc(buf, BUFSIZE);// 避免内循环被编译优化消掉.}++k;dt = clock() - tm0;}while(dt<TIMEOUT);// showmps = (double)k*testloop*BUFSIZE*CLOCKS_PER_SEC/(1024.0*1024.0*dt);// k*testloop*BUFSIZE/(1024.0*1024.0) 将数据规模换算为M,然后再乘以 CLOCKS_PER_SEC/dt 换算为M/s .if (mps_good<mps)mps_good=mps;// 选取最佳值.//printf("%s:\t%.0f M/s\t//%f\n", szname, mps, n);}printf("%s:\t%.0f M/s\t//%f\n", szname, mps_good, n);}
复制代码

 

  j是最内层的循环,负责多次调用TESTPROC函数指针。如果每调用一次TESTPROC函数指针后又调用clock函数,那会带来较大的计时开销,影响评测成绩。
  k循环负责检测超时。当发现超过预定时限,便计算mps,即每秒钟处理了多少百万个单精度浮点数。然后存储最佳的mps。
  i是最外层循环的循环变量,循环3次然后报告最佳值。


2.5.3 进行测试

  在进行测试之前,需要对buf数组进行初始化,将数组元素赋随机值——

复制代码
// init bufsrand( (unsigned)time( NULL ) );for (i = 0; i < BUFSIZE; i++) buf = (float)(rand() & 0x3f);// 使用&0x3f是为了让求和后的数值不会超过float类型的有效位数,便于观察结果是否正确.
复制代码

 

  然后可以开始测试了——

复制代码
// testrunTest("sumfloat_base", sumfloat_base);// 单精度浮点数组求和_基本版.#ifdef INTRIN_SSEif (simd_sse_level(NULL) >= SIMD_SSE_1){runTest("sumfloat_sse", sumfloat_sse);// 单精度浮点数组求和_SSE版.runTest("sumfloat_sse_4loop", sumfloat_sse_4loop);// 单精度浮点数组求和_SSE四路循环展开版.}#endif// #ifdef INTRIN_SSE#ifdef INTRIN_AVXif (simd_avx_level(NULL) >= SIMD_AVX_1){runTest("sumfloat_avx", sumfloat_avx);// 单精度浮点数组求和_AVX版.runTest("sumfloat_avx_4loop", sumfloat_avx_4loop);// 单精度浮点数组求和_AVX四路循环展开版.}#endif// #ifdef INTRIN_AVX
复制代码

 

  INTRIN_SSE、INTRIN_AVX 宏是 zintrin.h 提供的,用于在编译时检测编译器是否支持SSE、AVX指令集。
  simd_sse_level、simd_avx_level函数是 ccpuid.h 提供的,用于在运行时检测当前系统环境是否支持SSE、AVX指令集。


2.6 杂项

  为了方便对比测试,可以在程序启动时显示程序版本、编译器名称、CPU型号信息。即在main函数中加上——

复制代码
char szBuf[64];int i;printf("simdsumfloat v1.00 (%dbit)\n", INTRIN_WORDSIZE);printf("Compiler: %s\n", COMPILER_NAME);cpu_getbrand(szBuf);printf("CPU:\t%s\n", szBuf);printf("\n");
复制代码

 

  INTRIN_WORDSIZE 宏是 zintrin.h 提供的,为当前机器的字长。
  cpu_getbrand是 ccpuid.h 提供的,用于获得CPU型号字符串。
  COMPILER_NAME 是一个用来获得编译器名称的宏,它的详细定义是——

复制代码
// Compiler name#define MACTOSTR(x)#x#define MACROVALUESTR(x)MACTOSTR(x)#if defined(__ICL)// Intel C++#  if defined(__VERSION__)#define COMPILER_NAME"Intel C++ " __VERSION__#  elif defined(__INTEL_COMPILER_BUILD_DATE)#define COMPILER_NAME"Intel C++ (" MACROVALUESTR(__INTEL_COMPILER_BUILD_DATE) ")"#  else#define COMPILER_NAME"Intel C++"#  endif// #  if defined(__VERSION__)#elif defined(_MSC_VER)// Microsoft VC++#  if defined(_MSC_FULL_VER)#define COMPILER_NAME"Microsoft VC++ (" MACROVALUESTR(_MSC_FULL_VER) ")"#  elif defined(_MSC_VER)#define COMPILER_NAME"Microsoft VC++ (" MACROVALUESTR(_MSC_VER) ")"#  else#define COMPILER_NAME"Microsoft VC++"#  endif// #  if defined(_MSC_FULL_VER)#elif defined(__GNUC__)// GCC#  if defined(__CYGWIN__)#define COMPILER_NAME"GCC(Cygmin) " __VERSION__#  elif defined(__MINGW32__)#define COMPILER_NAME"GCC(MinGW) " __VERSION__#  else#define COMPILER_NAME"GCC " __VERSION__#  endif// #  if defined(_MSC_FULL_VER)#else#  define COMPILER_NAME"Unknown Compiler"#endif// #if defined(__ICL)// Intel C++
复制代码

 

 

三、全部代码

3.1 simdsumfloat.c

  全部代码——

复制代码
simdsumfloat.c
复制代码

 


3.2 makefile

  全部代码——

复制代码
makefile
复制代码

 


四、编译测试

4.1 编译

  在以下编译器中成功编译——
VC6:x86版。
VC2003:x86版。
VC2005:x86版。
VC2010:x86版、x64版。
GCC 4.7.0(Fedora 17 x64):x86版、x64版。
GCC 4.6.2(MinGW(20120426)):x86版。
GCC 4.7.1(TDM-GCC(MinGW-w64)):x86版、x64版。
llvm-gcc-4.2(Mac OS X Lion 10.7.4, Xcode 4.4.1):x86版、x64版。


4.2 测试

  因虚拟机上的有效率损失,于是仅在真实系统上进行测试。

  系统环境——
CPU:Intel(R) Core(TM) i3-2310M CPU @ 2.10GHz
操作系统:Windows 7 SP1 x64版

  然后分别运行VC与GCC编译的Release版可执行文件,即以下4个程序——
exe\simdsumfloat_vc32.exe:VC2010 SP1 编译的32位程序,/O2 /arch:SSE2。
exe\simdsumfloat_vc64.exe:VC2010 SP1 编译的64位程序,/O2 /arch:AVX。
exe\simdsumfloat_gcc32.exe:GCC 4.7.1(TDM-GCC(MinGW-w64)) 编译的32位程序,-O3 -mavx。
exe\simdsumfloat_gcc64.exe:GCC 4.7.1(TDM-GCC(MinGW-w64)) 编译的64位程序,-O3 -mavx。

  测试结果(使用cmdarg_ui)——

 



源码下载—— 
http://files.cnblogs.com/zyl910/simdsumfloat.rar

QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 12楼 发表于: 2013-08-11
3.1 simdsumfloat.c



simdsumfloat.c


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


#include <stdlib.h>
#include <stdio.h>
#include <time.h>


#include "zintrin.h"
#include "ccpuid.h"




// Compiler name
#define MACTOSTR(x)    #x
#define MACROVALUESTR(x)    MACTOSTR(x)
#if defined(__ICL)    // Intel C++
#  if defined(__VERSION__)
#    define COMPILER_NAME    "Intel C++ " __VERSION__
#  elif defined(__INTEL_COMPILER_BUILD_DATE)
#    define COMPILER_NAME    "Intel C++ (" MACROVALUESTR(__INTEL_COMPILER_BUILD_DATE) ")"
#  else
#    define COMPILER_NAME    "Intel C++"
#  endif    // #  if defined(__VERSION__)
#elif defined(_MSC_VER)    // Microsoft VC++
#  if defined(_MSC_FULL_VER)
#    define COMPILER_NAME    "Microsoft VC++ (" MACROVALUESTR(_MSC_FULL_VER) ")"
#  elif defined(_MSC_VER)
#    define COMPILER_NAME    "Microsoft VC++ (" MACROVALUESTR(_MSC_VER) ")"
#  else
#    define COMPILER_NAME    "Microsoft VC++"
#  endif    // #  if defined(_MSC_FULL_VER)
#elif defined(__GNUC__)    // GCC
#  if defined(__CYGWIN__)
#    define COMPILER_NAME    "GCC(Cygmin) " __VERSION__
#  elif defined(__MINGW32__)
#    define COMPILER_NAME    "GCC(MinGW) " __VERSION__
#  else
#    define COMPILER_NAME    "GCC " __VERSION__
#  endif    // #  if defined(_MSC_FULL_VER)
#else
#  define COMPILER_NAME    "Unknown Compiler"
#endif    // #if defined(__ICL)    // Intel C++




//////////////////////////////////////////////////
// sumfloat: 单精度浮点数组求和的函数
//////////////////////////////////////////////////


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


#ifdef INTRIN_SSE
// 单精度浮点数组求和_SSE版.
float sumfloat_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;    // 加载.
    const float* p = pbuf;    // SSE批量处理时所用的指针.
    const float* q;    // 将SSE变量上的多个数值合并时所用指针.


    // SSE批量处理.
    for(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];


    // 处理剩下的.
    for(i=0; i<cntRem; ++i)
    {
        s += p;
    }


    return s;
}


// 单精度浮点数组求和_SSE四路循环展开版.
float sumfloat_sse_4loop(const float* pbuf, size_t cntbuf)
{
    float s = 0;    // 返回值.
    size_t i;
    size_t nBlockWidth = 4*4;    // 块宽. SSE寄存器能一次处理4个float,然后循环展开4次.
    size_t cntBlock = cntbuf / nBlockWidth;    // 块数.
    size_t cntRem = cntbuf % nBlockWidth;    // 剩余数量.
    __m128 xfsSum = _mm_setzero_ps();    // 求和变量。[SSE] 赋初值0
    __m128 xfsSum1 = _mm_setzero_ps();
    __m128 xfsSum2 = _mm_setzero_ps();
    __m128 xfsSum3 = _mm_setzero_ps();
    __m128 xfsLoad;    // 加载.
    __m128 xfsLoad1;
    __m128 xfsLoad2;
    __m128 xfsLoad3;
    const float* p = pbuf;    // SSE批量处理时所用的指针.
    const float* q;    // 将SSE变量上的多个数值合并时所用指针.


    // SSE批量处理.
    for(i=0; i<cntBlock; ++i)
    {
        xfsLoad = _mm_load_ps(p);    // [SSE] 加载.
        xfsLoad1 = _mm_load_ps(p+4);
        xfsLoad2 = _mm_load_ps(p+8);
        xfsLoad3 = _mm_load_ps(p+12);
        xfsSum = _mm_add_ps(xfsSum, xfsLoad);    // [SSE] 单精浮点紧缩加法
        xfsSum1 = _mm_add_ps(xfsSum1, xfsLoad1);
        xfsSum2 = _mm_add_ps(xfsSum2, xfsLoad2);
        xfsSum3 = _mm_add_ps(xfsSum3, xfsLoad3);
        p += nBlockWidth;
    }
    // 合并.
    xfsSum = _mm_add_ps(xfsSum, xfsSum1);    // 两两合并(0~1).
    xfsSum2 = _mm_add_ps(xfsSum2, xfsSum3);    // 两两合并(2~3).
    xfsSum = _mm_add_ps(xfsSum, xfsSum2);    // 两两合并(0~3).
    q = (const float*)&xfsSum;
    s = q[0] + q[1] + q[2] + q[3];


    // 处理剩下的.
    for(i=0; i<cntRem; ++i)
    {
        s += p;
    }


    return s;
}
#endif    // #ifdef INTRIN_SSE




#ifdef INTRIN_AVX
// 单精度浮点数组求和_AVX版.
float sumfloat_avx(const float* pbuf, size_t cntbuf)
{
    float s = 0;    // 求和变量.
    size_t i;
    size_t nBlockWidth = 8;    // 块宽. AVX寄存器能一次处理8个float.
    size_t cntBlock = cntbuf / nBlockWidth;    // 块数.
    size_t cntRem = cntbuf % nBlockWidth;    // 剩余数量.
    __m256 yfsSum = _mm256_setzero_ps();    // 求和变量。[AVX] 赋初值0
    __m256 yfsLoad;    // 加载.
    const float* p = pbuf;    // AVX批量处理时所用的指针.
    const float* q;    // 将AVX变量上的多个数值合并时所用指针.


    // AVX批量处理.
    for(i=0; i<cntBlock; ++i)
    {
        yfsLoad = _mm256_load_ps(p);    // [AVX] 加载
        yfsSum = _mm256_add_ps(yfsSum, yfsLoad);    // [AVX] 单精浮点紧缩加法
        p += nBlockWidth;
    }
    // 合并.
    q = (const float*)&yfsSum;
    s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7];


    // 处理剩下的.
    for(i=0; i<cntRem; ++i)
    {
        s += p;
    }


    return s;
}


// 单精度浮点数组求和_AVX四路循环展开版.
float sumfloat_avx_4loop(const float* pbuf, size_t cntbuf)
{
    float s = 0;    // 求和变量.
    size_t i;
    size_t nBlockWidth = 8*4;    // 块宽. AVX寄存器能一次处理8个float,然后循环展开4次.
    size_t cntBlock = cntbuf / nBlockWidth;    // 块数.
    size_t cntRem = cntbuf % nBlockWidth;    // 剩余数量.
    __m256 yfsSum = _mm256_setzero_ps();    // 求和变量。[AVX] 赋初值0
    __m256 yfsSum1 = _mm256_setzero_ps();
    __m256 yfsSum2 = _mm256_setzero_ps();
    __m256 yfsSum3 = _mm256_setzero_ps();
    __m256 yfsLoad;    // 加载.
    __m256 yfsLoad1;
    __m256 yfsLoad2;
    __m256 yfsLoad3;
    const float* p = pbuf;    // AVX批量处理时所用的指针.
    const float* q;    // 将AVX变量上的多个数值合并时所用指针.


    // AVX批量处理.
    for(i=0; i<cntBlock; ++i)
    {
        yfsLoad = _mm256_load_ps(p);    // [AVX] 加载.
        yfsLoad1 = _mm256_load_ps(p+8);
        yfsLoad2 = _mm256_load_ps(p+16);
        yfsLoad3 = _mm256_load_ps(p+24);
        yfsSum = _mm256_add_ps(yfsSum, yfsLoad);    // [AVX] 单精浮点紧缩加法
        yfsSum1 = _mm256_add_ps(yfsSum1, yfsLoad1);
        yfsSum2 = _mm256_add_ps(yfsSum2, yfsLoad2);
        yfsSum3 = _mm256_add_ps(yfsSum3, yfsLoad3);
        p += nBlockWidth;
    }
    // 合并.
    yfsSum = _mm256_add_ps(yfsSum, yfsSum1);    // 两两合并(0~1).
    yfsSum2 = _mm256_add_ps(yfsSum2, yfsSum3);    // 两两合并(2~3).
    yfsSum = _mm256_add_ps(yfsSum, yfsSum2);    // 两两合并(0~3).
    q = (const float*)&yfsSum;
    s = q[0] + q[1] + q[2] + q[3] + q[4] + q[5] + q[6] + q[7];


    // 处理剩下的.
    for(i=0; i<cntRem; ++i)
    {
        s += p;
    }


    return s;
}


#endif    // #ifdef INTRIN_AVX






//////////////////////////////////////////////////
// main
//////////////////////////////////////////////////


// 变量对齐.
#ifndef ATTR_ALIGN
#  if defined(__GNUC__)    // GCC
#    define ATTR_ALIGN(n)    __attribute__((aligned(n)))
#  else    // 否则使用VC格式.
#    define ATTR_ALIGN(n)    __declspec(align(n))
#  endif
#endif    // #ifndef ATTR_ALIGN




#define BUFSIZE    4096    // = 32KB{L1 Cache} / (2 * sizeof(float))
ATTR_ALIGN(32) float buf[BUFSIZE];


// 测试时的函数类型
typedef float (*TESTPROC)(const float* pbuf, size_t cntbuf);


// 进行测试
void runTest(const char* szname, TESTPROC proc)
{
    const int testloop = 4000;    // 重复运算几次延长时间,避免计时精度问题.
    const clock_t TIMEOUT = CLOCKS_PER_SEC/2;    // 最短测试时间.
    int i,j,k;
    clock_t    tm0, dt;    // 存储时间.
    double mps;    // M/s.
    double mps_good = 0;    // 最佳M/s. 因线程切换会导致的数值波动, 于是选取最佳值.
    volatile float n=0;    // 避免内循环被优化.
    for(i=1; i<=3; ++i)    // 多次测试.
    {
        tm0 = clock();
        // main
        k=0;
        do
        {
            for(j=1; j<=testloop; ++j)    // 重复运算几次延长时间,避免计时开销带来的影响.
            {
                n = proc(buf, BUFSIZE);    // 避免内循环被编译优化消掉.
            }
            ++k;
            dt = clock() - tm0;
        }while(dt<TIMEOUT);
        // show
        mps = (double)k*testloop*BUFSIZE*CLOCKS_PER_SEC/(1024.0*1024.0*dt);    // k*testloop*BUFSIZE/(1024.0*1024.0) 将数据规模换算为M,然后再乘以 CLOCKS_PER_SEC/dt 换算为M/s .
        if (mps_good<mps)    mps_good=mps;    // 选取最佳值.
        //printf("%s:\t%.0f M/s\t//%f\n", szname, mps, n);
    }
    printf("%s:\t%.0f M/s\t//%f\n", szname, mps_good, n);
}


int main(int argc, char* argv[])
{
    char szBuf[64];
    int i;


    printf("simdsumfloat v1.00 (%dbit)\n", INTRIN_WORDSIZE);
    printf("Compiler: %s\n", COMPILER_NAME);
    cpu_getbrand(szBuf);
    printf("CPU:\t%s\n", szBuf);
    printf("\n");


    // init buf
    srand( (unsigned)time( NULL ) );
    for (i = 0; i < BUFSIZE; i++) buf = (float)(rand() & 0x3f);    // 使用&0x3f是为了让求和后的数值不会超过float类型的有效位数,便于观察结果是否正确.


    // test
    runTest("sumfloat_base", sumfloat_base);    // 单精度浮点数组求和_基本版.
#ifdef INTRIN_SSE
    if (simd_sse_level(NULL) >= SIMD_SSE_1)
    {
        runTest("sumfloat_sse", sumfloat_sse);    // 单精度浮点数组求和_SSE版.
        runTest("sumfloat_sse_4loop", sumfloat_sse_4loop);    // 单精度浮点数组求和_SSE四路循环展开版.
    }
#endif    // #ifdef INTRIN_SSE
#ifdef INTRIN_AVX
    if (simd_avx_level(NULL) >= SIMD_AVX_1)
    {
        runTest("sumfloat_avx", sumfloat_avx);    // 单精度浮点数组求和_AVX版.
        runTest("sumfloat_avx_4loop", sumfloat_avx_4loop);    // 单精度浮点数组求和_AVX四路循环展开版.
    }
#endif    // #ifdef INTRIN_AVX


    return 0;
}


3.2 makefile



makefile


# flags
CC = g++
CFS = -Wall -msse


# args
RELEASE =0
BITS =
CFLAGS =


# [args] 生成模式. 0代表debug模式, 1代表release模式. make RELEASE=1.
ifeq ($(RELEASE),0)
    # debug
    CFS += -g
else
    # release
    CFS += -O3 -DNDEBUG
    //CFS += -O3 -g -DNDEBUG
endif


# [args] 程序位数. 32代表32位程序, 64代表64位程序, 其他默认. make BITS=32.
ifeq ($(BITS),32)
    CFS += -m32
else
    ifeq ($(BITS),64)
        CFS += -m64
    else
    endif
endif


# [args] 使用 CFLAGS 添加新的参数. make CFLAGS="-mavx".
CFS += $(CFLAGS)




.PHONY : all clean


# files
TARGETS = simdsumfloat
OBJS = simdsumfloat.o


all : $(TARGETS)


simdsumfloat : $(OBJS)
    $(CC) $(CFS) -o $@ $^




simdsumfloat.o : simdsumfloat.c zintrin.h ccpuid.h
    $(CC) $(CFS) -c $<




clean :
    rm -f $(OBJS) $(TARGETS) $(addsuffix .exe,$(TARGETS))

QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 13楼 发表于: 2014-09-13
检查MMX和SSE系列指令集的支持级别
以前我写了一篇《[VC6] 检查MMX和SSE系列指令集的支持级别(最高SSE4.2)》(http://www.cnblogs.com/zyl910/archive/2012/03/01/checksimd.html)。现在发现该方法存在两点缺陷——
1.不支持64位,因为VC的64位程序不支持内嵌汇编;
2.没有区分硬件支持与操作系统支持。

  怎么解决这两点缺陷呢?
  对于第1点,可以利用Intrinsics函数来兼容32位和64位。为了更方便的使用CPUID指令,可以利用《如何在各个版本的VC及64位下使用CPUID指令》(http://www.cnblogs.com/zyl910/archive/2012/05/21/vcgetcpuid.html)的成果。
  对于第2点,考虑到再增加一组函数会使函数过多,于是决定采用增加一个指针参数的方式——函数的返回值用于返回操作系统支持性,指针参数用于返回硬件支持性。因为操作系统支持性,对开发SIMD程序来说更加重要。

一、检查MMX
  检查以下标志位可判断硬件是否支持MMX——
CPUID(1).EDX.MMX[bit 23]=1    // 硬件支持MMX

  检查完硬件支持性,还需检查操作系统的支持性。最简单的办法就是使用结构化异常处理来执行一条MMX指令。代码如下——

[pre]BOOL    simd_mmx(BOOL* phwmmx){    const INT32    BIT_D_MMX = 0x00800000;    // bit 23    BOOL    rt = FALSE;    // result    INT32 dwBuf[4];    // check processor support    __cpuid(dwBuf, 1);    // Function 1: Feature Information    if ( dwBuf[3] & BIT_D_MMX )    rt=TRUE;    if (NULL!=phwmmx)    *phwmmx=rt;    // check OS support    if ( rt )    {#if defined(_WIN64)        // VC编译器不支持64位下的MMX。        rt=FALSE;#else        __try         {            _mm_empty();    // MMX instruction: emms        }        __except (EXCEPTION_EXECUTE_HANDLER)        {            rt=FALSE;        }#endif    // #if defined(_WIN64)    }    return rt;}[/pre]


  根据Intel文档,似乎64位下也支持MMX指令。但是VC编译器似乎不允许在64位下使用MMX指令(例如对于_mm_empty,会报告找不到符号链接)。具体原因尚不清楚。

二、检查SSE

  检查以下标志位可判断硬件是否支持SSE——
CPUID(1).EDX.SSE[bit 25]=1    // 硬件支持SSE1
CPUID(1).EDX.SSE2[bit 26]=1    // 硬件支持SSE2
CPUID(1).ECX.SSE3[bit 0]=1    // 硬件支持SSE3
CPUID(1).ECX.SSSE3[bit 9]=1    // 硬件支持SSE3
CPUID(1).ECX.SSE41[bit 19]=1    // 硬件支持SSE4.1
CPUID(1).ECX.SSE42[bit 20]=1    // 硬件支持SSE4.2

  检查完硬件支持性,还需检查操作系统的支持性。很多资料说需要检查以下标志位——
CR0.EM[bit 2]=0    // 浮点模拟被禁止
CR4.OSFXSR[bit 9]=1    // 在进程切换时,操作系统支持保护SIMD浮点状态

  可是CR0、CR4这些控制寄存器只能在ring0中访问,而一般应用程序是ring3的,无法获得上述标志位信息。所以建议还是使用结构化异常处理来执行一条SSE指令。代码如下——

[pre]int    simd_sse_level(int* phwsse){    const INT32    BIT_D_SSE = 0x02000000;    // bit 25    const INT32    BIT_D_SSE2 = 0x04000000;    // bit 26    const INT32    BIT_C_SSE3 = 0x00000001;    // bit 0    const INT32    BIT_C_SSSE3 = 0x00000100;    // bit 9    const INT32    BIT_C_SSE41 = 0x00080000;    // bit 19    const INT32    BIT_C_SSE42 = 0x00100000;    // bit 20    int    rt = SIMD_SSE_NONE;    // result    INT32 dwBuf[4];    // check processor support    __cpuid(dwBuf, 1);    // Function 1: Feature Information    if ( dwBuf[3] & BIT_D_SSE )    {        rt = SIMD_SSE_1;        if ( dwBuf[3] & BIT_D_SSE2 )        {            rt = SIMD_SSE_2;            if ( dwBuf[2] & BIT_C_SSE3 )            {                rt = SIMD_SSE_3;                if ( dwBuf[2] & BIT_C_SSSE3 )                {                    rt = SIMD_SSE_3S;                    if ( dwBuf[2] & BIT_C_SSE41 )                    {                        rt = SIMD_SSE_41;                        if ( dwBuf[2] & BIT_C_SSE42 )                        {                            rt = SIMD_SSE_42;                        }                    }                }            }        }    }    if (NULL!=phwsse)    *phwsse=rt;    // check OS support    __try     {        __m128 xmm1 = _mm_setzero_ps();    // SSE instruction: xorps        if (0!=*(int*)&xmm1)    rt = SIMD_SSE_NONE;    // 避免Release模式编译优化时剔除上一条语句    }    __except (EXCEPTION_EXECUTE_HANDLER)    {        rt = SIMD_SSE_NONE;    }    return rt;}[/pre]

  “if (0!=*(int*)&xmm1) rt = SIMD_SSE_NONE;”是为了避免编译优化剔除无意义语句。“*(int*)&xmm1”返回xmm1中的首个int,如果上面的“_mm_setzero_ps()”执行成功,那么它应该是0。

三、全部代码
  全部代码——

[pre]#include <windows.h>#include <stdio.h>#include <conio.h>#include <tchar.h>#if _MSC_VER >=1400    // VC2005才支持intrin.h#include <intrin.h>    // 所有Intrinsics函数#else#include <emmintrin.h>    // MMX, SSE, SSE2#endif// SSE系列指令集的支持级别. simd_sse_level 函数的返回值。#define SIMD_SSE_NONE    0    // 不支持#define SIMD_SSE_1    1    // SSE#define SIMD_SSE_2    2    // SSE2#define SIMD_SSE_3    3    // SSE3#define SIMD_SSE_3S    4    // SSSE3#define SIMD_SSE_41    5    // SSE4.1#define SIMD_SSE_42    6    // SSE4.2const char*    simd_sse_names[] = {    "None",    "SSE",    "SSE2",    "SSE3",    "SSSE3",    "SSE4.1",    "SSE4.2",};char szBuf[64];INT32 dwBuf[4];#if defined(_WIN64)// 64位下不支持内联汇编. 应使用__cpuid、__cpuidex等Intrinsics函数。#else#if _MSC_VER < 1600    // VS2010. 据说VC2008 SP1之后才支持__cpuidexvoid __cpuidex(INT32 CPUInfo[4], INT32 InfoType, INT32 ECXValue){    if (NULL==CPUInfo)    return;    _asm{        // load. 读取参数到寄存器        mov edi, CPUInfo;    // 准备用edi寻址CPUInfo        mov eax, InfoType;        mov ecx, ECXValue;        // CPUID        cpuid;        // save. 将寄存器保存到CPUInfo        mov    [edi], eax;        mov    [edi+4], ebx;        mov    [edi+8], ecx;        mov    [edi+12], edx;    }}#endif    // #if _MSC_VER < 1600    // VS2010. 据说VC2008 SP1之后才支持__cpuidex#if _MSC_VER < 1400    // VC2005才支持__cpuidvoid __cpuid(INT32 CPUInfo[4], INT32 InfoType){    __cpuidex(CPUInfo, InfoType, 0);}#endif    // #if _MSC_VER < 1400    // VC2005才支持__cpuid#endif    // #if defined(_WIN64)// 取得CPU厂商(Vendor)//// result: 成功时返回字符串的长度(一般为12)。失败时返回0。// pvendor: 接收厂商信息的字符串缓冲区。至少为13字节。int cpu_getvendor(char* pvendor){    INT32 dwBuf[4];    if (NULL==pvendor)    return 0;    // Function 0: Vendor-ID and Largest Standard Function    __cpuid(dwBuf, 0);    // save. 保存到pvendor    *(INT32*)&pvendor[0] = dwBuf[1];    // ebx: 前四个字符    *(INT32*)&pvendor[4] = dwBuf[3];    // edx: 中间四个字符    *(INT32*)&pvendor[8] = dwBuf[2];    // ecx: 最后四个字符    pvendor[12] = '\0';    return 12;}// 取得CPU商标(Brand)//// result: 成功时返回字符串的长度(一般为48)。失败时返回0。// pbrand: 接收商标信息的字符串缓冲区。至少为49字节。int cpu_getbrand(char* pbrand){    INT32 dwBuf[4];    if (NULL==pbrand)    return 0;    // Function 0x80000000: Largest Extended Function Number    __cpuid(dwBuf, 0x80000000);    if (dwBuf[0] < 0x80000004)    return 0;    // Function 80000002h,80000003h,80000004h: Processor Brand String    __cpuid((INT32*)&pbrand[0], 0x80000002);    // 前16个字符    __cpuid((INT32*)&pbrand[16], 0x80000003);    // 中间16个字符    __cpuid((INT32*)&pbrand[32], 0x80000004);    // 最后16个字符    pbrand[48] = '\0';    return 48;}// 是否支持MMX指令集BOOL    simd_mmx(BOOL* phwmmx){    const INT32    BIT_D_MMX = 0x00800000;    // bit 23    BOOL    rt = FALSE;    // result    INT32 dwBuf[4];    // check processor support    __cpuid(dwBuf, 1);    // Function 1: Feature Information    if ( dwBuf[3] & BIT_D_MMX )    rt=TRUE;    if (NULL!=phwmmx)    *phwmmx=rt;    // check OS support    if ( rt )    {#if defined(_WIN64)        // VC编译器不支持64位下的MMX。        rt=FALSE;#else        __try         {            _mm_empty();    // MMX instruction: emms        }        __except (EXCEPTION_EXECUTE_HANDLER)        {            rt=FALSE;        }#endif    // #if defined(_WIN64)    }    return rt;}// 检测SSE系列指令集的支持级别int    simd_sse_level(int* phwsse){    const INT32    BIT_D_SSE = 0x02000000;    // bit 25    const INT32    BIT_D_SSE2 = 0x04000000;    // bit 26    const INT32    BIT_C_SSE3 = 0x00000001;    // bit 0    const INT32    BIT_C_SSSE3 = 0x00000100;    // bit 9    const INT32    BIT_C_SSE41 = 0x00080000;    // bit 19    const INT32    BIT_C_SSE42 = 0x00100000;    // bit 20    int    rt = SIMD_SSE_NONE;    // result    INT32 dwBuf[4];    // check processor support    __cpuid(dwBuf, 1);    // Function 1: Feature Information    if ( dwBuf[3] & BIT_D_SSE )    {        rt = SIMD_SSE_1;        if ( dwBuf[3] & BIT_D_SSE2 )        {            rt = SIMD_SSE_2;            if ( dwBuf[2] & BIT_C_SSE3 )            {                rt = SIMD_SSE_3;                if ( dwBuf[2] & BIT_C_SSSE3 )                {                    rt = SIMD_SSE_3S;                    if ( dwBuf[2] & BIT_C_SSE41 )                    {                        rt = SIMD_SSE_41;                        if ( dwBuf[2] & BIT_C_SSE42 )                        {                            rt = SIMD_SSE_42;                        }                    }                }            }        }    }    if (NULL!=phwsse)    *phwsse=rt;    // check OS support    __try     {        __m128 xmm1 = _mm_setzero_ps();    // SSE instruction: xorps        if (0!=*(int*)&xmm1)    rt = SIMD_SSE_NONE;    // 避免Release模式编译优化时剔除上一条语句    }    __except (EXCEPTION_EXECUTE_HANDLER)    {        rt = SIMD_SSE_NONE;    }    return rt;}int _tmain(int argc, _TCHAR* argv[]){    //__cpuidex(dwBuf, 0,0);    //__cpuid(dwBuf, 0);    //printf("%.8X\t%.8X\t%.8X\t%.8X\n", dwBuf[0],dwBuf[1],dwBuf[2],dwBuf[3]);    cpu_getvendor(szBuf);    printf("CPU Vendor:\t%s\n", szBuf);    cpu_getbrand(szBuf);    printf("CPU Name:\t%s\n", szBuf);    BOOL bhwmmx;    // 硬件支持MMX    BOOL bmmx;    // 操作系统支持MMX    bmmx = simd_mmx(&bhwmmx);    printf("MMX: %d\t// hw: %d\n", bmmx, bhwmmx);    int    nhwsse;    // 硬件支持SSE    int    nsse;    // 操作系统支持SSE    nsse = simd_sse_level(&nhwsse);    printf("SSE: %d\t// hw: %d\n", nsse, nhwsse);    for(int i=1; i<sizeof(simd_sse_names); ++i)    {        if (nhwsse>=i)    printf("\t%s\n", simd_sse_names);    }    return 0;}[/pre]


  在以下编译器中编译成功——
VC6(32位)
VC2003(32位)
VC2005(32位、64位)
VC2010(32位、64位)


四、测试结果

  在64位的win7中执行“x64\Release\checksimd64_2010.exe”,运行效果——


  还可以观察编译器生成的汇编代码,摘自“x64\Release\checksimd64.cod”——


[pre]; 187  :     // check OS support; 188  :     __try ; 189  :     {; 190  :         __m128 xmm1 = _mm_setzero_ps();    // SSE instruction: xorps  00077    0f 57 c0     xorps     xmm0, xmm0  0007a    0f 29 44 24 10     movaps     XMMWORD PTR xmm1$72829[rsp], xmm0; 191  :         if (0!=*(int*)&xmm1)    rt = SIMD_SSE_NONE;    // 避免Release模式编译优化时剔除上一条语句  0007f    83 7c 24 10 00     cmp     DWORD PTR xmm1$72829[rsp], 0  00084    45 0f 45 c2     cmovne     r8d, r10d  00088    44 89 04 24     mov     DWORD PTR rt$[rsp], r8d; 192  :     }
[/pre]

  可见Release模式下也正常生成了xorps指令,并没有被编译优化掉。

参考文献——
《Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B & 2C): Instruction Set Reference, A-Z》. May 2012. http://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-instruction-set-reference-manual-325383.html
《Intel® Processor Identification and the CPUID Instruction》. April 2012. http://developer.intel.com/content/www/us/en/processors/processor-identification-cpuid-instruction-note.html
《AMD64 Architecture Programmer's Manual Volume 3: General Purpose and System Instructions》. December 2011. http://support.amd.com/us/Processor_TechDocs/24594_APM_v3.pdf
《AMD CPUID Specification》. September 2010. http://support.amd.com/us/Embedded_TechDocs/25481.pdf
《[VC6] 检查MMX和SSE系列指令集的支持级别(最高SSE4.2)》:http://www.cnblogs.com/zyl910/archive/2012/03/01/checksimd.html
《如何在各个版本的VC及64位下使用CPUID指令》:http://www.cnblogs.com/zyl910/archive/2012/05/21/vcgetcpuid.html

源码下载——
http://files.cnblogs.com/zyl910/checksimd64.rar
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 14楼 发表于: 2015-06-28
大坑:丢失的SSE2 128bit/64bit 位移指令
逻辑位移

对于 MMX, SSE 的位移指令,我们很自然的想到:
逻辑左移:PSLLW/PSLLD/PSLLQ,Shift Packed Data Left Logical (压缩逻辑左移)
逻辑右移:PSRLW/PSRLD/PSRLQ,Shift Packed Data  Right Logical (压缩逻辑右移)
顾名思义,W 指的是Word(字),D 指的 DWORD (双字),Q 指的是 QWORD (四字),PSLLW 实现的是按 Word 的分组逻辑左移,
PSLLD 是按 DWORD 的分组逻辑左移,PSLLQ 是按 QWORD 实现的分组逻辑左移,这一切看起来都很 OK 。
这里以逻辑左移为例:

关于具体的逻辑左移指令的说明,可参考:
http://moeto.comoj.com/project/intel/instruct32_hh/vc256.htm
或者 http://x86.renejeschke.de/html/file_module_x86_id_259.html
右移也是类似的,在此不再螯述。

问题来了
  我们要实现的是 128bit 的逻辑位移,SSE2 里面有 PSLLDQ / PSRLDQ 指令,这里 DQ 即是 Double QWORD 的意思,
这不正好是我们需要的 128bit 按 bit 位移吗?No!!别高兴得太早,我们来看看 Intel 的文档:
PSLLDQ--Packed Shift Left Logical Double Quadword

http://moeto.comoj.com/project/intel/instruct32_hh/vc255.htm
截图如下:

我们看到,很遗憾,SSE2 并没有实现 128bit 的按 bit 位移,PSLLDQ 只能实现 128bit 的按 byte 位移,即最小位移量必须是一个 byte (即8个bit),这非常不科学,更不科学的是位移量只能是立即数!考虑到 Intel 并未真正实现 128bit 数据处理(SSE 大多数指令都只实现了最多 64bit 粒度的数据处理,例如一个双精度浮点数是 64bit 的),好吧,我们认了,但是!!但是!!Intel 你没搞错吧,PSLLDQ 的操作数只支持 imm8,imm8 意味着什么?imm8 是 8 位立即数的意思,那就是说我们只能在汇编里写死(常数),不能使用任何寄存器来做位移量。What the fu*K??
好吧,这我们也认了。。。CPU 是你设计的,我们拿你没办法。说句题外话,如果 PSLLDQ 支持 reg32, reg64 寄存器位移的话, 会方便很多,因为我们可以先用 PSLLDQ 位移足够位数的按 Byte 位移,然后再用 PSLLQ 位移剩下的剩余量(这是后话,为什么要这么用,到后面你就知道),可是,现在这种方法都不行!!这个 imm8 彻底让我蛋碎了。。。PSLLQ 对于128 bit 寄存器一次只能移 16 位(先破埂了),那么意味这我们如果要用这种方法,要 if / jump 好几次。。。

大坑开始
好吧,我们退而求其次,既然你不能实现 128 bit 的按 bit 位移,那我们分成两个 64 bit 的位移来实现好了,无非是多一次判断,多一次合并,虽然效率没有直接128 bit 位移的高,但是苦于你没实现嘛,只能这么干了。。。
好吧,我们开始吧。。。。GO!!!好了,我们换成 PSLLQ 了,执行PSLLQ xmm0, 32 或 PSLLQ xmm0, ecx (这里ecx的值为32),咦?xmm0怎么全为0了??啊,怎么回事??

我们回过头来重新看看 intel 的文档:

重点看两个我用红线框起来的,当 PSLLQ 作用于 64 bit 的寄存器时,我们看到是最大支持 COUNT = 64 位的位移(严格意义上讲是 max = 63,这个不纠结了,习惯问题,下同);
但是当 PSLLQ 作用于 128 bit 寄存器时,奇怪的事情发生了,最大只支持 COUNT = 16 位的位移(严格意义上是15位),如上图所示。
如果不是重新看 Intel 的文档,如果不是调试中发现问题,谁能想到最多只能移15位???Intel 的脑袋是被门夹了吗??Why??MMX 寄存器上都可以实现最多 63 位的位移, SSE 寄存器为什么就不可以?虽然我们知道 MMX 寄存器和 SSE 寄存器是不一样的,分开的,MMX 寄存器是借用 x87 浮点寄存器来实现 MMX 指令的,可是你在 MMX 寄存器上实现了 64 bit 的位移,为什么在 128 bit 的 SSE 寄存器上却只能移最多 15 位??你说难以实现,我认了,我不太懂为什么那么难,我们只能认了,可是你却实现了 128 bit 的按 byte 位移的 PSLLDQ 指令,这又作何解释??本来顾名思义,PSLLDQ 就来就应该是实现 128 bit 的按 bit 位移,限于历史原因,这个没实现我可以理解,可是你没有理由在 PSLLQ 作用于 128 bit 的 SSE 寄存器时却最多只能位移 15 位吧??这真的有那么难吗??真的难吗????真的那么难,你又是怎么实现 PSLLDQ 的 128 bit 按 Byte 位移的??

寻求答案
带着这些疑问,我们问了一下 Google 老先生,搜索“128 bit shift”,发现 N 多小伙伴都遇到过这个问题,例如:
Looking for sse 128 bit shift operation for non-immediate shift value
What is SSE !@#$% good for? #2: Bit vector operations

最后,Google老先生告诉了我们一个最好的解答,来自 Intel 的论坛,在这里:
Missing instruction in SSE: PSLLDQ with _bit_ shift amount?

是这样的,截图如下:

首先,Intel 是承认这个 missing instruction(丢失的指令)的,我们也意识到 missing instruction 无处不在,只是这个有点过分。
上面的回复,大意是:(E文不是太好,用 Google 辅助翻译的,见谅)

Hi Geoff,
  我们的一个工程师提供了以下回应,并做一些澄清。

  你这个问题是正确的,对于 SIMD(单指令多数据流) 来说,在当前的指令集里,bit 位移是比按 byte 位移难于实现的(指的是 SSE 寄存器
的 128 bit 按 bit 位移)。不幸的是,这不是一个小改变,实现一个这样的按 bit 位移指令。这里有更多的改变比简单的在立即字节里适应位移
距离--硬件实际完成按 bit 位移是一个被限制的问题。
  如果你有一个使用案例关于为什么这个操作是有用的,随着应用程序将受益于这个操作,这是我们有兴趣听到的。一般情况下,我们试图
设计新的指令来满足特定的需求,而不是只是提供 "missing instuctions“ (丢失指令)的支持。从实际情况来看,有很多这样的 "missing instuctions“
——更有趣的问题是,如果在实际的应用中应对这些 "missing instuctions“ 所带来的问题。

博主观点:
  对于 128 bit 的按 bit 位移比较难以实现,这我能理解,可是 PSLLQ 对于 SSE 的 128bit 寄存器只能最多位移15位我就不能理解了……
SSE2 的 128bit/64bit 位移你在哪里,为什么是15而不是31,63?亲爱的马航MH370,你到底在哪里?为什么要选择飞中国的航班?为什么??

解决之道
解决的办法有很多种,前面也讲过一个,就是:如果你要左移 count 位,先用 PSLLDQ 位移 x * 8 位, 这个是纯 128 bit 的位移,然后再用 PSLLQ 位移剩下的 y = (count - x * 8) 位,这里 y 要小于 16。但是由于 PSLLDQ 只能执行imm8立即数,所以你要先 if / jump 判断一下 count 的值,分别执行 PSLLDQ xmm0, 32; 或 PSLLDQ xmm0, 16; 或 PSLLDQ xmm0, 8; PSLLDQ xmm0, 4; PSLLDQ xmm0, 2; 以后,再执行 PSLLQ 位移剩下的 Y 位。这里PSLLDQ xmm0, 32也许可以用别的 SSE Shuffle 指令代替,但是是一样的,最大的问题是你要先 if 先判断一下再执行相应的指令,这种方法并不见得高效。
我们再来找一些好一点的办法:
既然,SSE 里我们没办法实现 64 bit 的位移,但是 MMX 寄存器里是可以的,但是我们又要在 SSE 寄存器里实现,那么我们可以先把数据从 SSE 寄存器里转移到 MMX 寄存器,位移好了,再合并到 SSE 寄存器里。虽然这个过程有点繁琐,但是相比上面第一种方法,还是高效了不少,而且有一关键的地方,很多时候,我们要做这个位移,都是接近最终输出结果的时候,这个时候就不必把数据合并回 SSE 寄存器了,可以直接用 MMX 寄存器的值作为输出即可,这样又快了一点儿,还不赖。
还有没有解决的办法,应该还有,容我再想一想,或者读者你也想想?有网友贴了 AVX 版的 VPSLLDQ 指令说明,可是同样只支持 imm8 立即数,而且并不是所有人的 CPU 都支持 AVX 的,博主本人的 CPU 就不支持。
(由于博主是睡到一半起来关电脑写下的这篇文章,所以我先去休息一会,有空再来补全这一块)

后记
我们现在遇到的问题做一个比喻,就是:我们前面有三条路,一条是大路,一条是小路,一条是其他未知的路,我们以为大路(PSLLDQ)最快,于是选择先走了大路,结果发现直接走是过不去的;转而选择小路(PSLLQ),走小路,结果发现有个陷阱,这个陷阱让我们到不了目的地,只能达到1/4;然后再回过头来看看大路,大路其实可以过去的,但是踩下去以后全是泥潭(只支持立即数的128位 byte 位移),要走过去,很艰难。那么我们只能选择第三条未知的路了(各种其他指令的组合模拟实现)。
Intel 的 MMX, SSE 各种缺失的指令由来已久,指令的设计也是混乱不勘,还有一个比较著名的就是只实现了POR, PAND, PANDN(and not),没有实现 PNOT (即对MMX, SSE寄存器取反),虽然 PNOT 的确可以用 PANDN 实现(你至少需要2个寄存器),或者用 PCMPEQB xmm0, xmm0 来实现 全置 1 的操作,但是有可能增加了寄存器的占用,可能会增加指令周期,反正是各种不好,虽然影响不算大,但是有时候寄存器捉襟见肘的时候,还是非常蛋疼的。
还有一点更可笑的是,我跟你说了,你一定会相信 Intel 是荒唐的,我们的确需要的是无符号的逻辑左/右移,但是如果你要实现的是有符号的右移(算术右移),
可以使用 PSRAW/PSRAD 指令 - 压缩算术右移,另外说一下,不存在算术左移,因为算术左移逻辑左移是一回事,可参考:
http://moeto.comoj.com/project/intel/instruct32_hh/vc257.htm
非常可笑的是,在这里,Intel 却实现了对于 128 bit 寄存器最多 31 位的位移,更可笑的,对于 64 bit 寄存器最多的位移数也是 31(见上面的链接),这你能懂???
到底是我们的智商有问题,还是 Intel 的智商有问题?!!
看下图:

PS:纠正一下,上面这个举例是错误的(是我看错了),Intel 并没有实现 PSRAQ,上面这个是 PSRAD 的,是针对 DWORD 的,而不是 QWORD,所以他这么实现是正确的,这里并没有问题。
这个问题,在另外一个著名的帖子里也有提到:
千分求汇编优化:UInt96x96To192(...)

到目前为止,都未实现 PSRAQ 指令。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
描述
快速回复

您目前还是游客,请 登录注册
如果您提交过一次失败了,可以用”恢复数据”来恢复帖子内容