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

  • 22304阅读
  • 14回复

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

级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
SSE是很常见的一个X86平台的指令集,早在P4时代就已经出现了。后来INTEL又接连着推出了SSE2, SSE3,SSE4等(不过可没有SSE5,原本规划是有的,后来INTEL独立发展了新一代AVX指令集旨在取代SSE,关于AV X现在资料还不是很多,用的也没有SSE普遍。毕竟支持AVX的CPU也不多,像我的T4400就不支持)。
废话不多说,还是来点实在的东西。大家都知道浮点数运算比起整数运算,速度的确是非常缓慢,很多领域比如图像处理中,需要大量用到浮点数运算,此时CPU就是一个很显著的瓶颈,为了提高浮点数性能,我们有两个方法:
1,化浮点为整形:即尽量通过某种数学变换将原来的浮点数运算变成整数运算。
2,使用SSE这类指令集:显然这种方法是本文重点,不过方法1也会一并用起。
以一个很常见的图像彩色转灰度为例。
根据色彩学上的一些理论,将一个RGB彩色像素转换成灰度,实际上是一个1*3矩阵和一个3*1矩阵相乘,说白了就是如下过程:
设原像素为p0 = (r0,g0,b0),转换为s=(r0*0.3,g0*0.6,b0*0.1),然后新的灰度像素p1=(s,s,s)。
这里可以看到,求得s值这一步中,有三次浮点运算,我们可以用方法1将这里的浮点运算暂时化为整数(全部乘以10),即
s=(r0*3,g0*6,b0*1),最后一次性除以10。
具体代码如下:
void doProcess(PBYTE pIn, DWORD size, DWORD width, DWORD height, DWORD bitCount)
{
  DWORD dwRGBSum = 0;
  for(DWORD dwIndex = 0; dwIndex < size; dwIndex+=3)
  {
    dwRGBSum =    
      1 * pIn[dwIndex+0] +     //Blue  
      6 * pIn[dwIndex+1] +     //Green  
      3 * pIn[dwIndex+2];        //Red  
    dwRGBSum /= 10.0;
    pIn[dwIndex+0] = dwRGBSum;
    pIn[dwIndex+1] = dwRGBSum;
    pIn[dwIndex+2] = dwRGBSum;
  }
}
现在我们再来使用SSE来进一步优化。
SSE一次性可以处理128位的运算,即4个浮点数。因而我们将四次除法放在一次进行,核心的一个数据结构是__m128,这是一个联合体,具体参见其源码。


SSE中批量浮点数乘法对应的C函数是_mm_mul_ps。用法可以参考MSDN或者INTEL官方网站上的一个PDF。


void doProcess(PBYTE pIn, DWORD size, DWORD width, DWORD height, DWORD bitCount)
{
  UINT16 dwRGBSum0 = 0;
  UINT16 dwRGBSum1 = 0;
  UINT16 dwRGBSum2 = 0;
  UINT16 dwRGBSum3 = 0;


  for(DWORD idx = 0; idx < size; idx+=12)
  {
    dwRGBSum0 =    
      1 * pIn[idx+0] +     //Blue
      6 * pIn[idx+1] +     //Green
      3 * pIn[idx+2];        //Red


    dwRGBSum1 =    
      1 * pIn[idx+3] +     //Blue
      6 * pIn[idx+4] +     //Green
      3 * pIn[idx+5];        //Red


    dwRGBSum2 =    
      1 * pIn[idx+6] +     //Blue
      6 * pIn[idx+7] +     //Green
      3 * pIn[idx+8];        //Red


    dwRGBSum3 =    
      1 * pIn[idx+9] +     //Blue
      6 * pIn[idx+10] +     //Green
      3 * pIn[idx+11];        //Red




    __m128 old = _mm_set_ps(dwRGBSum0, dwRGBSum1, dwRGBSum2, dwRGBSum3);
    __m128 ret = _mm_mul_ps(old, vec);


    pIn[idx+0] = pIn[idx+1] = pIn[idx+2] = (BYTE)ret.m128_f32[3];
    pIn[idx+3] = pIn[idx+4] = pIn[idx+5] = (BYTE)ret.m128_f32[2];
    pIn[idx+6] = pIn[idx+7] = pIn[idx+8] = (BYTE)ret.m128_f32[1];
    pIn[idx+9] = pIn[idx+10] = pIn[idx+11] = (BYTE)ret.m128_f32[0];
  }
}
代码看上去要比原来的复杂许多,不过其实原理很简单的,原来一次性处理一个像素,现在一次性处理4个,性能和效率大大提升了。


这个代码其实还可以优化的,因为SSE内除了浮点可以批量处理,整数也是可以的,对应的数据结构是__m128i。涉及到一些矩阵的知识,我就不多说了。


上面这个优化性能测试结果还是很明显的,原来的程序对一个2560*1600,24位色深的图片进行转换,需要将近800ms的时间,优化后,只需450ms了,提高了将近一倍。
关键词: intel 性能 编程
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 沙发  发表于: 2011-11-25
SSE介绍
SSE(Streaming SIMD Extensions)是英特尔在AMD的3D Now!发布一年之后,在其计算机芯片Pentium III中引入的指令集,是继MMX的扩充指令集。SSE 指令集提供了 70 条新指令。AMD后来在Athlon XP中加入了对这个新指令集的支持。
目录  [隐藏]
1 SSE 的暂存器
2 SSE 指令表
2.1 浮点指令
2.2 整数指令
2.3 其他指令
3 后续版本
3.1 SSE2
3.2 SSE3
3.3 SSSE3
3.4 SSE4
3.5 SSE5
3.6 AVX
3.7 FMA
4 参见
[编辑]SSE 的暂存器




SSE 加入新的 8 个 128 位暂存器(XMM0~XMM7)。而 AMD 发表的 x86-64 延伸架构《又称 AMD64》再加入额外 8 个暂存器。除此之外还有一个新的 32 位的控制/状态暂存器(MXCSR)。不过只能在 64 位的模式下才能使用额外 8 个暂存器。
每个暂存器可以容纳 4 个 32 位单精度浮点数,或是 2 个 64 位双精度浮点数,或是 4 个 32 位整数,或是 8 个 16 位短整数,或是 16 个字符。整数运算能够使用正负号运算。而整数 SIMD 运算可能仍然要与 8 个 64 位 MMX 暂存器一起运行。
因为操作系统必须要在进程切换的时候保护这些 128 位的暂存器状态,除非操作系统去启动这些暂存器,否则默认值是不会去激活的。这表示操作系统必须要知道如何使用 FXSAVE 与 FXRSTOR 指令才能存储 x87 与 SSE 暂存器的状态。而在当时 IA-32 的主流操作系统很快的都加入了此功能。
由于 SSE 加入了浮点支持,SSE 就比 MMX 更加常用。而 SSE2 加入了整数运算支持之后让 SSE 更加的有弹性,当 MMX 变成是多余的指令集,SSE 指令集甚至可以与 MMX 并发运作,在某些时候可以提供额外的性能增进。
第一个支持 SSE 的 CPU 是 Pentium III,在 FPU 与 SSE 之间共用运行支持。当编译出来的软件能够交叉的同时以 FPU 与 SSE 运作,Pentium III 并无法在同一个周期中同时运行 FPU 与 SSE。这个限制降低了指令管线的有效性,不过 XMM 暂存器能够让 SIMD 与标量浮点运算混合运行,而不会因为切换 MMX/浮点模式而产生性能的折损。
[编辑]SSE 指令表


SSE 提供标量与包裹式(packed)浮点指令。
[编辑]浮点指令
存储器到暂存器/暂存器到存储器/暂存器之间的数据搬移
标量 – MOVSS
包裹式 – MOVAPS, MOVUPS, MOVLPS, MOVHPS, MOVLHPS, MOVHLPS
数学运算
标量 – ADDSS, SUBSS, MULSS, DIVSS, RCPSS, SQRTSS, MAXSS, MINSS, RSQRTSS
包裹式 – ADDPS, SUBPS, MULPS, DIVPS, RCPPS, SQRTPS, MAXPS, MINPS, RSQRTPS
比较
标量 – CMPSS, COMISS, UCOMISS
包裹式 – CMPPS
数据拆包(unpack)与随机搬移(shuffle)
包裹式 – SHUFPS, UNPCKHPS, UNPCKLPS
数据型态转换
标量 – CVTSI2SS, CVTSS2SI, CVTTSS2SI
包裹式 – CVTPI2PS, CVTPS2PI, CVTTPS2PI
逐位逻辑运算
包裹式 – ANDPS, ORPS, XORPS, ANDNPS
[编辑]整数指令
数学运算
PMULHUW, PSADBW, PAVGB, PAVGW, PMAXUB, PMINUB, PMAXSW, PMINSW
数据搬移
PEXTRW, PINSRW
其他
PMOVMSKB, PSHUFW
[编辑]其他指令
MXCSR 管理
LDMXCSR, STMXCSR
高速缓存与存储器管理
MOVNTQ, MOVNTPS, MASKMOVQ, PREFETCH0, PREFETCH1, PREFETCH2, PREFETCHNTA, SFENCE
[编辑]后续版本


[编辑]SSE2
SSE2是Intel在Pentium 4处理器的最初版本中引入的,但是AMD后来在Opteron 和Athlon 64处理器中也加入了SSE2的支持。SSE2指令集添加了对64位双精度浮点数的支持,以及对整型数据的支持,也就是说这个指令集中所有的MMX指令都是多余的了,同时也避免了占用浮点数寄存器。这个指令集还增加了对CPU高速缓存的控制指令。AMD对它的扩展增加了8个XMM寄存器,但是需要切换到64位模式(x86-64/AMD64)才可以使用这些寄存器。Intel后来在其Intel 64架构中也增加了对x86-64的支持。
[编辑]SSE3
SSE3是Intel在Pentium 4处理器的 Prescott 核心中引入的第三代SIMD指令集,AMD在Athlon 64的第五个版本,Venice核心中也加入了SSE3的支持。这个指令集扩展的指令包含寄存器的局部位之间的运算,例如高位和低位之间的加减运算;浮点数到整数的转换,以及对超线程技术的支持。
[编辑]SSSE3
SSSE3是Intel针对SSE3指令集的一次额外扩充,最早自带于Core 2 Duo处理器中。
[编辑]SSE4
SSE4是Intel在Penryn核心的Core 2 Duo与Core 2 Solo处理器时,新增的47条新多媒体指令集,并且现在更新至SSE4.2。AMD也开发了属于自己的SSE4a多媒体指令集,并自带在Phenom与Opteron等K10架构处理器中,不过无法与Intel的SSE4系列指令集兼容。
[编辑]SSE5
SSE5是AMD为了打破Intel垄断在处理器指令集的独霸地位所提出的,SSE5初期规划将加入超过100条新指令,其中最引人注目的就是三运算对象指令(3-Operand Instructions)及熔合乘法累积(Fused Multiply Accumulate)。其中,三运算对象指令让处理器可将一个数学或逻辑库,套用到运算对象或输入数据。借由增加运算对象的数量,一个 x86 指令能处理二至三笔数据, SSE5 允许将多个简单指令汇整成一个指令,达到更有效率的指令处理模式。提升为三运算指令的运算能力,是少数 RISC 架构的水平。熔合乘法累积让允许创建新的指令,有效率地运行各种复杂的运算。熔合乘法累积可结合乘法与加法运算,通过单一指令运行多笔重复计算。通过简化代码,让系统能迅速运行绘图着色、快速相片着色、音场音效,以及复杂矢量演算等性能密集的应用作业。目前AMD已放弃下一代Bulldozer核心自带SSE5指令集,改自带Intel授权SSE4系列指令集。
[编辑]AVX
AVX(Advanced Vector Extensions) 是Intel的SSE延伸架构,如IA16至IA32般的把暂存器XMM 128bit提升至YMM 256bit,以增加一倍的运算效率。此架构支持了三运算指令(3-Operand Instructions),减少在编码上需要先复制才能运算的动作。在微码部分使用了LES LDS这两少用的指令作为延伸指令Prefix。
[编辑]FMA
FMA是Intel的AVX扩充指令集,如名称上熔合乘法累积(Fused Multiply Accumulate)的意思一样。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 板凳  发表于: 2011-11-25
SSE指令集
SSE和SSE2的指令系统非常相似,SSE2比SSE多的仅是少量的额外浮点处理功能、64位浮点数运算支持和64位整数运算支持。
    SSE为什么会比传统的浮点运算更快呢?因为它使用了128位的存储单元,这对于32位的浮点数来讲,是可以存下4个的,也就是说,SSE中的所有计算都是一次性针对4个浮点数来完成的。
    虽然SSE从理论上来讲要比传统的浮点运算会快,但是所受的限制也很多,首先,虽然它执行一次相当于四次,会比传统的浮点运算执行4次的速度要快,但是它执行一次的速度却并没有想象中的那么快,所以要体现SSE的速度,必须有Stream做前提,就是大量的流数据,这样才能发挥SIMD的强大作用。其次,SSE支持的数据类型是4个32位(共计128位)浮点数集合,就是C、C++语言中的float[4],并且必须是以16位字节边界对齐的。因此这也给输入和输出带来了不少的麻烦,实际上主要影响SSE发挥性能的就是不停地对数据进行复制以适用应它的数据格式。
    如果你是一个C++程序员,对汇编并不很熟,但又想用SSE来优化程序,该怎么做呢?幸好VC++为我们提供了很方便的指令C函数级的封装和C格式数据类型,我们只需像平时写C++代码一样定义变量、调用函数就可以很好地应用SSE指令了。
    当然了,我们需要包含一个头文件,这里面包括了我们需要的数据类型和函数的声明:
#include <xmmintrin.h>
SSE运算的标准数据类型只有一个,就是:__m128,它是这样定义的:
typedef struct __declspec(intrin_type) __declspec(align(16)) __m128
{
   float m128_f32[4];
} __m128;
简化一下,就是:
   struct __m128
{
   float m128_f32[4];
};
比如要定义一个__m128变量,并为它赋四个float整数,可以这样写:
__m128 S1 = { 1.0f, 2.0f, 3,0f, 4,0f };
要改变其中第2个(基数为0)元素时可以这样写:
S1.m128_f32[2] = 6.0f;
令外我们还会用到几个赋值的指令,它可以让我们更方便的使用这个数据结构:
S1 = _mm_set_ps1( 2.0f );
它会让S1.m128_f32中的四个元素全部赋予2.0f,这样会比你一个一个赋值要快的多。
S1 = _mm_setzero_ps();
这会让S1中的所有4个浮点数都置零。
还有一些其它的赋值指令,但执行起来还没有自己逐个赋值来的快,只作为一些特殊用途,如果想了解更多的信息,可以参考MSDN -> Visual Studio -> Reference -> C/C++Language -> Compiler Intrinsics -> MMX, SSE, and SSE2 Intrinsics -> Stream SIMD Extensions(SSE)章节。
    一般来讲,所有SSE指令函数都有3个部分组成,中间用下划线隔开:
  _mm_set_ps1
mm表示多媒体扩展指令集
set表示此函数的含义缩写
ps1表示该函数对结果变量的影响,由两个字母组成,第一个字母表示对结果变量的影响方式,p表示把结果作为指向一组数据的指针,每一个元素都将参与运算,S表示只将结果变量中的第一个元素参与运算;第二个字母表示参与运算的数据类型。s表示32位浮点数,d表示64位浮点数,i32表示32位定点数,i64表示64位定点数。
接下来举一个例子来说明SSE的指令函数是如何使用的,必须要说明的是我以下的代码都是在VC7.1的平台上写的,不保证对其它如Dev-C++、Borland C++等开发平台的完全兼容。
         为了方便对比速度,会用常规方法和SSE优化两种写法写出,并会用一个测试速度的类CTimer来进行计时。
这个算法是对一组float值进行放大,函数ScaleValue1是使用SSE指令优化的,函数ScaleValue2则没有。我们用10000个元素的float数组数据来测试这两个算法,每个算法运算10000遍,下面是测试程序和结果:
#include <xmmintrin.h>
#include <windows.h>
class CTimer
{
public:
       __forceinline CTimer( void )
       {
              QueryPerformanceFrequency( &m_Frequency );
              QueryPerformanceCounter( &m_StartCount );
       }
       __forceinline void Reset( void )
       {
              QueryPerformanceCounter( &m_StartCount );
       }
       __forceinline double End( void )
       {
              static __int64 nCurCount;
              QueryPerformanceCounter( (PLARGE_INTEGER)&nCurCount );
              return double( nCurCount * ( *(__int64*)&m_StartCount ) ) / double( *(__int64*)&m_Frequency );
       }
private:
       LARGE_INTEGER m_Frequency;
       LARGE_INTEGER m_StartCount;
};
void ScaleValue1( float *pArray, DWORD dwCount, float fScale )
{
       DWORD dwGroupCount = dwCount / 4;
       __m128 e_Scale = _mm_set_ps1( fScale );
       for ( DWORD i = 0; i < dwGroupCount; i++ )
       {
              *(__m128*)( pArray + i * 4 ) = _mm_mul_ps( *(__m128*)( pArray + i * 4 ), e_Scale );
       }
}
void ScaleValue2( float *pArray, DWORD dwCount, float fScale )
{
       for ( DWORD i = 0; i < dwCount; i++ )
       {
              pArray *= fScale;
       }
}
#define ARRAYCOUNT 10000
int __cdecl main()
{
       float __declspec(align(16)) Array[ARRAYCOUNT];
       memset( Array, 0, sizeof(float) * ARRAYCOUNT );
       CTimer t;
       double dTime;
       t.Reset();
   for ( int i = 0; i < 100000; i++ )
       {
              ScaleValue1( Array, ARRAYCOUNT, 1000.0f );
       }
       dTime = t.End();
       cout << "Use SSE:" << dTime << "秒" << endl;
       t.Reset();
       for ( int i = 0; i < 100000; i++ )
       {
              ScaleValue2( Array, ARRAYCOUNT, 1000.0f );
       }
       dTime = t.End();
       cout << "Not Use SSE:" << dTime << "秒" << endl;
       system( "pause" );
       return 0;
}
Use SSE:0.997817
Not Use SSE:2.84963
         这里要注意一下,此处使用了__declspec(align(16))作为数组定义的修释符,这表示该数组是以16字节为边界对齐的,因为SSE指令只能支持这种格式的内存数据。
  • SSE
    • CVTSI2SS – 把一个64位的有符号整型转换为一个浮点值,并把它插入到一个128位的参数中。内部指令:_mm_cvtsi64_ss
    • CVTSS2SI – 取出一个32位的浮点值,并取整(四舍五入)为一个64位的整型。内部指令:_mm_cvtss_si64
    • CVTTSS2SI – 取出一个32位的浮点值,并截断为一个64位的整型。内部指令:_mm_cvttss_si64
  • SSE2
    • CVTSD2SI – 取出最低位的64位浮点值,并取整为一个整型。内部指令:_mm_cvtsd_si64
    • CVTSI2SD – 取出最低位的64位整型,并将其转换为一个浮点值。内部指令:_mm_cvtsi64_sd
    • CVTTSD2SI – 取出一个64位的浮点值,并截断为一个64位的整型。内部指令:_mm_cvttsd_si64
    • MOVNTI – 写64位数据到特定内存位置。内部指令:_mm_stream_si64
    • MOVQ – 移动一个64位的整型到一个128位的参数中,或从128位的参数中移动一个64位的整型。内部指令:_mm_cvtsi64_si128、_mm_cvtsi128_si64
  • SSSE3
    • PABSB / PABSW / PABSD – 取有符号整型的绝对值。内部指令:_mm_abs_epi8、_mm_abs_epi16、_mm_abs_epi32、_mm_abs_pi8、_mm_abs_pi16、_mm_abs_pi32
    • PALIGNR – 结合两个参数并右移结果。内部指令:_mm_alignr_epi8、_mm_alignr_pi8
    • PHADDSW – 将两个包含16位有符号整型的参数相加,并尽量使结果为16位可表示的最大值。内部指令:_mm_hadds_epi16、_mm_hadds_pi16
    • PHADDW / PHADDD – 将两个包含有符号整型的参数相加。内部指令:_mm_hadd_epi16、_mm_hadd_epi32、_mm_hadd_pi16、_mm_hadd_pi32
    • PHSUBSW – 将两个包含16位有符号整型的参数相减,并尽量使结果为16位可表示的最大值。内部指令:_mm_hsubs_epi16、_mm_shubs_pi16
    • PHSUBW / PHSUBD – 将两个包含有符号整型的参数相减。内部指令:_mm_hsub_epi16、_mm_hsub_epi32、_mm_hsub_pi16、_mm_hsub_pi32
    • PMADDUBSW – 相乘并相加8位整型。内部指令:_mm_maddubs_epi16、_mm_maddubs_pi16
    • PMULHRSW – 乘以16位有符号整型,并右移结果。内部指令:_mm_mulhrs_epi16、_mm_mulhrs_pi16
    • PSHUFB – 从一个128位的参数中选取并乱序其中8位的数据块。内部指令:_mm_shuffle_epi8、_mm_shuffle_pi8
    • PSIGNB / PSIGNW / PSIGND – 求反(取非)、取零、或保留有符号整型。内部指令:_mm_sign_epi8、_mm_sign_epi16、_mm_sign_epi32、_mm_sign_pi8、_mm_sign_pi16、_mm_sign_pi32
  • SSE4A
    • EXTRQ – 从参数中取特定位。内部指令:_mm_extract_si64、_mm_extracti_si64
    • INSERTQ – 插入特定位到给定参数中。内部指令:_mm_insert_si64、_mm_inserti_si64
    • MOVNTSD / MOVNTSS – 不使用缓存,直接把数据位写到特定内存位置。内部指令:_mm_stream_sd、_mm_stream_ss
  • SSE4.1
    • DPPD / DPPS – 计算两参数的点结果。内部指令:_mm_dp_pd、_mm_dp_ps
    • EXTRACTPS – 从参数中取出一个特定的32位浮点值。内部指令:_mm_extract_ps
    • INSERTPS – 把一个32位整型插入到一个128位参数中,并把某些位置零。内部指令:_mm_insert_ps
    • MOVNTDQA – 从特定内存位置加载128位数据。内部指令:_mm_stream_load_si128
    • MPSADBW – 计算绝对差分的八个偏移总和。内部指令:_mm_mpsadbw_epu8
    • PACKUSDW – 使用16位饱和度,把32位有符号整型转换为有符号16位整型。内部指令:_mm_packus_epi32
    • PBLENDW / BLENDPD / BLENDPS / PBLENDVB / BLENDVPD / BLENDVPS – 把两个不同块大小的参数混合在一起。内部指令:_mm_blend_epi16、_mm_blend_pd、_mm_blend_ps、_mm_blendv_epi8、_mm_blendv_pd、_mm_blendv_ps
    • PCMPEQQ - 比较64位整型是否相等。内部指令:_mm_cmpeq_epi64
    • PEXTRB / PEXTRW / PEXTRD / PEXTRQ - 从输入的参数中取出一个整型。内部指令:_mm_extract_epi8、_mm_extract_epi16、_mm_extract_epi32、_mm_extract_epi64
    • PHMINPOSUW - 选择最小的16位无符号整型并确定它的下标。内部指令:_mm_minpos_epu16
    • PINSRB / PINSRD / PINSRQ - 把一个整型插入到一个128位参数中。内部指令:_mm_insert_epi8、_mm_insert_epi32、_mm_insert_epi64
    • PMAXSB / PMAXSD - 接受两个参数中的有符号整型,并选择其中的最大者。内部指令:_mm_max_epi8、_mm_max_epi32
    • PMAXUW / PMAXUD - 接受两个参数中的无符号整型,并选择其中的最大者。内部指令:_mm_max_epu16、_mm_max_epu32
    • PMINSB / PMINSD - 接受两个参数中的有符号整型,并选择其中的最小者。内部指令:_mm_min_epi8、_mm_min_epi32
    • PMINUW / PMINUD - 接受两个参数中的无符号整型,并选择其中的最小者。内部指令:_mm_min_epu16、_mm_min_epu32
    • PMOVSXBW / PMOVSXBD / PMOVSXBQ / PMOVSXWD / PMOVSXWQ / PMOVSXDQ - 把一有符号整型转换到更大的尺寸。内部指令:_mm_cvtepi8_epi16、_mm_cvtepi8_epi32、_mm_cvtepi8_epi64、_mm_cvtepi16_epi32、_mm_cvtepi16_epi64、_mm_cvtepi32_epi64
    • PMOVZXBW / PMOVZXBD / PMOVZXBQ / PMOVZXWD / PMOVZXWQ / PMOVZXDQ - 把一无符号整型转换到更大的尺寸。内部指令:_mm_cvtepu8_epi16、_mm_cvtepu8_epi32、_mm_cvtepu8_epi64、_mm_cvtepu16_epi32、_mm_cvtepu16_epi64、_mm_cvtepu32_epi64
    • PMULDQ - 32位有符号整型相乘,并把结果存储为64位有符号整型。内部指令:_mm_mul_epi32
    • PMULLUD - 32位有符号整型相乘。内部指令:_mm_mullo_epi32
    • PTEST - 按位计算两个128位参数,并基于CC标志寄存器的CF与ZF位返回值。内部指令:_mm_testc_si128、_mm_testnzc_si128、_mm_testz_si128
    • ROUNDPD / ROUNDPS - 取整浮点数值。内部指令:_mm_ceil_pd、_mm_ceil_ps、_mm_floor_pd、_mm_floor_ps、_mm_round_pd、_mm_round_ps
    • ROUNDSD / ROUNDSS - 结合两个参数,从其一取整到一个浮点数值。内部指令:_mm_ceil_sd、_mm_ceil_ss、_mm_floor_sd、_mm_floor_ss、_mm_round_sd、_mm_round_ss
  • SSE4.2
    • CRC32 - 计算参数的CRC-32C检验和。内部指令:_mm_crc32_u8、_mm_crc32_u16、_mm_crc32_u32、_mm_crc32_u64
    • PCMPESTRI / PCMPESTRM -比较特定长度的两个参数。内部指令:_mm_cmpestra、_mm_cmpestrc、_mm_cmpestri、_mm_cmpestrm、_mm_cmpestro、_mm_cmpestrs、_mm_cmpestrz
    • PCMPGTQ - 比较两个参数。内部指令:_mm_cmpgt_epi64
    • PCMPISTRI / PCMPISTRM - 比较两个参数。内部指令:_mm_cmpistra、_mm_cmpistrc、_mm_cmpistri、_mm_cmpistrm、_mm_cmpistro、_mm_cmpistrs、_mm_cmpistrz
    • POPCNT - 统计位集中1的数量。内部指令:_mm_popcnt_u32、_mm_popcnt_u64、__popcnt16、__popcnt、__popcnt64
  • 高级位操纵
    • LZCNT - 统计参数中零的数量。内部指令:__lzcnt16、 __lzcnt、__lzcnt64
    • POPCNT - 统计位集中1的数量。内部指令:_mm_popcnt_u32、_mm_popcnt_u64、__popcnt16、__popcnt、__popcnt64
  • 其他新指令
    • _InterlockedCompareExchange128 - 对比两个参数。
    • _mm_castpd_ps / _mm_castpd_si128 / _mm_castps_pd / _mm_castps_si128 / _mm_castsi128_pd / _mm_castsi128_ps - 对32位浮点值(ps)、64位浮点值(pd)及32位整型值(si128)重新解释。
    • _mm_cvtsd_f64 - 从参数中取出最低的64位浮点值。
    • _mm_cvtss_f32 - 取出一个32位的浮点值。
    • _rdtscp - 生成RDTSCP。把TSC AUX[31:0]写到内存,并返回64位时间戳计数器结果。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 地板  发表于: 2011-11-25
INTEL 指令集
SSE(Streaming SIMD Extensions)  历经4代
SSE指令集包括了70条指令,其中包含提高3D图形运算效率的50条SIMD(单指令多数据技术)浮点运算指令、12条MMX 整数运算增强指令、8条优化内存中连续数据块传输指令。理论上这些指令对目前流行的图像处理、浮点运算、3D运算、视频处理、音频处理等诸多多媒体应用起到全面强化的作用。SSE指令与3DNow!指令彼此互不兼容,但SSE包含了3DNow!(AMD)技术的绝大部分功能,只是实现的方法不同。SSE兼容MMX指令,它可以通过SIMD和单时钟周期并行处理多个浮点数据来有效地提高浮点运算速度。
MMX只能在整数上支持SIMD,而SSE指令增加了单精度浮点数的SIMD支持.MMX可以进行同时对2个32位的整数操作,而SSE可以同时对4个32位的浮点数操作。MMX和SSE的一个主要的区别是MMX并没有定义新的寄存器,而SSE定义了8个全新的128位寄存器,每个寄存器可以同时存放4个单精度浮点数(每个32位长),他们在寄存器中排列顺序见下图。
MMX是和原来的浮点寄存器共享的.一个浮点寄存器是80位长的,她的低端64位被用做MMX的寄存器.这样,一个应用程序就不能在执行MMX指令的同时进行浮点操作了.同时,处理器还要花掉大量的时钟周期去维护寄存器状态从MMX操作和浮点操作之间的切换。SSE指令集就没有这些限制了。由于她定义了全新的寄存器,应用程序可以在进行整数SIMD操作(MMX)的同时进行浮点数的SIMD操作(SSE),同样,SSE还可以在执行浮点数的非SIMD操作的同时进行SIMD操作。
SSE和SSE2的指令系统非常相似,SSE2比SSE多的仅是少量的额外浮点处理功能、64位浮点数运算支持和64位整数运算支持。
    SSE为什么会比传统的浮点运算更快呢?因为它使用了128位的存储单元,这对于32位的浮点数来讲,是可以存下4个的,也就是说,SSE中的所有计算都是一次性针对4个浮点数来完成的。
    虽然SSE从理论上来讲要比传统的浮点运算会快,但是所受的限制也很多,首先,虽然它执行一次相当于四次,会比传统的浮点运算执行4次的速度要快,但是它执行一次的速度却并没有想象中的那么快,所以要体现SSE的速度,必须有Stream做前提,就是大量的流数据,这样才能发挥SIMD的强大作用。其次,SSE支持的数据类型是4个32位(共计128位)浮点数集合,就是C、C++语言中的float[4],并且必须是以16位字节边界对齐的。(16*8)因此这也给输入和输出带来了不少的麻烦,实际上主要影响SSE发挥性能的就是不停地对数据进行复制以适用应它的数据格式.
SSE指令可以说是将Intel的MMX和AMD的3DNow!技朮相结合的产物,由于3DNow!使用的是浮点寄存方式,因而无法较好地同步进行正常的浮点运算。而SSE使用了分离的指令寄存器,从而可以全速运行,保证了与浮点运算的并行性。尤其是两者所使用的寄存器差异颇大─3DNow!是64位,而SSE是128位。同时为了充分发挥SSE的优势,Intel引进了新的“处理器分离模式”以提高浮点运算速度。
比如玩Quake时,3D对象均由多边形构成,而这些多边形以一系列点的形式保存。每个点都有对应的3轴坐标。如限制成只用整数,便不能精确地表示这些位置(如每个坐标轴使用16位,那么只能得到65536(2~16)个坐标点),造成图形显示非常糟糕。
要发挥SSE的速度优势,要满足以下条件:使用DirectX 6 API开发应用程序;使用支持SSE的图形驱动程序;使用支持“处理器分离模式”的操作系统等。由于SSE要求软件的数据结构重写,又比“3D NOW!”晚推出了9个多月,因此市场上使用“3D NOW!”的系统装机量远远超过SSE。基于以上种种原因,SSE一直没有得到充分的发展。直到Pentium 4发布之后,开发人员看到使用SSE指令之后,程序执行性能将得到极大的提升,于是Intel又在SSE的基础上推出了更先进的SSE2指令集。
4个单精确浮点数(SSE)·2个双倍精确浮点数(SSE2)·16个字节数(SSE2)·8个字组(word)数(SSE2)
·4个双倍字组数(SSE2)·2个四倍字组数(SSE2)·1个128位长的整数(SSE2)
  由于SSE2可供选择的数据型态很多,而且无疑地相当有用。 因此,当时Intel 极希望软件开发者能用SSE2 的双倍精确浮点指令来取代旧有的 x86 浮点指令,如此一来 Intel所称 Pentium4是当时FPU 性能最强的处理器这项谣言,最后也就变成了事实。在相关测试中,SSE2对于处理器的性能的提升是十分明显的,虽然在同频率的情况下,Pentium 4和性能不如Athlon XP,但由于Athlon XP不支持SSE2,所以经过SSE2优化后的程序Pentium 4的运行速度要明显高于Athlon XP。而AMD方面也注意到了这一情况,在随后的K-8系列处理器中,都加入SSE2指令集。
SSE3:
此前MMX包含有57条命令,SSE包含有50条命令,SSE2包含有144条命令,SSE3包含有13条命令
第一层中的指令是“数据传输命令”,只有一条指令:FISTTP,它有利于x87浮点转换成整数,并可以大大提高优化的效率。

  第二层中的指令是“数据处理命令”,一共有五条,分别是ADDSUBPS,ADDSUBPD,MOVSHDUP,MOVSLDUP,MOVDDUP。这些指令可以简化复杂数据的处理过程,由于未来数据处理流量将会越来越大,因此Intel在这里应用的指令集最多、达到了五条。

  第三层中的指令是“特殊处理命令”,也只有一条:LDDQU。在这条指令主要针对视频解码,用来提高处理器对处理媒体数据结果的精确性。

  第四层中的指令是“优化命令”,一共有四条指令,分别是HADDPS,HSUBPS,HADDPD,HSUBPD,它们可以对程序起到自动优化的作用。这些指令对处理3D图形相当有用。

  第五层中的指令是“超线程性能增强”,一共有两条针对线程处理的指令:MONITOR, MWAIT,这有助于增加Intel超线程的处理能力、大大简化了超线程的数据处理过程。

  从技术上来看,SSE3对于SEE2的改进非常有限,因此它为Prescott所带来的性能提升相当有限,它的优势仍体现在视频解码方面—Intel宣称如果在数据编码算法使用LDDQU指令,那么影象压缩速度可以提升10%左右。在当时来看,SSE3仍属于最先进的指令集,因此AMD、全美达后来也在它的处理器中加入对SSE3指令集的支持。
配合完整的128位SSE执行单元,以及庞大的执行单元数目,Conroe处理器可在一个频率周期内,同时执行128位乘法、128位加法、128位数据加载与128位数据回存,或着是4个32位单倍浮点精确度乘法与4个32位单倍浮点精确度加法运算,这将使其更利于多媒体应用。
   1997年 MMX使用了8个64位的寄存器------8个字节、4个单字、2个双字(风云的“游戏之旅,我的编程感悟”)
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 4楼 发表于: 2011-11-25
amd cpu对intel sse指令集的支持情况
首先测试下sse4.1,源代码如下


view plain
#include <stdio.h>  
#include <smmintrin.h>  
int main()  
{  
__m128i a, b;  
char * psA = (char * )&a;  
char * psB = (char * )&b;  
int i = 0 ;  
for( ;   i < 16; ++i)  
{  
psA = 0xcc; psB = 0x33;  
}  
int sResult = _mm_testz_si128(a,b);  
printf("sResult == %d/n",sResult);  
}  



编译方法


g++ -O2 -o1 -s -msse4.1 1.cpp


gdb报sigill,非法指令集,不支持sse4.1


接着测试sse3





view plain
#include <stdio.h>  
#include <pmmintrin.h>  
int main(){  
__m128 a, b,c;  
float * psA = (float*)&a;  
psA[0] = 0.1f;psA[1]= 0.2f; psA[2]= 0.3f; psA[3]= 0.4f;  
float * psB = (float*)&b;  
psB[0] = 0.001f; psB[1] = 0.002f; psB[2] = 0.003f; psB[3] = 0.004f;  
c = _mm_addsub_ps(a,b);  
float * psC = (float*)&c;  
printf("c[0] = %f,c[1] = %f,c[2] = %f,c[3] = %f", psC[0],psC[1],psC[2],psC[3]);  
}  



结果输出:


c[0] = 0.099000,c[1] = 0.202000,c[2] = 0.297000,c[3] = 0.404000


支持sse3


至于那个sse4a,那就真是算了,只有AMD支持,没有市场……
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 5楼 发表于: 2011-11-25
SSE指令在C、C++中的应用
SSE是英特尔提出的即MMX之后新一代(当然是几年前了)CPU指令集,最早应用在PIII系列CPU上。现在已经得到了Intel PIII、P4、Celeon、Xeon、AMD Athlon、duron等系列CPU的支持。而更新的SSE2指令集仅得到了P4系列CPU的支持,这也是为什么这篇文章是讲SSE而不是SSE2的原因之一。另一个原因就是SSE和SSE2的指令系统是非常相似的,SSE2比SSE多的仅是少量的额外浮点处理功能、64位浮点数运算支持和64位整数运算支持。
  SSE为什么会比传统的浮点运算更快呢?因为它使用了128位的存储单元,这对于32位的浮点数来讲,是可以存下4个的,也就是说,SSE中的所有计算都是一次性针对4个浮点数来完成的,这种批处理当然就会带来效率的提升。我们再来回顾一下SSE的全称:Stream SIMD Extentions(流SIMD扩展)。SIMD就是single instruction multiple data,连起来就是“数据流单指令多数据扩展”,从名字我们就可以更好的理解SSE是如何工作的了。
  虽然SSE从理论上来讲要比传统的浮点运算会快,但是他所受的限制也很多,首先,虽然他执行一次相当于四次,会比传统的浮点运算执行4次的速度要快,但是他执行一次的速度却并没有想象中的那么快,所以要体现SSE的速度,必须有Stream做前提,就是大量的流数据,这样才能发挥SIMD的强大作用。其次,SSE支持的数据类型是4个32位(共计128位)浮点数集合,就是C、C++语言中的float[4],并且必须是以16位字节边界对齐的(稍后会以代码来进行阐释,关于边界对齐的概念,读者可以参考论坛上的其它文章,都会有很详细的解答,我这里就恕不赘述了)。因此这也给输入和输出带来了不少的麻烦,实际上主要影响SSE发挥性能的就是不停的对数据进行复制以适用应它的数据格式。
  我是一个C++程序员,对汇编并不很熟,但我又想用SSE来优化我的程序,我该怎么做呢?幸好VC++.net为我们提供了很方便的指令C函数级的封装和C格式数据类型,我们只需像平时写C++代码一样定义变量、调用函数就可以很好的应用SSE指令了。
  当然了,我们需要包含一个头文件,这里面包括了我们需要的数据类型和函数的声明:


#include <xmmintrin.h>

  SSE运算的标准数据类型只有一个,就是:
__m128,它是这样定义的:


typedef struct __declspec(intrin_type) __declspec(align(16)) __m128 {
   float m128_f32[4];
} __m128;

  简化一下,就是:


struct __m128
{
   float m128_f32[4];
};

  比如要定义一个__m128变量,并为它赋四个float整数,可以这样写:


要改变其中第2个(基数为0)元素时可以这样写:


S1.m128_f32[2] = 6.0f;

  令外我们还会用到几个赋值的指令,它可以让我们更方便的使用这个数据结构:


S1 = _mm_set_ps1( 2.0f );

  它会让S1.m128_f32中的四个元素全部赋予2.0f,这样会比你一个一个赋值要快的多。


S1 = _mm_setzero_ps();

  这会让S1中的所有4个浮点数都置零。
  还有一些其它的赋值指令,但执行起来还没有自己逐个赋值来的快,只做为一些特殊用途,如果你想了解更多的信息,可以参考MSDN -> VisualC++参考 -> C/C++Language -> C++Language Reference -> Compiler Intrinsics -> MMX, SSE, and SSE2 Intrinsics -> Stream SIMD Extensions(SSE)章节。
  一般来讲,所有SSE指令函数都有3个部分组成,中间用下划线隔开:


_mm_set_ps1

  mm表示多媒体扩展指令集
  set表示此函数的含义缩写
  ps1表示该函数对结果变量的影响,由两个字母组成,第一个字母表示对结果变量的影响方式,p表示把结果做为指向一组数据的指针,每一个元素都将参与运算,S表示只将结果变量中的第一个元素参与运算;第二个字母表示参与运算的数据类型。s表示32位浮点数,d表示64位浮点数,i32表示32位定点数,i64表示64位定点数,由于SSE只支持32位浮点数的运算,所以你可能会在这些指令封装函数中找不到包含非s修饰符的,但你可以在MMX和SSE2的指令集中去认识它们。
  接下来我举一个例子来说明SSE的指令函数是如何使用的,必须要说明的是我以下的代码都是在VC7.1的平台上写的,不保证对其它如Dev-C++、Borland C++等开发平台的完全兼容。
  为了方便对比速度,我会用常归方法和SSE优化两种写法写出,并会用一个测试速度的类CTimer来进行计时。
  这个算法是对一组float值进行放大,函数ScaleValue1是使用SSE指令优化的,函数ScaleValue2则没有。我们用10000个元素的float数组数据来测试这两个算法,每个算法运算10000遍,下面是测试程序和结果:


#include <xmmintrin.h>
#include <windows.h>









class CTimer
{
public:
       __forceinline CTimer( void )
       {
              QueryPerformanceFrequency( &m_Frequency );
              QueryPerformanceCounter( &m_StartCount );
       }
       __forceinline void Reset( void )
       {
              QueryPerformanceCounter( &m_StartCount );
       }
       __forceinline double End( void )
       {
              static __int64 nCurCount;
              QueryPerformanceCounter( (PLARGE_INTEGER)&nCurCount );
              return double( nCurCount * ( *(__int64*)&m_StartCount ) ) / double( *(__int64*)&m_Frequency );
       }
private:
       LARGE_INTEGER m_Frequency;
       LARGE_INTEGER m_StartCount;
};
void ScaleValue1( float *pArray, DWORD dwCount, float fScale )
{


DWORD dwGroupCount = dwCount / 4;
       __m128 e_Scale = _mm_set_ps1( fScale );
       for ( DWORD i = 0; i < dwGroupCount; i++ )
       {
              *(__m128*)( pArray + i * 4 ) = _mm_mul_ps( *(__m128*)( pArray + i * 4 ), e_Scale );
       }
}
void ScaleValue2( float *pArray, DWORD dwCount, float fScale )
{
       for ( DWORD i = 0; i < dwCount; i++ )
       {
              pArray *= fScale;
       }
}
#define ARRAYCOUNT 10000
int __cdecl main()
{
       float __declspec(align(16)) Array[ARRAYCOUNT];
       memset( Array, 0, sizeof(float) * ARRAYCOUNT );
       CTimer t;
       double dTime;
       t.Reset();





     for ( int i = 0; i < 100000; i++ )
       {
              ScaleValue1( Array, ARRAYCOUNT, 1000.0f );
       }
       dTime = t.End();
       cout << "Use SSE:" << dTime << "秒" << endl;
       t.Reset();
       for ( int i = 0; i < 100000; i++ )
       {
              ScaleValue2( Array, ARRAYCOUNT, 1000.0f );
       }
       dTime = t.End();
       cout << "Not Use SSE:" << dTime << "秒" << endl;
       system( "pause" );
       return 0;
}
Use SSE:0.997817
Not Use SSE:2.84963

  这里要注意一下,我使用了__declspec(align(16))做为数组定义的修释符,这表示该数组且?6字节为边界对齐的,因为SSE指令只能支持这种格式的内存数据。

  我们在这里看到了SSE算法的强大,相信它会成为多媒体程序员手中用来对付无穷尽流媒体数据的一把利剑。


QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 6楼 发表于: 2012-09-30
探讨SSE指令
比较一下3DNow和浮点指令的性能差异,可以看出,3DNow指令集在运算速度上要远远
超过浮点指令。那么,SSE性能如何呢,它是否有能力同3DNow一拚高低?我想,很难说
那一个更好一些,因为它们都有着很高的性能。不过单从指令集上看,SSE还是要略胜一
筹的。毕竟是新增了8个128位的寄存器,而且指令的功能也要强大一些。3DNow使用MMX
指令的寄存器,可以借助MMX指令的强大功能,不必设计太多的新功能,不需要操作系
统提供专门的支持,而且口碑颇佳!从流水线的设计上看,双方也是各有所长。Pentiu
m III每个时钟周期最多可以解码3条指令,执行5个微操作,它把一些重要的微操作(例
如乘法和加法)分派到不同的端口去执行。 3DNow则是在两条流水线间共享3DNow的执行
单元和部分MMX的执行单元,所有的3DNow指令都是有两个时钟周期的延迟,并且完全被
流水线化。
最近,AMD的处理器似乎有了很大的变化,我看过一些有关它的64位处理器的资料,也是
添加了一堆寄存器,不过我没有仔细看,毕竟没有哪个缘份一睹芳颜。intel公司当然也
没有闲着,它的64位处理器则不能用“变化”二字来形容了,那简直可以说是脱胎换骨
,全新的指令,全新的体系!不过,咱们老百姓恐怕不会在短时间内用上这种处理器,
拥有三百多个寄存器的CPU肯定会处于我无法接受的价位。这样也还是有一个好处的,那
就是SSE指令集在短时间内不会过时,毕竟,转移到64位阵营还是要经历一个漫长的过程
。而且,在IA-64体系中专门提供了三条指令在32位代码和64位代码之间进行跳转,也就
是说,你可以在程序中任意使用两种代码。
所以,如果你想针对intel系列的处理器进行优化的话,就努力学好SSE吧,在相当长的
时间里都会大有用处的。
本文不会详细介绍每一个SSE指令,只是讨论一些重要的,常用的,能够对性能产生较大
影响的指令。如果你想更全面的了解SSE,请参阅 SSE指令简明参考。你可以从中查到每
条SSE指令的功能。
通过程序来讨论指令的用法是最好的办法。以前写过的两篇文章,一篇是关于浮点指令
优化的,一篇是关于3DNow指令优化的,这两篇文章都是使用了矩阵相乘作为例子程序,
因此本文还是以矩阵相乘为例,看一看SSE究竟有什么优势!它与单纯使用浮点指令的程
序相比效率能提高多少!
准备工作
选择合适的编译器
目前我还没有发现哪个编译器能够对SSE提供内联支持,据intel声称,它的C++ 编译器
可以做到,但是,恐怕没有几个人用过。建议大家使用MASM6.14,它支持SSE和3DNow。
大家可以从本站下载MASM6.14。高级语言的编译器也是要有的,我使用的是VC6.0。因为
VC在浮点程序方面比 C++ Builder优化的更好,这样就可以与汇编的优化结果进行比较
了。
设置编译器
在汇编程序里,应该加入伪指令来指示编译器支持何种指令集。“.xmm”表示要求编译
器支持SSE指令集。“.k3d”则是要求编译器支持3DNow指令集。
VC提供了一些支持,可以自动的编译汇编文件,你可以按照以下步骤进行:
在菜单中选择“Project | Setting”
选中指定的汇编文件(单击即可)
选中Custom Build页
在Commands中输入:
如果是DEBUG模式,则输入:
path e:\masm32\bin
ml /c /coff /Zi /FoDEBUG\$(InputName).obj $(InputPath)
如果是RELEASE模式,则输入:
path e:\masm32\bin
ml /c /coff /FoRELEASE\$(InputName).obj $(InputPath)
在Outputs中输入:
如果是DEBUG模式,则输入:
DEBUG\$(InputName).obj
如果是RELEASE模式,则输入:
RELEASE\$(InputName).obj
如果你的没有把masm安装在E盘,则要作相应的修改。
学习指令
你首先应该对SSE指令有所了解才能更好的阅读本文。SSE指令集是一个比较新的体系,
如果你没有学过MMX或者3DNow,还是有一定困难的。在全面优化Pentium III一文中对P
entium III 的体系有比较全面的阐述。
优化方针
针对SSE优化还是比较困难的,下面提出一些方法,以供参考:
摆脱高级语言的桎梏,根据硬件的特点,指令的功能,量体裁衣地设计算法。要知道,
汇编语言的算法与高级语言是有很大的不同的,只有重新设计的算法才有可能发挥出处
理器的最大潜力。
熟练使用一些常用的指令,知道它们的延迟和吞吐量是多少。本文的例子中所用的一些
重要的指令有:ADDPS,MULPS,SHUFPS,MOVSS,MOVAPS。关于它们的执行单元的相关数
据可以查阅处理器执行单元列表。
充分利用新增加的八个寄存器,减小内存的压力;设计并行算法,减轻流水线的延迟。


综合考虑解码器,流水线,执行端口等多方面因素,尽量增强处理器的并行处理的能力

举例详解
下面的程序是一个矩阵相乘的函数。在三维图形空间变换中,要用到4乘4的浮点矩阵,
而矩阵相乘的运算是很常用的。下面的函数的参数都是4乘4的浮点矩阵。写成这种形式
是为了保持比较强的伸缩性。
void MatMul_cpp(float *dest, float *m1, float *m2)
{
    for(int i = 0; i < 4; i ++)
    {
        for(int j = 0; j < 4; j ++)
        {
            dest[i*4+j] =
                m1[i*4+0]*m2[0*4+j] +
                m1[i*4+1]*m2[1*4+j] +
                m1[i*4+2]*m2[2*4+j] +
                m1[i*4+3]*m2[3*4+j] ;
        }
    }
}
VC的优化能力是很强的,象上面这样的比较常规的算法,你很难做出比它快得多的代码
。不过使用SSE以后就不一样了。下面是一个汇编函数,使用SSE 指令进行计算。注意,
这个函数只能运行于32位的环境中。“.xmm”指示编译器使用SSE指令集进行编译。
函数的C语言原型是这样的:
extern "C"
{
void __stdcall MatMul_xmm(float *dest, float *m1, float *m2);
}
对于一些不太常用汇编语言编程的朋友来说,下面的程序可能比较难于理解。我将对一
些常识性的东西做一下简单介绍。
在C语言中,代码段都是以“_TEXT”作为段名的。“use32”告诉编译器将代码编译为3
2位。
有些人看到“_MatMul_xmm@12”这个函数名以后可能会产生疑问。其实这只是遵循了VC
所采用的命名规范。在VC中,所有标志为“__stdcall” 调用的,采用“C”链接的函数
都要加下划线作为前缀,并且加上“@N”作为后缀,其中,“N”为参数的字节数。注意
,上面的函数是采用“C”链接的,如果是“C++”链接,命名规范就太复杂了。如果你
使用的是C++ Builder,命名规范就十分简单了,照搬函数名就行了。不同的调用规范将
采用不同的命名方法,即使对相同的调用规范,不同的编译器也不一定兼容。有一种调
用格式是每一个C++编译器都支持并且兼容的,那就是“__cdecl”。
各种调用格式所采用的堆栈操作也不太一样。使用“__stdcall”时,参数从右向左依次
入栈,参数的弹出需要函数自己来处理。这种做法和“__cdecl” 调用方式不太一样,
“__cdecl”的参数弹出需要调用者来处理。现在很流行的一种调用格式是“__fastcal
l”,也就是寄存器调用。这种调用方式通过寄存器“EAX”,“ECX”,“EDX”传递参
数,不过很可惜,这种调用也不是在各个编译器中兼容的。Inprise在C++ Builder中提
供了一个关键字“__msfastcall” 用来和微软兼容,如果你采用这种调用规范就可以在
多个编译器中正常调用了。不过还有一件事让人很受打击,VC没有对“__fastcall”提
供很好的优化,使用这种调用反而会降低效率。
并不是所有的寄存器都能够随意使用的,多数32位寄存器都要先保存的。你可以不必保
存的32位寄存器只有三个----“EAX”,“ECX”,“EDX”,其它的就只好“PUSH”,“
POP”了。另外,浮点堆栈寄存器是不必保存的;MMX 寄存器和浮点堆栈共享,也是不必
保存的;XMM寄存器不必保存。
很多SSE指令都会加上“ps”或“ss”后缀。“ps”表示“Packed Single-FP”,即打包
的浮点数,带这种后缀的指令通常是一次性对四个数进行操作的。“ss” 表示“Scala
r Single-FP”,带这种后缀的指令通常是对最低位的单精度数进行操作的。
下面这个汇编函数是一行一行计算的,咱们先用类似于C的语法简述一下第一行的计算过
程:
    xmm0 = m1[0],m1[0],m1[0],m1[0];
    xmm1 = m1[1],m1[1],m1[1],m1[1];
    xmm2 = m1[2],m1[2],m1[2],m1[2];
    xmm3 = m1[3],m1[3],m1[3],m1[3];
    xmm4 = m2[0],m2[1],m2[2],m2[3];
    xmm5 = m2[4],m2[5],m2[6],m2[7];
    xmm6 = m2[8],m2[9],m2[10],m2[11];
    xmm7 = m2[12],m2[13],m2[14],m2[15];
    xmm0 *= xmm4;
    xmm1 *= xmm5;
    xmm2 *= xmm6;
    xmm3 *= xmm7;
    xmm1 += xmm0;
    xmm2 += xmm1;
    xmm3 += xmm2;
    dst[0],dst[1],dst[2],dst[3] = xmm3;
上面的代码可读性还是比较好的,因为只进行了第一行的计算。实际运算中,为了增强
并行度,为了减小指令的延迟,实际上是两行并行计算的。而且,运算过程并不是象算
法描述那样写得那么有规律。
        .686p
        .xmm
        .model flat
_TEXT segment public use32 'CODE'
public _MatMul_xmm@12
_MatMul_xmm@12 proc
;;parameters
retaddress = 0
dst = retaddress+4
m1 = dst+4
m2 = m1+4
        mov          edx,     [esp+m1]
        mov          ecx,     [esp+m2]
        mov          eax,     [esp+dst]
        movss        xmm0,    [edx+16*0+4*0]   ;读入第一行的数据
        movaps       xmm4,    [ecx+16*0]
        movss        xmm1,    [edx+16*0+4*1]
        shufps       xmm0,    xmm0,    00h
        movaps       xmm5,    [ecx+16*1]
        movss        xmm2,    [edx+16*0+4*2]
        shufps       xmm1,    xmm1,    00h
        mulps        xmm0,    xmm4
        movaps       xmm6,    [ecx+16*2]
        mulps        xmm1,    xmm5
        movss        xmm3,    [edx+16*0+4*3]
        shufps       xmm2,    xmm2,    00h
        movaps       xmm7,    [ecx+16*3]
        shufps       xmm3,    xmm3,    00h
        mulps        xmm2,    xmm6
        addps        xmm1,    xmm0
        movss        xmm0,    [edx+16*1+4*0]   ;读入第二行的数据
        mulps        xmm3,    xmm7
        shufps       xmm0,    xmm0,    00h
        addps        xmm2,    xmm1
        movss        xmm1,    [edx+16*1+4*1]
        mulps        xmm0,    xmm4
        shufps       xmm1,    xmm1,    00h
        addps        xmm3,    xmm2
        movss        xmm2,    [edx+16*1+4*2]
        mulps        xmm1,    xmm5
        shufps       xmm2,    xmm2,    00h
        movaps       [eax+16*0],    xmm3
        movss        xmm3,    [edx+16*1+4*3]
        mulps        xmm2,    xmm6
        shufps       xmm3,    xmm3,    00h
        addps        xmm1,    xmm0
        movss        xmm0,    [edx+16*2+4*0]   ;读入第三行的数据
        mulps        xmm3,    xmm7
        shufps       xmm0,    xmm0,    00h
        addps        xmm2,    xmm1
        movss        xmm1,    [edx+16*2+4*1]
        mulps        xmm0,    xmm4
        shufps       xmm1,    xmm1,    00h
        addps        xmm3,    xmm2
        movss        xmm2,    [edx+16*2+4*2]
        mulps        xmm1,    xmm5
        shufps       xmm2,    xmm2,    00h
        movaps       [eax+16*1],    xmm3
        movss        xmm3,    [edx+16*2+4*3]
        mulps        xmm2,    xmm6
        shufps       xmm3,    xmm3,    00h
        addps        xmm1,    xmm0
        movss        xmm0,    [edx+16*3+4*0]   ;读入第四行的数据
        mulps        xmm3,    xmm7
        shufps       xmm0,    xmm0,    00h
        addps        xmm2,    xmm1
        movss        xmm1,    [edx+16*3+4*1]
        mulps        xmm0,    xmm4
        shufps       xmm1,    xmm1,    00h
        addps        xmm3,    xmm2
        movss        xmm2,    [edx+16*3+4*2]
        mulps        xmm1,    xmm5
        shufps       xmm2,    xmm2,    00h
        movaps       [eax+16*2],    xmm3
        movss        xmm3,    [edx+16*3+4*3]
        mulps        xmm2,    xmm6
        shufps       xmm3,    xmm3,    00h
        addps        xmm1,    xmm0
        mulps        xmm3,    xmm7
        addps        xmm2,    xmm1
        addps        xmm3,    xmm2
        movaps       [eax+16*3],    xmm3
        ret          12
_MatMul_xmm@12 endp
_TEXT ends
        end
上面的代码几乎没有加什么注释,只是在读入每行第一个数据时作了标记。因为,SSE
的指令可读性还是比较好的,除了要加上一些后缀以外,它们和普通的整数运算指令很
相似。
一些关键性的指令有必要解释一下:
movss和movaps:
movss是将一个单精度数传输到xmm寄存器的低32位,而movaps则是一次性向寄存器中写
入四个单精度数。也许有些人会认为movaps效率更高一些,其实并不一定是这样。从处
理器执行单元列表中,你可以查到这些指令的延迟。如果都是从寄存器中读取数据,两
个指令的延迟是一样的。如果是从内存中读取数据,movss只有一个时钟周期的延迟,而
movaps却有四个时钟周期的延迟。
上面的汇编代码混合使用了这两条指令。那么,应该在什么时候选择哪一条指令呢?这
要看你对数据的需求了。如果你希望能够尽快地使用数据,就应当首选movss,因为它几
乎能够让你立即使用数据。如果你并不急于使用某些数据,只是想先把它读入寄存器,
那么毫无疑问movaps是你的最佳选择。 movaps使用端口2读取数据,如果在它执行完毕
之前你不去使用它的数据,这条指令的实际延迟就只有一个时钟周期。考虑到处理器能
够在5个端口并行执行微操作,那么这条指令的延迟可能还不到一个时钟周期。
从上面的代码中,你可以看到,每一条movaps指令和它的相关指令之间都至少插入了四
条指令,这样可以基本上避免延迟。
虽然movss指令只有一个时钟周期的延迟,但是这也并不意味着你可以把这条指令和它的
相关指令写在一起,因为这有可能会影响处理器的并行度。虽然 Pentium III有着强大
的乱序执行的能力,可是这毕竟是不太保险的,还是自己动手,丰衣足食吧。
SHUFPS
这是一条可以将操作数打乱顺序的指令。这一条指令有很多种用法,它根据常量参数的
不同执行不同的功能。本文中只使用了一种用法:
    shufps      xmmreg,  xmmreg,  00h
这条指令的作用是把某个寄存器的最低位的单精度数传输到该寄存器的其它三个部分。


在某些时候,shufps和unpcklps(或unpckhps)可以执行相同的功能。这时,推荐使用
shufps,因为这条指令有两个时钟周期的延迟。unpcklps和unpckhps 都是有三个时钟周
期的延迟。
ADDPS和MULPS
这两条指令是很重要的计算指令,有必要弄清楚它们的执行情况。
addps有4个时钟周期的延迟,mulps有5个时钟周期的延迟,我们应该根据这些数据考虑
清楚,究竟在它们的相关代码中插入多少条指令。
这两条指令都是每两个时钟周期才允许执行一次,如果你把相同的两条这样的指令写在
一起,第二条指令就有可能被延误一个时钟周期。应该插入一些其它指令来掩盖这段延
迟。
mulps在端口0执行,addps在端口1执行,如果你的代码把乘法和加法指令写在一起,它
们会被分配到不同的端口并行执行,这比只有一条流水线的FPU要高效的多。
优化思路:
下面将解释一下上面代码的优化思路。
打乱指令
在算法描述中,各条操作写得非常有规律,但是在真正编程的时候却不是这样。为了保
证流水线的流畅运作,就要把相关的代码分离开来,尽量避免或减轻指令的延迟。这样
就要打乱指令,在两条相关指令之间插入一些其它的指令,同时也要考虑指令之间是否
存在资源的竞争。
并行算法
多个数据并行计算是解决指令延迟问题的有效方法。我们不能傻傻地等待一条指令的计
算结果,而是要在等待的过程中进行其它数据的计算。在上面程序的算法中,每当寄存
器有了空闲,就马上从内存中读入新的数据,尽量保证有两组数据在寄存器中并行计算

内存访问
访问内存的指令不要过于密集,这一方面可以减轻对带宽的需求,另一方面也会提高解
码的效率。访问内存的指令至少有两个微操作,这样的指令只能每个时钟周期解码一条
,而Pentium III的解码极限可是每个时钟周期三条指令啊。为了提高处理器的并行度,
有必要在内存访问指令上下功夫。在我的代码中,内存访问指令的排布还是比较有规律
的,差不多是每隔三条指令访问一次内存。当然,在计算第一行数据时,因为要读取一
些初始化的数据,内存访问比后面的代码要频繁。
灵活性
矩阵的运算是一行一行进行的,每一行数据只被读取一次。这就意味着,我们可以把运
算结果保存在任何一个矩阵里,即保存在m1或者m2中,因为这两个矩阵中的数据已经不
会被再次读取了,也就不用担心破坏数据。这种灵活性可以是我们轻而易举地完成矩阵
左乘或者右乘的代码。在Direct3D中,空间变换是按照如下方式进行计算的:
在进行多次变换时,只要在原有的矩阵上右乘一个变换矩阵就可以了。下面的代码就是
这样的一个例子:
MatMul_xmm(m1, m1, m2);
如果使用高级语言来实现恐怕就要麻烦一些,你要使用一些中间变量,程序如下所示:


void MatMul_Right_cpp(float *dest, float *m)
{
    float tmp[16];
    MatMul_cpp(tmp, dest, m)
    memcpy(dest, tmp, 16*4);
}

QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 7楼 发表于: 2012-09-30
SSE指令集速度测试
 本篇致力于解决以下问题——1.SSE/SSE2指令集是什么?2.如何阅读Intel/AMD的手册?3.如何运用SSE指令集?如何将MMX代码升级为SSE代码。4.如何在VC++6.0这样的高级语言编译器中使用MMX指令集?一、简介  1999 年 Intel 推出了第 1 代的 SSE(Streaming SIMD Extensions)指令以回击 AMD 的 3DNow! 指令,使用在 Pentium III 处理器上。随后 AMD 在 2001 年 10 月 发布 的 Athlon XP 处理器上首次加入了 SSE 指令集。  2001 年 Intel 推出第 2 个版本的 SSE 指令,使用在 Pentium 4 处理器上,AMD 在 2003 年推出的 Athlon 64 和 Opteron 处理器上加入对 SSE2 指令的支持。  SSE有很多后续版本,详见——http://www.cnblogs.com/zyl910/archive/2012/02/26/x86_simd_table.html[x86]SIMD指令集发展历程表(MMX、SSE、AVX等)1.1 概述  SSE技术对x86体系的编程环境的扩展是——1.8个128位的SSE寄存器(xmm0~xmm7)。 // 64位环境下增加到16个寄存器(xmm0~xmm15)。2.SSE数据类型(紧缩单精度浮点)。而在SSE2中,又增加了整数和双精度浮点类型。3.SSE指令系统。1.2 SSE寄存器  SSE寄存器集是由8个128位寄存器组成,见下图。SSE指令使用寄存器名xmm0~xmm7直接访问SSE寄存器。

  还有一个新的控制/状态寄存器MXCSR,用于屏蔽/开放数值异常处理、设置舍入方式、设置清零方式和观察状态标志。  与之前的MMX或3DNow!不同,这些寄存器并不是原来己有的寄存器(MMX和3DNow!均是使用x87浮点数寄存器),所以不需要像MMX或3DNow!一样,要使用x87指令之前,需要利用一个EMMS指令来清除寄存器的状态。因此,不像MMX或3DNow!指令,SSE运算指令,可以很自由地和x87浮点指令,或是MMX指令共用。  2003年,AMD发表的x86-64 延伸架构,为SSE技术增加了8个寄存器,共16个寄存器(xmm0~xmm15)。
(Figure 4-1. SSE Registers。出自AMD手册 Vol.1 112)
1.3 SSE数据类型  SSE技术定义了以下数据类型——1.128位紧缩单精度浮点(128-Bit Packed Single-Precision Floating-Point):4个单精度浮点(single)紧缩成一个128位。
(Figure 10-4. 128-Bit Packed Single-Precision Floating-Point Data Type。出自Intel手册 Vol.1 10-8)  而在SSE2中,又定义了以下5种数据类型——1.128位紧缩双精度浮点(128-Bit Packed Double-Precision Floating-Point):2个双精度浮点(double)紧缩成一个128位。2.128位紧缩字节(128-Bit Packed Byte Integers):16个字节(byte)紧缩成一个128位。3.128位紧缩字(128-Bit Packed Word Integers):8个字(word)紧缩成一个128位。4.128位紧缩双字(128-Bit Packed Doubleword Integers):4个双字(doubleword)紧缩成一个128位。5.128位紧缩四字(128-Bit Packed Quadword Integers):2个四字(quadword)紧缩成一个128位。
(Figure 11-2. Data Types Introduced with the SSE2 Extensions。出自Intel手册 Vol.1 11-5)  因为“将64位像素转为32位像素”这项工作需要字节(byte)和字(word),所以只有SSE2能满足需求。
二、指令解读  对于PACKUSWB指令来说,操作数不仅可以是MMX寄存器(出自MMX指令集),也可以是SSE寄存器(出自SSE2指令集),甚至可以是AVX寄存器(出自AVX指令集。本文不讨AVX指令集)。虽然实际的机器码有所不同。  对于这一点,Intel手册的编排方案是——以指令名为准,然后再在那一节内分别介绍不同寄存器的效果。  而AMD的手册不一样,它按寄存器长度分成两个文档——1.SSE、AVS指令在第4卷中(AMD64 Architecture Programmer's Manual Volume 4: 128-bit and 256 bit media instructions)。2.MMX、3DNow!、浮点指令在第5卷中(AMD64 Architecture Programmer's Manual Volume 5: 64-Bit Media and x87 Floating-Point Instructions)。2.1 Intel手册原文  略。与前一篇(http://www.cnblogs.com/zyl910/archive/2012/04/09/noifopex7.html)的“2.1 Intel手册对PACKUSWB指令的说明”相同。  PS:SSE编程指南见“Volume 1: Basic Architecture”的“Chapter 10 Programming with Streaming SIMD Extensions (SSE)”和“Chapter 11 Programming with Streaming SIMD Extensions 2 (SSE2)”。不属于本文范畴,读者可自行翻阅。2.2 AMD手册原文  找到AMD手册的第4卷(AMD64 Architecture Programmer's Manual Volume 4: 128-bit and 256 bit media instructions)。如果没有的话,请在官网下载——http://developer.amd.com/documentation/guides/Pages/default.aspx#manuals  打开第4卷(26568_APM_v4.pdf)。在左侧的书签树中依次展开“2 Instruction Reference”,然后拖动滚动条找到“PACKUSWB”——
图4  AMD手册对PACKUSWB指令的说明有两页,上图(图4)是第一页的内容。第二页内容不属于本文范畴,故不贴图,读者可自行翻阅。  PS:SSE编程指南见“Volume 1: Application Programming”的“4 Streaming SIMD Extensions Media and Scientific Programming”。不属于本文范畴,读者可自行翻阅。2.3 手册解读  首先看Intel手册,图1最上面的那个方框内,列出了PACKUSWB指令在不同环境下(MMX、SSE、AVX)的效果。
  本篇只关心SSE指令集。此时该指令的格式为“PACKUSWB xmm1, xmm2/m128”,描述信息为“Converts 8 signed word integers from xmm1 and 8 signed word integers from xmm2/m128 into 16 unsigned byte integers in xmm1 using unsigned saturation.”。  而在AMD手册(图4)中,对该指令的描述是“Converts 16-bit signed integers in xmm1 and xmm2 or mem128 into 8-bit signed integers with saturation. Writes packed results to xmm1.”。  在Intel手册PACKUSWB指令的第二页(图2)中,有解释该指令功能的伪代码——
PACKUSWB (with 128-bit operands)DEST[7:0]← SaturateSignedWordToUnsignedByte (DEST[15:0]);DEST[15:8] ← SaturateSignedWordToUnsignedByte (DEST[31:16]);DEST[23:16] ← SaturateSignedWordToUnsignedByte (DEST[47:32]);DEST[31:24] ← SaturateSignedWordToUnsignedByte (DEST[63:48]);DEST[39:32] ← SaturateSignedWordToUnsignedByte (DEST[79:64]);DEST[47:40] ← SaturateSignedWordToUnsignedByte (DEST[95:80]);DEST[55:48] ← SaturateSignedWordToUnsignedByte (DEST[111:96]);DEST[63:56] ← SaturateSignedWordToUnsignedByte (DEST[127:112]);DEST[71:64] ← SaturateSignedWordToUnsignedByte (SRC[15:0]);DEST[79:72] ← SaturateSignedWordToUnsignedByte (SRC[31:16]);DEST[87:80] ← SaturateSignedWordToUnsignedByte (SRC[47:32]);DEST[95:88] ← SaturateSignedWordToUnsignedByte (SRC[63:48]);DEST[103:96] ← SaturateSignedWordToUnsignedByte (SRC[79:64]);DEST[111:104] ← SaturateSignedWordToUnsignedByte (SRC[95:80]);DEST[119:112] ← SaturateSignedWordToUnsignedByte (SRC[111:96]);DEST[127:120] ← SaturateSignedWordToUnsignedByte (SRC[127:112]);

  该伪代码的大致含义为——将DEST中的 每16位的带符号整数 饱和转换为 8位的无符号整数,放到返回值(DEST)的低64位;将SRC中的每16位的整数 饱和转换为 8位的整数,放到返回值的高64位。注:x86指令的2操作数指令一般是——第1个参数是DEST,第2个参数是SRC。即参数格式为“PACKUSWB DEST, SRC”。
2.4 画图解释  用文字或伪代码来解释MMX指令都不太直观,用图片就直观多了。  这一次AMD的手册没有配图。  因此,我画了一张图片,更能清晰表示SSE2模式下PACKUSWB指令的功能——
  该图的风格与第7篇(http://www.cnblogs.com/zyl910/archive/2012/04/09/noifopex7.html)的图片类似,左侧是内存中的源数据,右侧是运算结果,中间是SSE寄存器,箭头代表运算过程。该图 绘有三种操作——1.加载(红色箭头)。将内存中的源数据(源缓冲区)加载到SSE寄存器。因为SSE寄存器是128位(16字节)的,所以该环节共加载了32字节数据,分别加载到2个SSE寄存器中(PACKUSWB需要两个操作数)。2.运算(绿色将头)。这里就是PACKUSWB指令的功能,将 每个16位的带符号整数 饱和转换为 8位的无符号整数。3.存储(蓝色箭头)。将SSE寄存器中的运算结果 存储到内存(目标缓冲区)。
三、如何在VC中使用MMX指令集?  对于Visual C++ 6.0来说,依次打上SP5、PP5补丁后,就能支持MMX、SSE、SSE2这三套指令集。它们的下载地址是——SP5(Visual Studio 6.0 Service Pack 5):http://www.microsoft.com/download/en/details.aspx?id=2618PP5(Visual C++ 6.0 Processor Pack):http://msdn.microsoft.com/en-us/library/aa718349.aspx  对于更高版本Visual Studio,它们内置了对MMX指令集的支持,不需要安装补丁。详见——http://www.cnblogs.com/zyl910/archive/2012/02/28/vs_intrin_table.htmlIntrinsics头文件与SIMD指令集、Visual Studio版本对应表3.1 使用内嵌汇编  在VC中,最直接的办法就是使用内嵌汇编,即利用“_asm”关键字直接写汇编语句。  例如下面那段代码,先尝试执行cpuid指令检查CPU特性,再尝试执行xorps指令测试系统中是否能运行SSE指令——
// 检测SSE系列指令集的支持级别int    simd_sse_level(){    const DWORD    BIT_D_SSE = 0x02000000;    // bit 25    const DWORD    BIT_D_SSE2 = 0x04000000;    // bit 26    const DWORD    BIT_C_SSE3 = 0x00000001;    // bit 0    const DWORD    BIT_C_SSSE3 = 0x00000100;    // bit 9    const DWORD    BIT_C_SSE41 = 0x00080000;    // bit 19    const DWORD    BIT_C_SSE42 = 0x00100000;    // bit 20    BYTE    rt = SIMD_SSE_NONE;    // result    DWORD    v_edx;    DWORD    v_ecx;
    // check processor support    __try     {        _asm         {            mov eax, 1            cpuid            mov v_edx, edx            mov v_ecx, ecx        }    }    __except (EXCEPTION_EXECUTE_HANDLER)    {        return SIMD_SSE_NONE;    }    if ( v_edx & BIT_D_SSE )    {        rt = SIMD_SSE_1;        if ( v_edx & BIT_D_SSE2 )        {            rt = SIMD_SSE_2;            if ( v_ecx & BIT_C_SSE3 )            {                rt = SIMD_SSE_3;                if ( v_ecx & BIT_C_SSSE3 )                {                    rt = SIMD_SSE_3S;                    if ( v_ecx & BIT_C_SSE41 )                    {                        rt = SIMD_SSE_41;                        if ( v_ecx & BIT_C_SSE42 )                        {                            rt = SIMD_SSE_42;                        }                    }                }            }        }    }
    // check OS support    __try     {        _asm        {            xorps xmm0, xmm0    // executing any SSE instruction        }    }    __except (EXCEPTION_EXECUTE_HANDLER)    {        return SIMD_SSE_NONE;    }
    return rt;}
http://www.cnblogs.com/zyl910/archive/2012/03/01/checksimd.html[VC6] 检查MMX和SSE系列指令集的支持级别(最高SSE4.2)
3.2 使用Intrinsics函数  SSE指令也有对应的Intrinsics函数。  在MSDN中可以找到SSE/SSE2的Intrinsics函数帮助:http://msdn.microsoft.com/en-us/library/y0dh78ez(v=vs.110).aspx。具体的目录层次是——MSDN LibraryDevelopment Tools and LanguagesVisual Studio 11 BetaVisual C++  ReferenceC/C++ LanguagesCompiler IntrinsicsMMX, SSE, and SSE2 Intrinsics  SSE/SSE2的Intrinsics函数的应用方法,推荐Alex Farber的《Introduction to SSE Programming》——http://www.codeproject.com/Articles/4522/Introduction-to-SSE-ProgrammingIntroduction to SSE Programming  中文翻译版见——http://dev.gameres.com/Program/Other/sseintro.htm基于SSE指令集的程序设计简介
四、实际应用  现在我们想使用PACKUSWB指令,怎么知道该指令对应的Intrinsics函数呢?  最简单的办法就是查阅Intel手册。在Intel手册PACKUSWB指令的第三页(图3),列出Intrinsic函数的名称——Intel C/C++ Compiler Intrinsic EquivalentPACKUSWB: __m64 _mm_packs_pu16(__m64 m1, __m64 m2)PACKUSWB: __m128i _mm_packus_epi16(__m128i m1, __m128i m2)
  因现在是探讨SSE指令集,所以应该选用_mm_packus_epi16函数。  现在万事具备,可以完成SSE版的“将64位像素转为32位像素”函数了——
// 饱和处理SSE版void f5_sse(BYTE* pbufD, const signed short* pbufS, int cnt){    //const signed short* pS = pbufS;    //BYTE* pD = pbufD;    const __m128i* pS = (const __m128i*)pbufS;    __m128i* pD = (__m128i*)pbufD;    int i;    for(i=0; i<cnt; i+=4)    {        // 同时对四个像素做饱和处理。即 将四个64位像素(4通道,每分量为带符号16位) 转为 四个32位像素(每分量为无符号8位)。        pD[0] = _mm_packus_epi16(pS[0], pS[1]);    // 饱和方式数据打包(带符号16位->无符号8位)。等价于 for(i=0;i<8;++i){ pD[0].uB[i]=SU(pS[0].iW[i]); r.uB[8+i]=SU(pS[1].iW[i]); }        // next        pS += 2;        pD += 1;    }}

  因SSE2版PACKUSWB指令(_mm_packus_epi16)的功能,现在的内循环变得十分简单,一次就能处理4个像素。所以每次循环时“i+=4”。  注意这里将pbufS、pbufD这两个指针均设定为__m128i指针类型。所以“pD += 1”实际上将指针地址前移了16个字节,而“pS += 2”将指针地址前移了32个字节。
五、全部代码  全部代码——
// MMX, SSE, SSE2#include <emmintrin.h>

// 用位掩码做饱和处理.用求负生成掩码#define LIMITSU_FAST(n, bits) ( (n) & -((n) >= 0) | -((n) >= (1<<(bits))) )#define LIMITSU_SAFE(n, bits) ( (LIMITSU_FAST(n, bits)) & ((1<<(bits)) - 1) )#define LIMITSU_BYTE(n) ((BYTE)(LIMITSU_FAST(n, 8)))
// 用位掩码做饱和处理.用带符号右移生成掩码#define LIMITSW_FAST(n, bits) ( ( (n) | ((signed short)((1<<(bits)) - 1 - (n)) >> 15) ) & ~((signed short)(n) >> 15) )#define LIMITSW_SAFE(n, bits) ( (LIMITSW_FAST(n, bits)) & ((1<<(bits)) - 1) )#define LIMITSW_BYTE(n) ((BYTE)(LIMITSW_FAST(n, 8)))

// 数据规模#define DATASIZE    16384    // 128KB / (sizeof(signed short) * 4)
// 缓冲区。SSE需要按128位对齐__declspec(align(16)) signed short    bufS[DATASIZE*4];    // 源缓冲区。64位的颜色(4通道,每通道16位)__declspec(align(16)) BYTE    bufD[DATASIZE*4];    // 目标缓冲区。32位的颜色(4通道,每通道8位)
// 测试时的函数类型typedef void (*TESTPROC)(BYTE* pbufD, const signed short* pbufS, int cnt);

// http://www.cnblogs.com/zyl910/archive/2012/03/01/checksimd.html// 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.2
const char*    simd_sse_names[] = {    "None",    "SSE",    "SSE2",    "SSE3",    "SSSE3",    "SSE4.1",    "SSE4.2",};
// 是否支持MMX指令集BOOL    simd_mmx(){    const DWORD    BIT_DX_MMX = 0x00800000;    // bit 23    DWORD    v_edx;
    // check processor support    __try     {        _asm         {            mov eax, 1            cpuid            mov v_edx, edx        }    }    __except (EXCEPTION_EXECUTE_HANDLER)    {        return FALSE;    }    if ( v_edx & BIT_DX_MMX )    {        // check OS support        __try         {            _asm            {                pxor mm0, mm0    // executing any MMX instruction                emms            }            return TRUE;        }        __except (EXCEPTION_EXECUTE_HANDLER)        {        }    }    return FALSE;}
// 检测SSE系列指令集的支持级别int    simd_sse_level(){    const DWORD    BIT_D_SSE = 0x02000000;    // bit 25    const DWORD    BIT_D_SSE2 = 0x04000000;    // bit 26    const DWORD    BIT_C_SSE3 = 0x00000001;    // bit 0    const DWORD    BIT_C_SSSE3 = 0x00000100;    // bit 9    const DWORD    BIT_C_SSE41 = 0x00080000;    // bit 19    const DWORD    BIT_C_SSE42 = 0x00100000;    // bit 20    BYTE    rt = SIMD_SSE_NONE;    // result    DWORD    v_edx;    DWORD    v_ecx;
    // check processor support    __try     {        _asm         {            mov eax, 1            cpuid            mov v_edx, edx            mov v_ecx, ecx        }    }    __except (EXCEPTION_EXECUTE_HANDLER)    {        return SIMD_SSE_NONE;    }    if ( v_edx & BIT_D_SSE )    {        rt = SIMD_SSE_1;        if ( v_edx & BIT_D_SSE2 )        {            rt = SIMD_SSE_2;            if ( v_ecx & BIT_C_SSE3 )            {                rt = SIMD_SSE_3;                if ( v_ecx & BIT_C_SSSE3 )                {                    rt = SIMD_SSE_3S;                    if ( v_ecx & BIT_C_SSE41 )                    {                        rt = SIMD_SSE_41;                        if ( v_ecx & BIT_C_SSE42 )                        {                            rt = SIMD_SSE_42;                        }                    }                }            }        }    }
    // check OS support    __try     {        _asm        {            xorps xmm0, xmm0    // executing any SSE instruction        }    }    __except (EXCEPTION_EXECUTE_HANDLER)    {        return SIMD_SSE_NONE;    }
    return rt;}

// 用if分支做饱和处理void f0_if(BYTE* pbufD, const signed short* pbufS, int cnt){    const signed short* pS = pbufS;    BYTE* pD = pbufD;    int i;    for(i=0; i<cnt; ++i)    {        // 分别对4个通道做饱和处理        pD[0] = (pS[0]<0) ? 0 : ( (pS[0]>255) ? 255 : (BYTE)pS[0] );        pD[1] = (pS[1]<0) ? 0 : ( (pS[1]>255) ? 255 : (BYTE)pS[1] );        pD[2] = (pS[2]<0) ? 0 : ( (pS[2]>255) ? 255 : (BYTE)pS[2] );        pD[3] = (pS[3]<0) ? 0 : ( (pS[3]>255) ? 255 : (BYTE)pS[3] );        // next        pS += 4;        pD += 4;    }}
// 用min、max饱和处理void f1_min(BYTE* pbufD, const signed short* pbufS, int cnt){    const signed short* pS = pbufS;    BYTE* pD = pbufD;    int i;    for(i=0; i<cnt; ++i)    {        // 分别对4个通道做饱和处理        pD[0] = min(max(0, pS[0]), 255);        pD[1] = min(max(0, pS[1]), 255);        pD[2] = min(max(0, pS[2]), 255);        pD[3] = min(max(0, pS[3]), 255);        // next        pS += 4;        pD += 4;    }}
// 用位掩码做饱和处理.用求负生成掩码void f2_neg(BYTE* pbufD, const signed short* pbufS, int cnt){    const signed short* pS = pbufS;    BYTE* pD = pbufD;    int i;    for(i=0; i<cnt; ++i)    {        // 分别对4个通道做饱和处理        pD[0] = LIMITSU_BYTE(pS[0]);        pD[1] = LIMITSU_BYTE(pS[1]);        pD[2] = LIMITSU_BYTE(pS[2]);        pD[3] = LIMITSU_BYTE(pS[3]);        // next        pS += 4;        pD += 4;    }}
// 用位掩码做饱和处理.用带符号右移生成掩码void f3_sar(BYTE* pbufD, const signed short* pbufS, int cnt){    const signed short* pS = pbufS;    BYTE* pD = pbufD;    int i;    for(i=0; i<cnt; ++i)    {        // 分别对4个通道做饱和处理        pD[0] = LIMITSW_BYTE(pS[0]);        pD[1] = LIMITSW_BYTE(pS[1]);        pD[2] = LIMITSW_BYTE(pS[2]);        pD[3] = LIMITSW_BYTE(pS[3]);        // next        pS += 4;        pD += 4;    }}
// 饱和处理MMX版void f4_mmx(BYTE* pbufD, const signed short* pbufS, int cnt){    //const signed short* pS = pbufS;    //BYTE* pD = pbufD;    const __m64* pS = (const __m64*)pbufS;    __m64* pD = (__m64*)pbufD;    int i;    for(i=0; i<cnt; i+=2)    {        // 同时对两个像素做饱和处理。即 将两个64位像素(4通道,每分量为带符号16位) 转为 两个32位像素(每分量为无符号8位)。        pD[0] = _mm_packs_pu16(pS[0], pS[1]);    // 饱和方式数据打包(带符号16位->无符号8位)。等价于 for(i=0;i<4;++i){ pD[0].uB[i]=SU(pS[0].iW[i]); pD[0].uB[4+i]=SU(pS[1].iW[i]); }        // next        pS += 2;        pD += 1;    }
    // MMX状态置空    _mm_empty();}
// 饱和处理SSE版void f5_sse(BYTE* pbufD, const signed short* pbufS, int cnt){    //const signed short* pS = pbufS;    //BYTE* pD = pbufD;    const __m128i* pS = (const __m128i*)pbufS;    __m128i* pD = (__m128i*)pbufD;    int i;    for(i=0; i<cnt; i+=4)    {        // 同时对四个像素做饱和处理。即 将四个64位像素(4通道,每分量为带符号16位) 转为 四个32位像素(每分量为无符号8位)。        pD[0] = _mm_packus_epi16(pS[0], pS[1]);    // 饱和方式数据打包(带符号16位->无符号8位)。等价于 for(i=0;i<8;++i){ pD[0].uB[i]=SU(pS[0].iW[i]); r.uB[8+i]=SU(pS[1].iW[i]); }        // next        pS += 2;        pD += 1;    }}
// 进行测试void runTest(char* szname, TESTPROC proc){    const int nLoop = 16;    // 使用MMX/SSE指令时速度太快了,只好再多循环几次    int i,j,k;    DWORD    tm0, tm1;    // 存储时间    for(i=1; i<=3; ++i)    // 多次测试    {        //tm0 = GetTickCount();        tm0 = timeGetTime();        // main        for(k=1; k<=nLoop; ++k)        {            for(j=1; j<=4000; ++j)    // 重复运算几次延长时间,避免计时精度问题            {                proc(bufD, bufS, DATASIZE);            }        }        // show        //tm1 = GetTickCount() - tm0;        tm1 = timeGetTime() - tm0;        printf("%s[%d]:\t%.1f\n", szname, i, (double)tm1/nLoop);        // check        //if (1==i)        //{        //    // 检查结果        //    for(j=0; j<=16; ++j)        //    printf("[%d]:\t%d\t%u\n", j, bufS[j], bufD[j]);        //}
    }}
int main(int argc, char* argv[]){    int i;    // 循环变量
    //printf("Hello World!\n");    printf("== noif:VC6 SIMD ==");
    // 初始化    srand( (unsigned)time( NULL ) );    for(i=0; i<DATASIZE*4; ++i)    {        bufS[i] = (signed short)((rand()&0x1FF) - 128);    // 使数值在 [-128, 383] 区间    }
    // 准备开始。可以将将进程优先级设为实时    if (argc<=1)    {        printf("<Press any key to continue>");        getch();        printf("\n");    }
    // 进行测试    //runTest("f0_if", f0_if);    //runTest("f1_min", f1_min);    //runTest("f2_neg", f2_neg);    //runTest("f3_sar", f3_sar);    if (simd_mmx())    runTest("f4_mmx", f4_mmx);    if (simd_sse_level()>=SIMD_SSE_2)    runTest("f5_sse", f5_sse);
    // 结束前提示    if (argc<=1)    {        printf("<Press any key to exit>");        getch();        printf("\n");    }
    return 0;}

  注意——1.SSE需要按128位对齐。于是bufS、bufD的声明中增加了“__declspec(align(16))”。2.在调用测试函数时,需要先检查是否支持相应的指令集——    if (simd_mmx())    runTest("f4_mmx", f4_mmx);    if (simd_sse_level()>=SIMD_SSE_2)    runTest("f5_sse", f5_sse);

六、测试结果  在32位winXP上的测试结果——
== noif:VC6 SIMD ==<Press any key to continue>f4_mmx[1]:      37.5f4_mmx[2]:      37.3f4_mmx[3]:      38.9f5_sse[1]:      25.6f5_sse[2]:      26.1f5_sse[3]:      25.7
  在64位win7上的测试结果——
== noif:VC6 SIMD ==<Press any key to continue>f4_mmx[1]:      37.1f4_mmx[2]:      37.1f4_mmx[3]:      36.1f5_sse[1]:      25.4f5_sse[2]:      24.4f5_sse[3]:      25.3
  硬件环境——CPU:Intel Core i3-2310M, 2100 MHz内存:DDR3-1066 参考文献——《Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B & 2C): Instruction Set Reference, A-Z》. December 2011. http://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-instruction-set-reference-manual-325383.html《AMD64 Architecture Programmer's Manual Volume 4: 128-bit and 256 bit media instructions》. December 2011. http://support.amd.com/us/Processor_TechDocs/26568_APM_v4.pdf《Pentium Ⅱ/Ⅲ体系结构及扩展技术》。刘清森、马鸣锦、吴灏等著。国防工业出版社,2000年7月。《Introduction to SSE Programming》。Alex Farber 著。http://www.codeproject.com/Articles/4522/Introduction-to-SSE-Programming《基于SSE指令集的程序设计简介》(Introduction to SSE Programming)。?译。http://dev.gameres.com/Program/Other/sseintro.htm《Pentium III处理器的SSE入门》。Bipin Patwardhan著,foenix译。http://www.vckbase.com/document/viewdoc/?id=322《在C/C++代码中使用SSE等指令集的指令(1)介绍》。gengshenghong著。http://blog.csdn.net/gengshenghong/article/details/7007100《x86 指令集发展历程》。mik著。http://www.mouseos.com/x64/SIMD/x86_ISA.html 源码下载——http://files.cnblogs.com/zyl910/noifVC6s.rar(建议阅读编译器生成的汇编代码,位于Release\noifVC6s.asm)
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 8楼 发表于: 2012-10-01
SIMD(MMX/SSE/AVX)变量命名规范心得
当使用Intrinsics函数来操作SIMD指令集(MMX/SSE/AVX等)时,会面对不同长度的SIMD数据类型,其中又分为多种紧缩格式。为此,我设计了一套SIMD变量命名规范,可以有效的提高代码的可读性。
一、SIMD数据类型简介
  SIMD数据类型有——
__m64:64位紧缩整数(MMX)。
__m128:128位紧缩单精度(SSE)。
__m128d:128位紧缩双精度(SSE2)。
__m128i:128位紧缩整数(SSE2)。
__m256:256位紧缩单精度(AVX)。
__m256d:256位紧缩双精度(AVX)。
__m256i:256位紧缩整数(AVX)。
注:紧缩整数包括了8位、16位、32位、64位的带符号和无符号整数。

  这些数据类型与寄存器的对应关系为——
64位MM寄存器(MM0~MM7):__m64。
128位SSE寄存器(XMM0~XMM15):__m128、__m128d、__m128i。
256位AVX寄存器(YMM0~YMM15):__m256、__m256d、__m256i。


二、SIMD变量命名规范

  参考匈牙利命名法(Hungarian notation),在变量名前面增加类型前缀。
  类型前缀为3个小写字母,首字母代表寄存器宽度,最后两个字母代表紧缩数据类型。

  寄存器宽度(首字母)——
m:64位MM寄存器。对应 __m64
x:128位SSE寄存器。对应 __m128、__m128d、__m128i。
y:256位AVX寄存器。对应 __m256、__m256d、__m256i。

  紧缩数据类型(两个字母)——
mb:8位数据。用于只知道长度、不知道具体紧缩格式时。(b:Byte)
mw:16位数据。(w:Word)
md:32位数据。(d:DoubleWord)
mq:64位数据。(q:QuadWord)
mo:128位数据。(o:OctaWord)
mh:256位数据。(h:HexWord)
ub:8位无符号整数。
uw:16位无符号整数。
ud:32位无符号整数。
uq:64位无符号整数。
ib:8位带符号整数。
iw:16位带符号整数。
id:32位带符号整数。
iq:64位带符号整数。
fh:16位浮点数,即半精度浮点数。(h:Half)
fs:32位浮点数,即单精度浮点数。(s:Single)
fd:64位浮点数,即双精度浮点数。(d:double)

  例如——
mub:64位紧缩字节(64位MMX寄存器,其中存放了8个8位无符号整数)。
xfs:128位紧缩单精度(128位SSE寄存器,其中存放了4个单精度浮点数)。
xid:128位紧缩带符号字(128位SSE寄存器,其中存放了4个32位带符号整数)。
yfd:256位紧缩双精度(256位AVX寄存器,其中存放了4个双精度浮点数)。
yfh:256位紧缩半精度(256位AVX寄存器,其中存放了16个半精度浮点数)。


三、示例代码
  例如SSE累加求和程序——


int sum3_Intrinsics(int *a, int size)
{
    if (NULL==a)    return 0;
    if (size<0)    return 0;


    int s = 0;    // 返回值
    __m128i xidSum = _mm_setzero_si128();    // 累积。[SSE2] 赋初值0
    __m128i xidLoad;    // 加载
    int cntBlock = size / 4;    // 块数。SSE寄存器能一次处理4个DWORD
    int cntRem = size & 3;    // 剩余数量
    __m128i* p = (__m128i*)a;
    for(int i = 0; i < cntBlock; ++i)
    {
        xidLoad = _mm_load_si128(p);    // [SSE2] 加载
        xidSum = _mm_add_epi32(xidSum, xidLoad);    // [SSE2] 带符号32位紧缩加法
        ++p;
    }


    // 处理剩下的
    int* q = (int*)p;
    for(int i = 0; i < cntRem; ++i)    s += q;


    // 将累加值合并
    xidSum = _mm_hadd_epi32(xidSum, xidSum);    // [SSSE3] 带符号32位水平加法
    xidSum = _mm_hadd_epi32(xidSum, xidSum);
    s += _mm_cvtsi128_si32(xidSum);    // [SSE2] 返回低32位


    return s;
}

QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 9楼 发表于: 2013-08-11
SIMD(MMX/SSE/AVX)变量命名规范心得

当使用Intrinsics函数来操作SIMD指令集(MMX/SSE/AVX等)时,会面对不同长度的SIMD数据类型,其中又分为多种紧缩格式。为此,我设计了一套SIMD变量命名规范,可以有效的提高代码的可读性。

一、SIMD数据类型简介

  SIMD数据类型有——
__m64:64位紧缩整数(MMX)。
__m128:128位紧缩单精度(SSE)。
__m128d:128位紧缩双精度(SSE2)。
__m128i:128位紧缩整数(SSE2)。
__m256:256位紧缩单精度(AVX)。
__m256d:256位紧缩双精度(AVX)。
__m256i:256位紧缩整数(AVX)。
注:紧缩整数包括了8位、16位、32位、64位的带符号和无符号整数。

  这些数据类型与寄存器的对应关系为——
64位MM寄存器(MM0~MM7):__m64。
128位SSE寄存器(XMM0~XMM15):__m128、__m128d、__m128i。
256位AVX寄存器(YMM0~YMM15):__m256、__m256d、__m256i。


二、SIMD变量命名规范

  参考匈牙利命名法(Hungarian notation),在变量名前面增加类型前缀。
  类型前缀为3个小写字母,首字母代表寄存器宽度,最后两个字母代表紧缩数据类型。

  寄存器宽度(首字母)——
m:64位MM寄存器。对应 __m64
x:128位SSE寄存器。对应 __m128、__m128d、__m128i。
y:256位AVX寄存器。对应 __m256、__m256d、__m256i。

  紧缩数据类型(两个字母)——
mb:8位数据。用于只知道长度、不知道具体紧缩格式时。(b:Byte)
mw:16位数据。(w:Word)
md:32位数据。(d:DoubleWord)
mq:64位数据。(q:QuadWord)
mo:128位数据。(o:OctaWord)
mh:256位数据。(h:HexWord)
ub:8位无符号整数。
uw:16位无符号整数。
ud:32位无符号整数。
uq:64位无符号整数。
ib:8位带符号整数。
iw:16位带符号整数。
id:32位带符号整数。
iq:64位带符号整数。
fh:16位浮点数,即半精度浮点数。(h:Half)
fs:32位浮点数,即单精度浮点数。(s:Single)
fd:64位浮点数,即双精度浮点数。(d:double)

  例如——
mub:64位紧缩字节(64位MMX寄存器,其中存放了8个8位无符号整数)。
xfs:128位紧缩单精度(128位SSE寄存器,其中存放了4个单精度浮点数)。
xid:128位紧缩带符号字(128位SSE寄存器,其中存放了4个32位带符号整数)。
yfd:256位紧缩双精度(256位AVX寄存器,其中存放了4个双精度浮点数)。
yfh:256位紧缩半精度(256位AVX寄存器,其中存放了16个半精度浮点数)。

 

三、示例代码

  例如SSE累加求和程序——

  1. int sum3_Intrinsics(int *a, int size)  
  2. {  
  3.     if (NULL==a)    return 0;  
  4.     if (size<0)    return 0;  
  5.   
  6.     int s = 0;    // 返回值  
  7.     __m128i xidSum = _mm_setzero_si128();    // 累积。[SSE2] 赋初值0  
  8.     __m128i xidLoad;    // 加载  
  9.     int cntBlock = size / 4;    // 块数。SSE寄存器能一次处理4个DWORD  
  10.     int cntRem = size & 3;    // 剩余数量  
  11.     __m128i* p = (__m128i*)a;  
  12.     for(int i = 0; i < cntBlock; ++i)  
  13.     {  
  14.         xidLoad = _mm_load_si128(p);    // [SSE2] 加载  
  15.         xidSum = _mm_add_epi32(xidSum, xidLoad);    // [SSE2] 带符号32位紧缩加法  
  16.         ++p;  
  17.     }  
  18.   
  19.     // 处理剩下的  
  20.     int* q = (int*)p;  
  21.     for(int i = 0; i < cntRem; ++i)    s += q;  
  22.   
  23.     // 将累加值合并  
  24.     xidSum = _mm_hadd_epi32(xidSum, xidSum);    // [SSSE3] 带符号32位水平加法  
  25.     xidSum = _mm_hadd_epi32(xidSum, xidSum);  
  26.     s += _mm_cvtsi128_si32(xidSum);    // [SSE2] 返回低32位  
  27.   
  28.     return s;  
  29. }  
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
描述
快速回复

您目前还是游客,请 登录注册
批量上传需要先选择文件,再选择上传