SSE指令集學習:Compiler Intrinsic

大多數的函數是在庫中,Intrinsic Function卻內嵌在編譯器中(built in to the compiler)。html

1. Intrinsic Function

Intrinsic Function做爲內聯函數,直接在調用的地方插入代碼,即避免了函數調用的額外開銷,又可以使用比較高效的機器指令對該函數進行優化。優化器(Optimizer)內置的一些Intrinsic Function行爲信息,能夠對Intrinsic進行一些不適用於內聯彙編的優化,因此一般來講Intrinsic Function要比等效的內聯彙編(inline assembly)代碼快。優化器可以根據不一樣的上下文環境對Intrinsic Function進行調整,例如:以不一樣的指令展開Intrinsic Function,將buffer存放在合適的寄存器等。
使用 Intrinsic Function對代碼的移植性會有必定的影響,這是因爲有些Intrinsic Function只適用於Visual C++,在其餘編譯器上是不適用的;更有些Intrinsic Function面向的是特定的CPU架構,不是全平臺通用的。上面提到的這些因素對使用Intrinsic Function代碼的移植性有一些很差的影響,可是和內聯彙編相比,移植含有Intrinsic Function的代碼無疑是方便了不少。另外,64位平臺已經再也不支持內聯彙編。算法

2. SSE Intrinsic

VS和GCC都支持SSE指令的Intrinsic,SSE有多個不一樣的版本,其對應的Intrinsic也包含在不一樣的頭文件中,若是肯定只使用某個版本的SSE指令則只包含相應的頭文件便可。數組


引用自:http://www.cnblogs.com/zyl910/archive/2012/02/28/vs_intrin_table.html架構

例如,要使用SSE3,則函數

#include <tmmintrin.h>

若是不關心使用那個版本的SSE指令,則能夠包含全部學習

#include <intrin.h>

2.1 數據類型

Intrinsic使用的數據類型和其寄存器是想對應,有優化

  • 64位 MMX指令集使用
  • 128位 SSE指令集使用
  • 256位 AVX指令集使用

甚至AVX-512指令集有512位的寄存器,那麼相對應Intrinsic的數據也就有512位。
具體的數據類型及其說明以下:ui

  1. **__m64** 64位對應的數據類型,該類型僅能供MMX指令集使用。因爲MMX指令集也能使用SSE指令集的128位寄存器,故該數據類型使用的狀況較少。
  2. **__m128 / __m128i / __m128d** 這三種數據類型都是128位的數據類型。因爲SSE指令集即能操做整型,又能操做浮點型(單精度和雙精度),這三種數據類型根據所帶後綴的不一樣表明不一樣類型的操做數。__m128是單精度浮點數,__m128i是整型,__m128d是雙精度浮點數。

256和512的數據類型和128位的相似,只是存放的個數不一樣,這裏再也不贅述。
知道了各類數據類型的長度以及其代碼的意義,那麼它的表現形式究竟是怎麼樣的呢?看下圖
調試

__m128i yy;

yy是__m128i型,從上圖能夠看出__m128i是一個聯合體(union),根據不一樣成員包含不一樣的數據類型。看其具體的成員包含了8位、16位、32位和64位的有符號/無符號整數(這裏__m128i是整型,故只有整型的成員,浮點數的使用__m128)。而每一個成員都是一個數組,數組中填充着相應的數據,而且根據數據長度的不一樣數組的長度也不一樣(數組長度 = 128 / 每一個數據的長度(位))。在使用的時候必定要特別的注意要操做數據的類型,也就是數據的長度,例如上圖同一個變量yy看成4個32位有符號整型使用時其數據是:0,0,1024,1024;可是當作64位有符號整型時其數據爲:0,4398046512128,大大的不一樣。
在MSVC下可使用yy.m128i_i32[0]取出第一個32位整型數據,原生的Intrinsic函數是沒有提供該功能的,這是在MSVC的擴展,比較像Microsoft的風格,使用及其的方便可是效率不好,因此這種方法在GCC/Clang下面是不可用的。在MSVC下面能夠根據須要使用不使用這種抽取數據的方法,可是這種功能在調試代碼時是很是方便的,如上圖能夠很容易的看出128位的數據在不一樣數據類型下其值的不一樣。code

2.2 Intrinsic 函數的命名

Intrinsic函數的命名也是有必定的規律的,一個Intrinsic一般由3部分構成,這個三個部分的具體含義以下:

  1. 第一部分爲前綴_mm,表示是SSE指令集對應的Intrinsic函數。_mm256或_mm512是AVX,AVX-512指令集的Intrinsic函數前綴,這裏只討論SSE故略去不做說明。
  2. 第二部分爲對應的指令的操做,如_add,_mul,_load等,有些操做可能會有修飾符,如loadu將未16位對齊的操做數加載到寄存器中。
  3. 第三部分爲操做的對象名及數據類型,_ps packed操做全部的單精度浮點數;_pd packed操做全部的雙精度浮點數;_pixx(xx爲長度,能夠是8,16,32,64)packed操做全部的xx位有符號整數,使用的寄存器長度爲64位;_epixx(xx爲長度)packed操做全部的xx位的有符號整數,使用的寄存器長度爲128位;_epuxx packed操做全部的xx位的無符號整數;_ss操做第一個單精度浮點數。....

將這三部分組合到以其就是一個完整的Intrinsic函數,如_mm_mul_epi32 對參數中全部的32位有符號整數進行乘法運算。

SSE指令集對分支處理能力很是的差,並且從128位的數據中提取某些元素數據的代價又很是的大,所以不適合有複雜邏輯的運算。

3. Intrinsic版雙線性插值

在上一篇文章SSE指令集優化學習:雙線性插值 使用SSE彙編指令對雙線性插值算法進行了優化,這裏將其改爲爲Intrinsic版的。

3.1 計算 (y * width + x) * depth

目的像素須要其映射到源像素周圍最近的4個像素插值獲得,這裏同時計算源像素的最近的4個像素值的偏移量。

__m128i wwidth = _mm_set_epi32(0, width, 0, width);
                __m128i yy = _mm_set_epi32(0, y2, 0, y1);
                yy = _mm_mul_epi32(yy, wwidth);  //y1 * width 0 y2 *width 0
                yy = _mm_shuffle_epi32(yy, 0xd8); // y1 * width y2 * width 0 0        
                yy = _mm_unpacklo_epi32(yy, yy); // y1 * width y2 * width y1 * width y2 * width
                yy = _mm_shuffle_epi32(yy, _MM_SHUFFLE(3, 1, 2, 0));
                __m128i xx = _mm_set_epi32(x2, x2, x1, x1);
                xx = _mm_add_epi32(xx, yy); // (x1,y1) (x1,y2) (x2,y1) (x2,y2)
                __m128i x1x1 = _mm_shuffle_epi32(xx, 0x50); // (x1,y1) (x1,y2)
                __m128i x2x2 = _mm_shuffle_epi32(xx, 0xfa); // (x2,y1) (x2,y2)
  1. 使用set函數將須要的數據填充到__m128Intel中
  2. mul函數進行乘法運算,兩個32位的整型相乘的結果是一個64位整型。
  3. 因爲計算的是像素的偏移量,使用32位整型也就足夠了,使用shffule對__m128i中的數據進行從新排列,使用unpack函數再從新組合,將數據組合爲須要的結構。
  4. _MM_SHUFFLE是一個宏,可以方便的生成shuffle中所須要的當即數。例如
_mm_shuffle_epi32(yy,_MM_SHUFFLE(3,1,2,0);

將yy中存放的第2和第3個32位整數交換順序。

3.2 數據類型的轉換

SSE彙編指令和其Intrinsic函數之間基本存在這一一對應的關係,有了彙編的實現再改成Intrinsic是挺簡單的,再在這羅列代碼也乜嘢什麼意義了。這裏就記錄下使用的過程當中遇到的最大的問題:數據類型之間的轉換
作圖像處理,因爲像素通道值是8位的無符號整數,而與其運算的每每又是浮點數,這就須要將8位無符號整數轉換爲浮點數;運算完畢後,獲得的結果又要寫回圖像通道,就要是8位無符號整數,還要涉及到超出8位的截斷。開始不注意時吃了大虧....
類型轉換主要如下幾種:

  1. 浮點數和整數的轉換及32位浮點數和64位浮點數之間的轉換。 這種轉換簡單直接,只須要調用相應的函數指令便可。
  2. 有符號整數的高位擴展將8位、16位、32位有符號整數擴展爲16位、32位、64位。
  3. 有符號整數的截斷 將16位、32位、64位有符號壓縮
  4. 無符號整數到有符號整數的擴展
    在Intrinsic函數中 上述類型轉換的格式
  • _mm_cvtepixx_epixx (xx是位數8/16/32/64)這是有符號整數之間的轉換
  • _mm_cvtepixx_ps / _mm_cvtepixx_pd 整數到單精度/雙精度浮點數之間的轉換
  • _mm_cvtepuxx_epixx 無符號整數向有符號整數的擴展,採用高位0擴展的方式,這些函數是對無符號高位0擴展變成相應位數的有符號整數。沒有32位無符號整數轉換爲16位有符號整數這樣的操做。
  • _mm_cvtepuxx_ps / _mm_cvtepuxx_pd 無符號整數轉換爲單精度/雙精度浮點數。

上面的數據轉換還少了一種,整數的飽和轉換。什麼是飽和轉換呢,超過的最大值的以最大值來計算,例如8位無符號整數最大值爲255,則轉換爲8位無符號時超過255的值視爲255。
整數的飽和轉換有兩種:

  • 有符號之間的 SSE的Intrinsic函數提供了兩種
__m128i _mm_packs_epi32(__m128i a, __m128i b)
__m128i _mm_packs_epi16(__m128i a , __m128i b)

用於將16/32位的有符號整數飽和轉換爲8/16位有符號整數。

  • 有符號到無符號之間的
__m128i _mm_packus_epi32(__m128i a, __m128i b)
__m128i _mm_packus_epi16(__m128i a , __m128i b)

用於將16/32位的有符號整數飽和轉換爲8/16位無符號整數

4. SSE彙編指令和Intrinsic函數的對比

這裏只是作了一個粗略的對比,畢竟還只是個初學者。先說結果吧,在Debug下使用純彙編的SSE代碼會快很多,應該是因爲沒有編譯器的優化,彙編代碼的效率仍是有很大的優點的。可是在Release下面,前面也有提到過優化器內置了Intrinsic函數的行爲信息,可以對Intrinsic函數提供很強大的優化,二者沒有什麼差異。PS:應該是因爲選用數據的問題 ,普通的C++代碼,SSE彙編代碼以及Intrinsic函數三者在Release下的速度相差無幾,編譯器自己的優化功能是很強大的。

4.1 Intrinsic 函數進行屢次內存讀寫操做

在對比時發現使用Intrinsic函數另外一個問題,就是數據的存取。使用SSE彙編時,能夠將中間的計算結果保存到xmm寄存器中,在使用的時候直接取出便可。Intrinsic函數不能操做xmm寄存器,也就不能如此操做,它須要將每次的計算結果寫回內存中,使用的時候再次讀取到xmm寄存器中。

yy = _mm_mul_epi32(yy, wwidth);

上述代碼是進行32位有符號整數乘法運算,計算的結果保存在yy中,反彙編後其對應的彙編代碼:

000B0428  movaps      xmm0,xmmword ptr [ebp-1B0h] 
000B042F  pmuldq      xmm0,xmmword ptr [ebp-190h] 
000B0438  movaps      xmmword ptr [ebp-7A0h],xmm0 
000B043F  movaps      xmm0,xmmword ptr [ebp-7A0h] 
000B0446  movaps      xmmword ptr [ebp-1B0h],xmm0

上述彙編代碼中有屢次的movaps操做。而上述操做在使用匯編時只需一條指令

pmuludq xmm0, xmm1;

在使用Intrinsic函數時,每個函數至少要進行一次內存的讀取,將操做數從內存讀入到xmm寄存器;一次內存的寫操做,將計算結果從xmm寄存器寫回內存,也就是保存到變量中去。因而可知,在只有很簡單的計算中(例如:同時進行4個32位浮點數的乘法運算)和使用SSE彙編指令不會有很大的差異,可是若是邏輯稍微複雜些或者調用的Intrinsic函數較多,就會有不少的內存讀寫操做,這在效率上仍是有一部分損失的。

4.2 簡單運算的Intrinsic和SSE指令的對比

一個比較極端的例子,未通過優化的C++代碼以下:

_MM_ALIGN16 float a[] = { 1.0f,2.0f,3.0f,4.0f };
    _MM_ALIGN16 float b[] = { 5.0f,6.0f,7.0f,8.0f };
    const int count = 1000000000;

    float c[4] = { 0,0,0,0 };
    cout << "Normal Time(ms):";
    double tStart = static_cast<double>(clock());
    for (int i = 0; i < count; i++)
        for (int j = 0; j < 4; j++)
            c[j] = a[j] + b[j];
    double tEnd = static_cast<double>(clock());

對兩個有4個單精度浮點數的數組作屢次加法運算,而且這種加法是重複進行,進行1次和進行1000次的結果是相同的。使用SSE彙編指令的代碼以下:

for(int i = 0; i < count; i ++)
        _asm
        {
            movaps xmm0, [a];
            movaps xmm1, [b];
            addps xmm0, xmm1;
        }

使用Intrinsic函數的代碼:

__m128 a1, b2;
    __m128 c1;
    for (int i = 0; i < count; i++)
    {
        a1 = _mm_load_ps(a);
        b2 = _mm_load_ps(b);
        c1 = _mm_add_ps(a1, b2);
    }

在Debug下的運行

這個結果應該在乎料之中的,SSE彙編指令 < Intrinsic函數 < C++。SSE彙編指令比Intrinsic函數快了近1/3,下面是Intrinsic函數的反彙編代碼

a1 = _mm_load_ps(a);
00FB2570  movaps      xmm0,xmmword ptr [a] 
00FB2574  movaps      xmmword ptr [ebp-220h],xmm0 
00FB257B  movaps      xmm0,xmmword ptr [ebp-220h] 
00FB2582  movaps      xmmword ptr [a1],xmm0 
        b2 = _mm_load_ps(b);
00FB2586  movaps      xmm0,xmmword ptr [b] 
00FB258A  movaps      xmmword ptr [ebp-240h],xmm0 
00FB2591  movaps      xmm0,xmmword ptr [ebp-240h] 
00FB2598  movaps      xmmword ptr [b2],xmm0 
        c1 = _mm_add_ps(a1, b2);
00FB259F  movaps      xmm0,xmmword ptr [a1] 
00FB25A3  addps       xmm0,xmmword ptr [b2] 
00FB25AA  movaps      xmmword ptr [ebp-260h],xmm0 
00FB25B1  movaps      xmm0,xmmword ptr [ebp-260h] 
00FB25B8  movaps      xmmword ptr [c1],xmm0

能夠看到共有12個movaps指令和1個addps指令。而SSE的彙編代碼只有2個movaps指令和1個addps指令,可見其時間的差異應該主要是因爲Intrinsic的內存讀寫形成的。
Debug下面的結果是沒有出意料以外的,那麼Release下的結果則真是出乎意料的

使用SSE彙編的最慢,C++實現都比起快很好,可見編譯器的優化仍是很是給力的。而Intrinsic的時間則是0,是怎麼回事。查看反彙編的代碼發現,那個加法只執行了一次,而不是執行了不少次。應該是優化器根據Intrinsic行爲作了預測,後面的屢次循環都是無心義的(一同窗告訴個人,他是作編譯器生成代碼優化的,作的是分支預測,不過也是在實現中,不知道他說的對不對)。

5. 總結

學習SSE指令將近兩個周了,作了兩篇學習筆記,差很少也算入門了吧。這段時間的學習總結以下:

  1. SSE指令集正如其名字 Streaming SIMD Extensions,最強大的是其可以在一條指令並行的對多個操做數進行相同的運算,根據操做數長度和寄存器長度的不一樣可以同時運算的個數也不一樣。以32位有符號整數爲例,128位寄存器(也是最經常使用的SSE指令集的寄存器)可以同時運算4個;AVX指令集的256位寄存器可以同時運算8個;AVX-512 的512位寄存器可以同時運算16個。
  2. 在使用SSE指令時要特別主要操做數的類型,整型則要區分是有符號仍是無符號;浮點數則注意其精度是單精度仍是雙精度。另外就是操做數的長度。即便是一樣的128位二進制串,根據其類型和長度也有多種不一樣的解釋。
  3. 前面屢次提到,編譯器的優化能力是很強的,不要刻意的使用SSE指令優化。而在要必須使用SSE的時候,要謹記SSE的強大之處是其並行能力。

又是一個陽光明媚的週五下午,說好的今天要下大暴雨呢,早晨都沒敢騎自行車來上班,回去的得擠公交啊。話說,爲啥不說坐公交或者乘公交,而要擠公交呢。

相關文章
相關標籤/搜索