SIMD

一、SIMD

SIMD单指令流多数据流(SingleInstruction Multiple Data,SIMD)是一种采用一个控制器来控制多个处理器,同时对一组数据(又称数据向量)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。在微处理器中,单指令流多数据流技术则是一个控制器控制多个平行的处理微元,例如IntelMMXSSEAVX以及AMD3D Now!技术,本文只介绍IntelSIMD技术。

二 、MMX

MMX是由英特尔开发的一种SIMD多媒体指令集,共有57条指令。它于1996年集成在英特尔奔腾(Pentium) MMX处理器上,以提高其多媒体数据的处理能力。

优点是增加了处理器关于多媒体方面的处理能力,缺点是占用浮点数寄存器进行运算(64MMX寄存器实际上就是浮点数寄存器的别名)以至于MMX指令和浮点数操作不能同时工作。为了减少在MMX和浮点数模式切换之间所消耗的时间,程序员们尽可能减少模式切换的次数,也就是说,这两种操作在应用上是互斥的。后来英特尔在此基础上发展出SSE指令集。现在新开发的程序不再仅使用MMX来优化软件执行效能,而是改使用如SSEAVX等更容易优化效能的新一代多媒体指令集,不过目前的处理器仍可以执行针对MMX优化的较早期软件。

MMX寄存器,称作MM0MM7,实际上就是处理器内部80比特字长的浮点寄存器栈st(0)st(7)的尾数部分(64比特长)的复用。由于浮点栈寄存器的高16位未被MMX技术使用,因此这16位都置为1,因此从栈寄存器的角度看,其浮点值为NaNInfinities,这可用于区分寄存器是处于浮点栈状态还是MMX状态.作为MMX寄存器都是直接访问。利用了装配数据类型(packeddata type)的概念,每个MMX寄存器的64比特字长可以看作是232位整数、或者416位整数、或者88位整数,从而可以执行整数SIMD运算。这对于1990年代中期的2D3D计算的加速还是很有意义的,因为当时的计算机的图形处理器(GPU)还很不发达。但现在MMX整数SIMD运算对于图形运算来说是多余的技术了。不过MMX的饱和算术运算(saturationarithmeticoperations)对于一些数字信号处理应用还是有用的

 

三、 SSE

MMX技术之后,Intel又于1999年在Pentium-III处理器上推出SSE技术,引入了新的128比特宽的寄存器集 (register file),称作XMM0XMM7。这些XMM寄存器用于4个单精度浮点数运算的SIMD执行,并可以与MMX整数运算或x87浮点运算混合执行。 2001年在Pentium 4上引入了SSE2技术,进一步扩展了指令集,使得XMM寄存器上可以执行8/16/32位宽的整数SIMD运算或双精度浮点数的SIMD运算。这使得 SIMD技术基本完善。

SSE(StreamingSIMD Extensions)是英特尔在其计算机芯片Pentium III中引入的指令集,是继MMX的扩充指令集。SSE指令集提供了 70条新指令

 

 SSE寄存器

SSE 加入新的 8 28 位缓存器(XMM0XMM7)。除此之外还有一个新的 32 位的控制/状态缓存器(MXCSR)。不过只能在 64位的模式下才能使用额外8个缓存器。

每个缓存器可以容纳 432位单精度浮点数,或是2 64位双精度浮点数,或是 432位整数,或是 816位短整数,或是 16个字符。整数运算能够使用正负号运算。而整数 SIMD运算可能仍然要与 864MMX缓存器一起执行。

因为操作系统必须要在进程切换的时候保护这些128位的缓存器状态,除非操作系统去启动这些缓存器,否则默认值是不会去启用的。这表示操作系统必须要知道如何使用FXSAVEFXRSTOR 指令才能储存x87 SSE缓存器的状态。而在当时IA-32的主流操作系统很快的都加入了此功能。

由于 SSE加入了浮点支持,SSE就比MMX更加常用。而SSE2加入了整数运算支持之后让SSE更加的有弹性,当MMX变成是多余的指令集,SSE指令集甚至可以与MMX并行运作,在某些时候可以提供额外的性能增进。

第一个支持 SSE CPU Pentium III,在FPUSSE之间共享执行支持。当编译出来的软件能够交叉的同时以FPUSSE运作,PentiumIII并无法在同一个周期中同时执行FPUSSE。这个限制降低了指令管线的有效性,不过XMM缓存器能够让SIMD与纯量浮点运算混合执行,而不会因为切换MMX/浮点模式而产生性能的折损。

 

数据类型

Intrinsic使用的数据类型和其寄存器是对应,有

·        64 MMX指令集使用

·        128 SSE指令集使用

·        256 AVX指令集使用

甚至AVX-512指令集有512位的寄存器,那么相对应Intrinsic的数据也就有512位。
具体的数据类型及其说明如下:

1.   **__m64** 64位对应的数据类型,该类型仅能供MMX指令集使用。由于MMX指令集也能使用SSE指令集的128位寄存器,故该数据类型使用的情况较少。

2.   **__m128 / __m128i / __m128d** 这三种数据类型都是128位的数据类型。由于SSE指令集即能操作整型,又能操作浮点型(单精度和双精度),这三种数据类型根据所带后缀的不同代表不同类型的操作数。__m128是单精度浮点数,__m128i是整型,__m128d是双精度浮点数。

3.   256512的数据类型和128位的类似,只是存放的个数不同,这里不再赘述。

 

知道了各种数据类型的长度以及其代码的意义,那么它的表现形式到底是怎么样的呢?看下图


__m128i yy;

yy__m128i型,从上图可以看出__m128i是一个联合体(union),根据不同成员包含不同的数据类型。看其具体的成员包含了8位、16位、32位和64位的有符号/无符号整数(这里__m128i是整型,故只有整型的成员,浮点数的使用__m128)。而每个成员都是一个数组,数组中填充着相应的数据,并且根据数据长度的不同数组的长度也不同(数组长度 = 128 / 每个数据的长度(位))。在使用的时候一定要特别的注意要操作数据的类型,也就是数据的长度,例如上图同一个变量yy当作432位有符号整型使用时其数据是:0010241024;但是当做64位有符号整型时其数据为:04398046512128,大大的不同

 

Intrinsic 函数的命名

Intrinsic函数的命名也是有一定的规律的,一个Intrinsic通常由3部分构成,这个三个部分的具体含义如下:

1.   第一部分为前缀_mm,表示是SSE指令集对应的Intrinsic函数。_mm256_mm512AVX,AVX-512指令集的Intrinsic函数前缀,这里只讨论SSE故略去不作说明。

2.   第二部分为对应的指令的操作,如_add_mul_load等,有些操作可能会有修饰符,如loadu将未16位对齐的操作数加载到寄存器中。

3.   第三部分为操作的对象名及数据类型,_ps packed操作所有的单精度浮点数;_pd packed操作所有的双精度浮点数;_pixxxx为长度,可以是8163264packed操作所有的xx位有符号整数,使用的寄存器长度为64位;_epixxxx为长度)packed操作所有的xx位的有符号整数,使用的寄存器长度为128位;_epuxx packed操作所有的xx位的无符号整数;_ss操作第一个单精度浮点数。....

将这三部分组合到以其就是一个完整的Intrinsic函数,如_mm_mul_epi32对参数中所有的32位有符号整数进行乘法运算。

SSE指令集对分支处理能力非常的差,而且从128位的数据中提取某些元素数据的代价又非常的大,因此不适合有复杂逻辑的运算。

 

后续版本

SSE2 IntelPentium 4处理器的最初版本中引入的。SSE2指令集添加了对64位双精度浮点数的支持,以及对整型数据的支持,也就是说这个指令集中所有的MMX指令都是多余的了,同时也避免了占用浮点数寄存器。这个指令集还增加了对CPU快取的控制指令。Intel后来在其Intel 64架构中也增加了对x86-64的支持。

SSE3 IntelPentium 4处理器的 Prescott核心中引入的第三代SIMD指令集。这个指令集扩展的指令包含寄存器的局部位之间的运算,例如高位和低位之间的加减运算;浮点数到整数的转换,以及对超线程技术的支持

SSSE3Intel针对SSE3指令集的一次额外扩充,最早内建于Core2 Duo处理器中。

SSE4IntelPenryn核心的Core 2 DuoCore2 Solo处理器时,新增的47条新多媒体指令集,并且现在更新至SSE4.2

AVXSandy BridgeLarrabee架构新指令集,Intel的微架构也进入了全速发展的时期,在20104月结束的IDF峰会上Intel公司就发布了2010年的RoadMap20111Intel发布全新的处理器微架构Sandy Bridge,其中全新增加的指令集也将带来CPU性能的提

升。AVX(AdvancedVector Extensions)IntelSSE延伸架构,如IA16IA32般的把缓存器XMM 128bit提升至YMM 256bit,以增加一倍的运算效率。此架构支持了三运算指令(3-OperandInstructions),减少在编码上需要先复制才能运算的动作。在微码部分使用了LES LDS这两少用的指令作为延伸指令Prefix



SSE sample vec_add.cpp

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
#include<sys/time.h>
#include<stdbool.h>

#include<xmintrin.h>//SSE指令需要的头文件

#define N 4*100000

float op1[N] __attribute__((aligned(32)));//内存对齐
float op2[N] __attribute__((aligned(32)));
float result1[N] __attribute__((aligned(32)));
float result2[N] __attribute__((aligned(32)));

void init()
{
    for(int i = 0;i < N; i++){
        op1[i] = (float)rand()/(float)RAND_MAX;
        op2[i] = (float)rand()/(float)RAND_MAX;
    }
}

void checkResult(int debug)
{
    bool isSame = true;
    for(int i = 0;i < N; i++)
    {
        if (debug){
            printf("%lf     %lf\n", result1[i], result2[i]);
        }
        else{
            if (fabs(result1[i] - result2[i]) > 0.000001){
                isSame = false;
                break;
            }
        }
    }
    if (!debug) {
        if (isSame)
            printf("Result is Same\n");
        else
            printf("Result is not same\n");
    }
}

void add1()
{
    for(int i = 0; i < N;i++)
        result1[i] = op1[i] + op2[i];
}

void add2()
{
    __m128  a;
    __m128  b;
    __m128  c;

    for(int i = 0; i < N;i = i + 4)
    {
		//从op1+i执向的内存中,加载4个float数到寄存器
        a = _mm_load_ps(op1 + i); 
        b = _mm_load_ps(op2 + i);

        c = _mm_add_ps(a, b);//并行计算加4个float数据

		//将寄存器中的 4个float数存储到c指向内存中
        _mm_store_ps(result2 + i, c); 
    }
}

int main(int argc, char* argv[])
{
    init();
    srand((unsigned int)time(NULL));

    printf("Add a vector:\n");
    struct timeval start;
    struct timeval end;

    gettimeofday(&start,NULL);
    add1();
    gettimeofday(&end,NULL);

    float time_use=1000.0*(float)(end.tv_sec-start.tv_sec)+(float)(end.tv_usec-start.tv_usec)/1000.0;
    printf("add a vector Time use =%f(ms)\n",time_use);

    printf("\n");


    printf("Add a vector with SSE instructions:\n");
    gettimeofday(&start,NULL);
    add2();
    gettimeofday(&end,NULL);

    time_use=1000.0*(float)(end.tv_sec-start.tv_sec)+(float)(end.tv_usec-start.tv_usec)/1000.0;
    printf("add a vector with sse Time use =%f(ms)\n",time_use);
    printf("\n");

    checkResult(0);

    //test();

    return 0;
}

编译:

g++ vec_add.cpp -msse -o vec_add

执行结果:


 

四、自动向量化

使用SSE、AVX的指令可以提高CPU的计算性能,但是学习和使用SIMD指令都需要成本。自动向量话只需要在编译时加入自动向量话指令,当然性能和使用SIMD指令要稍差些。

自动向量化编译选项:(gcc编译器)

          -O3(使用O3默认打开自动向量化)

         -ftree-vectorize(打开自动向量化)

         -ftree-vectorizer-verbose=n(编译时输出向量化信息,n是整数,3和9常用)

 

 

 

 

 

参考:

1https://software.intel.com/sites/landingpage/IntrinsicsGuide/#

2http://blog.csdn.net/conowen/article/details/7255920

3https://gcc.gnu.org/projects/tree-ssa/vectorization.html#using


  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值