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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章