SIMD初學

SIMD初學

在學習SIMD之前,我們首先需要了解兩個概念。
浮點運算指令分爲兩大類:Packed(矢量) 和Scalar(標量)。
Packed指令是一次對寄存器中的四個浮點數(即DATA0 ~ DATA3)均進行計算,而Scalar一次則只對寄存器中的DATA0進行計算。如下圖所示:
在這裏插入圖片描述

1.SIMD的歷史與指令集分類

SIMD(Single Instruction Multiple Data)即單指令流多數據流,是一種採用一個控制器來控制多個處理器,同時對一組數據(又稱“數據向量”)中的每一個分別執行相同的操作從而實現空間上的並行性的技術。簡單來說就是一個指令能夠同時處理多個數據。
SIMD於20世紀70年代首次引用於ILLIAC IV大規模並行計算機上。而大規模應用到消費級計算機則是在20實際90年代末。

1996年Intel推出了X86的MMX(MultiMedia eXtension)指令集擴展,MMX定義了8個寄存器,稱爲MM0到MM7,以及對這些寄存器進行操作的指令。每個寄存器爲64位寬,可用於以“壓縮”格式保存64位整數或多個較小整數,然後可以將單個指令一次應用於兩個32位整數,四個16位整數或8個8位整數。

intel在1999年又推出了全面覆蓋MMX的SSE(Streaming SIMD Extensions, 流式SIMD擴展)指令集,並將其應用到Pentium III系列處理器上,SSE添加了八個新的128位寄存器(XMM0至XMM7),而後來的X86-64擴展又在原來的基礎上添加了8個寄存器(XMM8至XMM15)。SSE支持單個寄存器存儲4個32位單精度浮點數,之後的SSE2則支持單個寄存器存儲2個64位雙精度浮點數,2個64位整數或4個32位整數或8個16位短整形。SSE2之後還有SSE3,SSE4以及AVX,AVX2等擴展指令集。

AVX引入了16個256位寄存器(YMM0至YMM15),AVX的256位寄存器和SSE的128位寄存器存在着相互重疊的關係(XMM寄存器爲YMM寄存器的低位),所以最好不要混用AVX與SSE指令集,否在會導致transition penalty(過渡處罰)。

這裏主要講AVX和SSE這兩種指令集。
AVX與SSE支持的數據類型如下:
在這裏插入圖片描述

2.如何使用SIMD

使用SIMD總共有下面五種方法,但是這裏主要學習使用**內置函數(intrinsics)**的方法。
1.最簡單的方法是使用Intel開發的跨平臺函數庫(IPP,Intel Integrated Performance Primitives ),裏面的函數實現都使用了SIMD指令進行優化。
2.藉助於Auto-vectorization(自動矢量化),藉助編譯器將標量操作轉化爲矢量操作。
3.使用編譯器指示符(compiler directive),如Cilk裏的#pragma simd和OpenMP裏的#pragma omp simd。

void add_floats(float * a,float * b,float * c,float * d,float * e,int n)
{
    int i;
#pragma simd
    for(i = 0; i <n; i ++{
        a [i] = a [i] + b [i] + c [i] + d [i] + e [i];
    }
}

4.使用內置函數(intrinsics)的方式,如下所示,使用SSE _mm_add_ps 內置函數,一次執行8個單精度浮點數的加法:

int  main()
{
	__m128 v0 = _mm_set_ps(1.0f, 2.0f, 3.0f, 4.0f);
	__m128 v1 = _mm_set_ps(1.0f, 2.0f, 3.0f, 4.0f);

	__m128 result = _mm_add_ps(v0, v1);
}

5.是使用匯編直接操作寄存器,直接使用匯編又難又麻煩。

3.SSE/AVX Intrinsics簡介

1.頭文件
SSE/AVX指令主要定義於以下一些頭文件中:
<xmmintrin.h> : SSE, 支持同時對4個32位單精度浮點數的操作。
<emmintrin.h> : SSE 2, 支持同時對2個64位雙精度浮點數的操作。
<pmmintrin.h> : SSE 3, 支持對SIMD寄存器的水平操作(horizontal operation),如hadd, hsub等…。
<tmmintrin.h> : SSSE 3, 增加了額外的instructions。
<smmintrin.h> : SSE 4.1, 支持點乘以及更多的整形操作。
<nmmintrin.h> : SSE 4.2, 增加了額外的instructions。(這個支持之前所有版本的SEE)
<immintrin.h> : AVX, 支持同時操作8個單精度浮點數或4個雙精度浮點數。

2.命名規則(很重要)
SSE/AVX提供的數據類型和函數的命名規則如下:
a.數據類型通常以_mxxx(T)的方式進行命名,其中xxx代表數據的位數,如SSE提供的__m128爲128位,AVX提供的__m256爲256位。T爲類型,若爲單精度浮點型則省略,若爲整形則爲i,如__m128i,若爲雙精度浮點型則爲d,如__m256d。
b.操作浮點數的內置函數命名方式爲:_mm(xxx)_name_PT。 xxx爲SIMD寄存器的位數,若爲128m則省略,如_mm_addsub_ps,若爲_256m則爲256,如_mm256_add_ps。 name爲函數執行的操作的名字,如加法爲_mm_add_ps,減法爲_mm_sub_ps。 P代表的是對矢量(packed data vector)還是對標量(scalar)進行操作,如_mm_add_ss是隻對最低位的32位浮點數執行加法,而_mm_add_ps則是對4個32位浮點數執行加法操作。 T代表浮點數的類型,若爲s則爲單精度浮點型,若爲d則爲雙精度浮點,如_mm_add_pd和_mm_add_ps。
c.操作整形的內置函數命名方式爲:_mm(xxx)_name_epUY。xxx爲SIMD寄存器的位數,若爲128位則省略。 name爲函數的名字。U爲整數的類型,若爲無符號類型則爲u,否在爲i,如_mm_adds_epu16和_mm_adds_epi16。Y爲操作的數據類型的位數,如_mm_cvtpd_pi32。

3.內置函數(instructions)
1).存取操作(load/store/set)

    __attribute__((aligned(32))) int d1[8] = {-1,-2,-3,-4,-5,-6,-7,-8};
    __m256i d = _mm256_load_si256((__m256i*)d1);//裝在int可以使用指針類型轉換 必須32位對齊
這裏說明一下,使用load函數要保證數組的起始地址32位字節對齊。在linux下就需要__attribute__((aligned(32))),Windows下要用__declspec(align(32))

這裏有沒有疑問,爲什麼要字節對齊呢?
現代計算機中內存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定類型變量的時候經常在特定的內存地址訪問,這就需要各種類型數據按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。
對齊的作用和原因:各個硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數據只能從某些特定地址開始存取。比如有些架構的CPU在訪問一個沒有進行對齊的變量的時候會發生錯誤,那麼在這種架構下編程必須保證字節對齊.其他平臺可能沒有這種情況,但是最常見的是如果不按照適合其平臺要求對數據存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設爲32位系統)如果存放在偶地址開始的地方,那麼一個讀週期就可以讀出這32bit,而如果存放在奇地址開始的地方,就需要2個讀週期,並對兩次讀出的結果的高低字節進行拼湊才能得到該32bit數據。顯然在讀取效率上下降很多。

上面是從手冊查詢到的load系列的函數。其中,
_mm_load_ss用於scalar的加載,所以,加載一個單精度浮點數到暫存器的低字節,其它三個字節清0,(r0 := *p, r1 := r2 := r3 := 0.0)。
_mm_load_ps用於packed的加載(下面的都是用於packed的),要求p的地址是16字節對齊,否則讀取的結果會出錯,(r0 := p[0], r1 := p[1], r2 := p[2], r3 := p[3])。
_mm_load1_ps表示將p地址的值,加載到暫存器的四個字節,需要多條指令完成,所以,從性能考慮,在內層循環不要使用這類指令。(r0 := r1 := r2 := r3 := *p)。
_mm_loadh_pi和_mm_loadl_pi分別用於從兩個參數高底字節等組合加載。具體參考手冊。
_mm_loadr_ps表示以_mm_load_ps反向的順序加載,需要多條指令完成,當然,也要求地址是16字節對齊。(r0 := p[3], r1 := p[2], r2 := p[1], r3 := p[0])。
_mm_loadu_ps和_mm_load_ps一樣的加載,但是不要求地址是16字節對齊,對應指令爲movups。

store系列可以將SSE/AVX提供的類型中的數據存儲到內存中,如:

void test() 
{
	__declspec(align(16)) float p[] = { 1.0f, 2.0f, 3.0f, 4.0f };
	__m128 v = _mm_load_ps(p);

	__declspec(align(16)) float a[] = { 1.0f, 2.0f, 3.0f, 4.0f };
	_mm_store_ps(a, v);
}

_mm_store_ps可以__m128中的數據存儲到16字節對齊的內存。
_mm_storeu_ps不要求存儲的內存對齊。
_mm_store_ps1則是把__m128中最低位的浮點數存儲爲4個相同的連續的浮點數,即:p[0] = m[0], p[1] = m[0], p[2] = m[0], p[3] = m[0]。
_mm_store_ss是存儲__m128中最低位的位浮點數到內存中。
_mm_storer_ps是按相反順序存儲__m128中的4個浮點數。

set系列可以直接設置SSE/AVX提供的類型中的數據,如:

__m128 v = _mm_set_ps(0.5f, 0.2f, 0.3f, 0.4f);

_mm_set_ps可以將4個32位浮點數按相反順序賦值給__m128中的4個浮點數,即:_mm_set_ps(a, b, c, d) : m[0] = d, m[1] = c, m[2] = b, m[3] = a。
_mm_set_ps1則是將一個浮點數賦值給__m128中的四個浮點數。
_mm_set_ss是將給定的浮點數設置到__m128中的最低位浮點數中,並將高三位的浮點數設置爲0.
_mm_setzero_ps是將__m128中的四個浮點數全部設置爲0.

2). 算術運算
SSE/AVX提供的算術運算操作包括:
_mm_add_ps,_mm_add_ss 等加法系列
_mm_sub_ps,_mm_sub_pd 等減法系列
_mm_mul_ps,_mm_mul_epi32 等乘法系列
_mm_div_ps,_mm_div_ss 等除法系列
_mm_sqrt_pd,_mm_rsqrt_ps 等開平方系列
_mm_rcp_ps,_mm_rcp_ss 等求倒數系列
_mm_dp_pd,_mm_dp_ps 計算點乘
此外還有向下取整,向上取整等運算,這裏只列出了浮點數支持的算術運算類型,還有一些整形的算術運算類型未列出。

3).比較運算
SSE/AVX提供的比較運算操作包括:
_mm_max_ps逐分量對比兩個數據,並將較大的分量存儲到返回類型的對應位置中。
_mm_min_ps逐分量對比兩個數據,並將較小的分量存儲到返回類型的對應位置中。
_mm_cmpeq_ps逐分量對比兩個數據是否相等。
_mm_cmpge_ps逐分量對比一個數據是否大於等於另一個是否相等。
_mm_cmpgt_ps逐分量對比一個數據是否大於另一個是否相等。
_mm_cmple_ps逐分量對比一個數據是否小於等於另一個是否相等。
_mm_cmplt_ps逐分量對比一個數據是否小於另一個是否相等。
_mm_cmpneq_ps逐分量對比一個數據是否不等於另一個是否相等。
_mm_cmpnge_ps逐分量對比一個數據是否不大於等於另一個是否相等。
_mm_cmpngt_ps逐分量對比一個數據是否不大於另一個是否相等。
_mm_cmpnle_ps逐分量對比一個數據是否不小於等於另一個是否相等。
_mm_cmpnlt_ps逐分量對比一個數據是否不小於另一個是否相等。

4).邏輯運算
SSE/AVX提供的邏輯運算操作包括:
_mm_and_pd對兩個數據逐分量and
_mm_andnot_ps先對第一個數進行not,然後再對兩個數據進行逐分量and
_mm_or_pd對兩個數據逐分量or
_mm_xor_ps對兩個數據逐分量xor

詳情可查Intel的Intrinsics Guide

發佈了2 篇原創文章 · 獲贊 2 · 訪問量 84
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章