大多數的函數是在庫中,Intrinsic Function卻內嵌在編譯器中(built in to the compiler)。html
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位平臺已經再也不支持內聯彙編。算法
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>
Intrinsic使用的數據類型和其寄存器是想對應,有優化
甚至AVX-512指令集有512位的寄存器,那麼相對應Intrinsic的數據也就有512位。
具體的數據類型及其說明以下:ui
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
Intrinsic函數的命名也是有必定的規律的,一個Intrinsic一般由3部分構成,這個三個部分的具體含義以下:
將這三部分組合到以其就是一個完整的Intrinsic函數,如_mm_mul_epi32 對參數中全部的32位有符號整數進行乘法運算。
SSE指令集對分支處理能力很是的差,並且從128位的數據中提取某些元素數據的代價又很是的大,所以不適合有複雜邏輯的運算。
在上一篇文章SSE指令集優化學習:雙線性插值 使用SSE彙編指令對雙線性插值算法進行了優化,這裏將其改爲爲Intrinsic版的。
目的像素須要其映射到源像素周圍最近的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)
_MM_SHUFFLE
是一個宏,可以方便的生成shuffle中所須要的當即數。例如_mm_shuffle_epi32(yy,_MM_SHUFFLE(3,1,2,0);
將yy中存放的第2和第3個32位整數交換順序。
SSE彙編指令和其Intrinsic函數之間基本存在這一一對應的關係,有了彙編的實現再改成Intrinsic是挺簡單的,再在這羅列代碼也乜嘢什麼意義了。這裏就記錄下使用的過程當中遇到的最大的問題:數據類型之間的轉換。
作圖像處理,因爲像素通道值是8位的無符號整數,而與其運算的每每又是浮點數,這就須要將8位無符號整數轉換爲浮點數;運算完畢後,獲得的結果又要寫回圖像通道,就要是8位無符號整數,還要涉及到超出8位的截斷。開始不注意時吃了大虧....
類型轉換主要如下幾種:
上面的數據轉換還少了一種,整數的飽和轉換。什麼是飽和轉換呢,超過的最大值的以最大值來計算,例如8位無符號整數最大值爲255,則轉換爲8位無符號時超過255的值視爲255。
整數的飽和轉換有兩種:
__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位無符號整數
這裏只是作了一個粗略的對比,畢竟還只是個初學者。先說結果吧,在Debug下使用純彙編的SSE代碼會快很多,應該是因爲沒有編譯器的優化,彙編代碼的效率仍是有很大的優點的。可是在Release下面,前面也有提到過優化器內置了Intrinsic函數的行爲信息,可以對Intrinsic函數提供很強大的優化,二者沒有什麼差異。PS:應該是因爲選用數據的問題 ,普通的C++代碼,SSE彙編代碼以及Intrinsic函數三者在Release下的速度相差無幾,編譯器自己的優化功能是很強大的。
在對比時發現使用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函數較多,就會有不少的內存讀寫操做,這在效率上仍是有一部分損失的。
一個比較極端的例子,未通過優化的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行爲作了預測,後面的屢次循環都是無心義的(一同窗告訴個人,他是作編譯器生成代碼優化的,作的是分支預測,不過也是在實現中,不知道他說的對不對)。
學習SSE指令將近兩個周了,作了兩篇學習筆記,差很少也算入門了吧。這段時間的學習總結以下:
又是一個陽光明媚的週五下午,說好的今天要下大暴雨呢,早晨都沒敢騎自行車來上班,回去的得擠公交啊。話說,爲啥不說坐公交或者乘公交,而要擠公交呢。