探討SSE指令

 

轉載自:未知

    比較一下3DNow和浮點指令的性能差異,可以看出,3DNow指令集在運算速度上要遠遠
超過浮點指令。那麼,SSE性能如何呢,它是否有能力同3DNow一拚高低?我想,很難說
那一個更好一些,因爲它們都有着很高的性能。不過單從指令集上看,SSE還是要略勝一
籌的。畢竟是新增了8個128位的寄存器,而且指令的功能也要強大一些。3DNow使用MMX
指令的寄存器,可以藉助MMX指令的強大功能,不必設計太多的新功能,不需要操作系
統提供專門的支持,而且口碑頗佳!從流水線的設計上看,雙方也是各有所長。Pentiu
m III每個時鐘週期最多可以解碼3條指令,執行5個微操作,它把一些重要的微操作(例
如乘法和加法)分派到不同的端口去執行。 3DNow則是在兩條流水線間共享3DNow的執行
單元和部分MMX的執行單元,所有的3DNow指令都是有兩個時鐘週期的延遲,並且完全被
流水線化。
最近,AMD的處理器似乎有了很大的變化,我看過一些有關它的64位處理器的資料,也是
添加了一堆寄存器,不過我沒有仔細看,畢竟沒有哪個緣份一睹芳顏。intel公司當然也
沒有閒着,它的64位處理器則不能用“變化”二字來形容了,那簡直可以說是脫胎換骨
,全新的指令,全新的體系!不過,咱們老百姓恐怕不會在短時間內用上這種處理器,
擁有三百多個寄存器的CPU肯定會處於我無法接受的價位。這樣也還是有一個好處的,那
就是SSE指令集在短時間內不會過時,畢竟,轉移到64位陣營還是要經歷一個漫長的過程
。而且,在IA-64體系中專門提供了三條指令在32位代碼和64位代碼之間進行跳轉,也就
是說,你可以在程序中任意使用兩種代碼。
所以,如果你想針對intel系列的處理器進行優化的話,就努力學好SSE吧,在相當長的
時間裏都會大有用處的。
本文不會詳細介紹每一個SSE指令,只是討論一些重要的,常用的,能夠對性能產生較大
影響的指令。如果你想更全面的瞭解SSE,請參閱 SSE指令簡明參考。你可以從中查到每
條SSE指令的功能。
通過程序來討論指令的用法是最好的辦法。以前寫過的兩篇文章,一篇是關於浮點指令
優化的,一篇是關於3DNow指令優化的,這兩篇文章都是使用了矩陣相乘作爲例子程序,
因此本文還是以矩陣相乘爲例,看一看SSE究竟有什麼優勢!它與單純使用浮點指令的程
序相比效率能提高多少!
準備工作
選擇合適的編譯器
目前我還沒有發現哪個編譯器能夠對SSE提供內聯支持,據intel聲稱,它的C++ 編譯器
可以做到,但是,恐怕沒有幾個人用過。建議大家使用MASM6.14,它支持SSE和3DNow。
大家可以從本站下載MASM6.14。高級語言的編譯器也是要有的,我使用的是VC6.0。因爲
VC在浮點程序方面比 C++ Builder優化的更好,這樣就可以與彙編的優化結果進行比較
了。
設置編譯器
在彙編程序裏,應該加入僞指令來指示編譯器支持何種指令集。“.xmm”表示要求編譯
器支持SSE指令集。“.k3d”則是要求編譯器支持3DNow指令集。
VC提供了一些支持,可以自動的編譯彙編文件,你可以按照以下步驟進行:
在菜單中選擇“Project | Setting”
選中指定的彙編文件(單擊即可)
選中Custom Build頁
在Commands中輸入:
如果是DEBUG模式,則輸入:
path e:/masm32/bin
ml /c /coff /Zi /FoDEBUG/$(InputName).obj $(InputPath)
如果是RELEASE模式,則輸入:
path e:/masm32/bin
ml /c /coff /FoRELEASE/$(InputName).obj $(InputPath)
在Outputs中輸入:
如果是DEBUG模式,則輸入:
DEBUG/$(InputName).obj
如果是RELEASE模式,則輸入:
RELEASE/$(InputName).obj
如果你的沒有把masm安裝在E盤,則要作相應的修改。
學習指令
你首先應該對SSE指令有所瞭解才能更好的閱讀本文。SSE指令集是一個比較新的體系,
如果你沒有學過MMX或者3DNow,還是有一定困難的。在全面優化Pentium III一文中對P
entium III 的體系有比較全面的闡述。
優化方針
針對SSE優化還是比較困難的,下面提出一些方法,以供參考:
擺脫高級語言的桎梏,根據硬件的特點,指令的功能,量體裁衣地設計算法。要知道,
彙編語言的算法與高級語言是有很大的不同的,只有重新設計的算法纔有可能發揮出處
理器的最大潛力。
熟練使用一些常用的指令,知道它們的延遲和吞吐量是多少。本文的例子中所用的一些
重要的指令有:ADDPS,MULPS,SHUFPS,MOVSS,MOVAPS。關於它們的執行單
元的相關數據可以查閱處理器執行單元列表。
充分利用新增加的八個寄存器,減小內存的壓力;設計並行算法,減輕流水線的延遲。

綜合考慮解碼器,流水線,執行端口等多方面因素,儘量增強處理器的並行處理的能力

舉例詳解
下面的程序是一個矩陣相乘的函數。在三維圖形空間變換中,要用到4乘4的浮點矩陣,
而矩陣相乘的運算是很常用的。下面的函數的參數都是4乘4的浮點矩陣。寫成這種形式
是爲了保持比較強的伸縮性。
void MatMul_cpp(float *dest, float *m1, float *m2)
{
     for(int i = 0; i < 4; i ++)
     {
         for(int j = 0; j < 4; j ++)
         {
             dest[i*4+j] =
                 m1[i*4+0]*m2[0*4+j] +
                 m1[i*4+1]*m2[1*4+j] +
                 m1[i*4+2]*m2[2*4+j] +
                 m1[i*4+3]*m2[3*4+j] ;
         }
     }
}
VC的優化能力是很強的,象上面這樣的比較常規的算法,你很難做出比它快得多的代碼
。不過使用SSE以後就不一樣了。下面是一個彙編函數,使用SSE 指令進行計算。注意,
這個函數只能運行於32位的環境中。“.xmm”指示編譯器使用SSE指令集進行編譯。
函數的C語言原型是這樣的:
extern "C"
{
void __stdcall MatMul_xmm(float *dest, float *m1, float *m2);
}
對於一些不太常用彙編語言編程的朋友來說,下面的程序可能比較難於理解。我將對一
些常識性的東西做一下簡單介紹。
在C語言中,代碼段都是以“_TEXT”作爲段名的。“use32”告訴編譯器將代碼編譯爲3
2位。
有些人看到“_MatMul_xmm@12”這個函數名以後可能會產生疑問。其實這只是遵循了VC
所採用的命名規範。在VC中,所有標誌爲“__stdcall” 調用的,採用“C”鏈接的函數
都要加下劃線作爲前綴,並且加上“@N”作爲後綴,其中,“N”爲參數的字節數。注意
,上面的函數是採用“C”鏈接的,如果是“C++”鏈接,命名規範就太複雜了。如果你
使用的是C++ Builder,命名規範就十分簡單了,照搬函數名就行了。不同的調用規範將
採用不同的命名方法,即使對相同的調用規範,不同的編譯器也不一定兼容。有一種調
用格式是每一個C++編譯器都支持並且兼容的,那就是“__cdecl”。
各種調用格式所採用的堆棧操作也不太一樣。使用“__stdcall”時,參數從右向左依次
入棧,參數的彈出需要函數自己來處理。這種做法和“__cdecl” 調用方式不太一樣,
“__cdecl”的參數彈出需要調用者來處理。現在很流行的一種調用格式是“__fastcal
l”,也就是寄存器調用。這種調用方式通過寄存器“EAX”,“ECX”,“EDX”傳遞參
數,不過很可惜,這種調用也不是在各個編譯器中兼容的。Inprise在C++ Builder中提
供了一個關鍵字“__msfastcall” 用來和微軟兼容,如果你採用這種調用規範就可以在
多個編譯器中正常調用了。不過還有一件事讓人很受打擊,VC沒有對“__fastcall”提
供很好的優化,使用這種調用反而會降低效率。
並不是所有的寄存器都能夠隨意使用的,多數32位寄存器都要先保存的。你可以不必保
存的32位寄存器只有三個----“EAX”,“ECX”,“EDX”,其它的就只好“PUSH”,“
POP”了。另外,浮點堆棧寄存器是不必保存的;MMX 寄存器和浮點堆棧共享,也是不必
保存的;XMM寄存器不必保存。
很多SSE指令都會加上“ps”或“ss”後綴。“ps”表示“Packed Single-FP”,即打包
的浮點數,帶這種後綴的指令通常是一次性對四個數進行操作的。“ss” 表示“Scala
r Single-FP”,帶這種後綴的指令通常是對最低位的單精度數進行操作的。
下面這個彙編函數是一行一行計算的,咱們先用類似於C的語法簡述一下第一行的計算過
程:
     xmm0 = m1[0],m1[0],m1[0],m1[0];
     xmm1 = m1[1],m1[1],m1[1],m1[1];
     xmm2 = m1[2],m1[2],m1[2],m1[2];
     xmm3 = m1[3],m1[3],m1[3],m1[3];
     xmm4 = m2[0],m2[1],m2[2],m2[3];
     xmm5 = m2[4],m2[5],m2[6],m2[7];
     xmm6 = m2[8],m2[9],m2[10],m2[11];
     xmm7 = m2[12],m2[13],m2[14],m2[15];
     xmm0 *= xmm4;
     xmm1 *= xmm5;
     xmm2 *= xmm6;
     xmm3 *= xmm7;
     xmm1 += xmm0;
     xmm2 += xmm1;
     xmm3 += xmm2;
     dst[0],dst[1],dst[2],dst[3] = xmm3;
上面的代碼可讀性還是比較好的,因爲只進行了第一行的計算。實際運算中,爲了增強
並行度,爲了減小指令的延遲,實際上是兩行並行計算的。而且,運算過程並不是象算
法描述那樣寫得那麼有規律。
         .686p
         .xmm
         .model flat
_TEXT segment public use32 'CODE'
public _MatMul_xmm@12
_MatMul_xmm@12 proc
;;parameters
retaddress = 0
dst = retaddress+4
m1 = dst+4
m2 = m1+4
         mov           edx,      [esp+m1]
         mov           ecx,      [esp+m2]
         mov           eax,      [esp+dst]
         movss         xmm0,     [edx+16*0+4*0]    ;讀入第一行的數據
         movaps        xmm4,     [ecx+16*0]
         movss         xmm1,     [edx+16*0+4*1]
         shufps        xmm0,     xmm0,     00h
         movaps        xmm5,     [ecx+16*1]
         movss         xmm2,     [edx+16*0+4*2]
         shufps        xmm1,     xmm1,     00h
         mulps         xmm0,     xmm4
         movaps        xmm6,     [ecx+16*2]
         mulps         xmm1,     xmm5
         movss         xmm3,     [edx+16*0+4*3]
         shufps        xmm2,     xmm2,     00h
         movaps        xmm7,     [ecx+16*3]
         shufps        xmm3,     xmm3,     00h
         mulps         xmm2,     xmm6
         addps         xmm1,     xmm0
         movss         xmm0,     [edx+16*1+4*0]    ;讀入第二行的數據
         mulps         xmm3,     xmm7
         shufps        xmm0,     xmm0,     00h
         addps         xmm2,     xmm1
         movss         xmm1,     [edx+16*1+4*1]
         mulps         xmm0,     xmm4
         shufps        xmm1,     xmm1,     00h
         addps         xmm3,     xmm2
         movss         xmm2,     [edx+16*1+4*2]
         mulps         xmm1,     xmm5
         shufps        xmm2,     xmm2,     00h
         movaps        [eax+16*0],     xmm3
         movss         xmm3,     [edx+16*1+4*3]
         mulps         xmm2,     xmm6
         shufps        xmm3,     xmm3,     00h
         addps         xmm1,     xmm0
         movss         xmm0,     [edx+16*2+4*0]    ;讀入第三行的數據
         mulps         xmm3,     xmm7
         shufps        xmm0,     xmm0,     00h
         addps         xmm2,     xmm1
         movss         xmm1,     [edx+16*2+4*1]
         mulps         xmm0,     xmm4
         shufps        xmm1,     xmm1,     00h
         addps         xmm3,     xmm2
         movss         xmm2,     [edx+16*2+4*2]
         mulps         xmm1,     xmm5
         shufps        xmm2,     xmm2,     00h
         movaps        [eax+16*1],     xmm3
         movss         xmm3,     [edx+16*2+4*3]
         mulps         xmm2,     xmm6
         shufps        xmm3,     xmm3,     00h
         addps         xmm1,     xmm0
         movss         xmm0,     [edx+16*3+4*0]    ;讀入第四行的數據
         mulps         xmm3,     xmm7
         shufps        xmm0,     xmm0,     00h
         addps         xmm2,     xmm1
         movss         xmm1,     [edx+16*3+4*1]
         mulps         xmm0,     xmm4
         shufps        xmm1,     xmm1,     00h
         addps         xmm3,     xmm2
         movss         xmm2,     [edx+16*3+4*2]
         mulps         xmm1,     xmm5
         shufps        xmm2,     xmm2,     00h
         movaps        [eax+16*2],     xmm3
         movss         xmm3,     [edx+16*3+4*3]
         mulps         xmm2,     xmm6
         shufps        xmm3,     xmm3,     00h
         addps         xmm1,     xmm0
         mulps         xmm3,     xmm7
         addps         xmm2,     xmm1
         addps         xmm3,     xmm2
         movaps        [eax+16*3],     xmm3
         ret           12
_MatMul_xmm@12 endp
_TEXT ends
         end
上面的代碼幾乎沒有加什麼註釋,只是在讀入每行第一個數據時作了標記。因爲,SSE
的指令可讀性還是比較好的,除了要加上一些後綴以外,它們和普通的整數運算指令很
相似。
一些關鍵性的指令有必要解釋一下:
movss和movaps:
movss是將一個單精度數傳輸到xmm寄存器的低32位,而movaps則是一次性向寄存器中寫
入四個單精度數。也許有些人會認爲movaps效率更高一些,其實並不一定是這樣。從處
理器執行單元列表中,你可以查到這些指令的延遲。如果都是從寄存器中讀取數據,兩
個指令的延遲是一樣的。如果是從內存中讀取數據,movss只有一個時鐘週期的延遲,而
movaps卻有四個時鐘週期的延遲。
上面的彙編代碼混合使用了這兩條指令。那麼,應該在什麼時候選擇哪一條指令呢?這
要看你對數據的需求了。如果你希望能夠儘快地使用數據,就應當首選movss,因爲它幾
乎能夠讓你立即使用數據。如果你並不急於使用某些數據,只是想先把它讀入寄存器,
那麼毫無疑問movaps是你的最佳選擇。 movaps使用端口2讀取數據,如果在它執行完畢
之前你不去使用它的數據,這條指令的實際延遲就只有一個時鐘週期。考慮到處理器能
夠在5個端口並行執行微操作,那麼這條指令的延遲可能還不到一個時鐘週期。
從上面的代碼中,你可以看到,每一條movaps指令和它的相關指令之間都至少插入了四
條指令,這樣可以基本上避免延遲。
雖然movss指令只有一個時鐘週期的延遲,但是這也並不意味着你可以把這條指令和它的
相關指令寫在一起,因爲這有可能會影響處理器的並行度。雖然 Pentium III有着強大
的亂序執行的能力,可是這畢竟是不太保險的,還是自己動手,豐衣足食吧。
SHUFPS
這是一條可以將操作數打亂順序的指令。這一條指令有很多種用法,它根據常量參數的
不同執行不同的功能。本文中只使用了一種用法:
     shufps       xmmreg,   xmmreg,   00h
這條指令的作用是把某個寄存器的最低位的單精度數傳輸到該寄存器的其它三個部分。

在某些時候,shufps和unpcklps(或unpckhps)可以執行相同的功能。這時,推薦使用
shufps,因爲這條指令有兩個時鐘週期的延遲。unpcklps和unpckhps 都是有三個時鐘周
期的延遲。
ADDPS和MULPS
這兩條指令是很重要的計算指令,有必要弄清楚它們的執行情況。
addps有4個時鐘週期的延遲,mulps有5個時鐘週期的延遲,我們應該根據這些數據考慮
清楚,究竟在它們的相關代碼中插入多少條指令。
這兩條指令都是每兩個時鐘週期才允許執行一次,如果你把相同的兩條這樣的指令寫在
一起,第二條指令就有可能被延誤一個時鐘週期。應該插入一些其它指令來掩蓋這段延
遲。
mulps在端口0執行,addps在端口1執行,如果你的代碼把乘法和加法指令寫在一起,它
們會被分配到不同的端口並行執行,這比只有一條流水線的FPU要高效的多。
優化思路:
下面將解釋一下上面代碼的優化思路。
打亂指令
在算法描述中,各條操作寫得非常有規律,但是在真正編程的時候卻不是這樣。爲了保
證流水線的流暢運作,就要把相關的代碼分離開來,儘量避免或減輕指令的延遲。這樣
就要打亂指令,在兩條相關指令之間插入一些其它的指令,同時也要考慮指令之間是否
存在資源的競爭。
並行算法
多個數據並行計算是解決指令延遲問題的有效方法。我們不能傻傻地等待一條指令的計
算結果,而是要在等待的過程中進行其它數據的計算。在上面程序的算法中,每當寄存
器有了空閒,就馬上從內存中讀入新的數據,儘量保證有兩組數據在寄存器中並行計算

內存訪問
訪問內存的指令不要過於密集,這一方面可以減輕對帶寬的需求,另一方面也會提高解
碼的效率。訪問內存的指令至少有兩個微操作,這樣的指令只能每個時鐘週期解碼一條
,而Pentium III的解碼極限可是每個時鐘週期三條指令啊。爲了提高處理器的並行度,
有必要在內存訪問指令上下功夫。在我的代碼中,內存訪問指令的排布還是比較有規律
的,差不多是每隔三條指令訪問一次內存。當然,在計算第一行數據時,因爲要讀取一
些初始化的數據,內存訪問比後面的代碼要頻繁。
靈活性
矩陣的運算是一行一行進行的,每一行數據只被讀取一次。這就意味着,我們可以把運
算結果保存在任何一個矩陣裏,即保存在m1或者m2中,因爲這兩個矩陣中的數據已經不
會被再次讀取了,也就不用擔心破壞數據。這種靈活性可以是我們輕而易舉地完成矩陣
左乘或者右乘的代碼。在Direct3D中,空間變換是按照如下方式進行計算的:
在進行多次變換時,只要在原有的矩陣上右乘一個變換矩陣就可以了。下面的代碼就是
這樣的一個例子:
MatMul_xmm(m1, m1, m2);
如果使用高級語言來實現恐怕就要麻煩一些,你要使用一些中間變量,程序如下所示:

void MatMul_Right_cpp(float *dest, float *m)
{
     float tmp[16];
     MatMul_cpp(tmp, dest, m)
     memcpy(dest, tmp, 16*4);
}

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