比較一下3DNow和浮點(diǎn)指令的性能差異,可以看出,3DNow指令集在運(yùn)算速度上要遠(yuǎn)遠(yuǎn)
超過(guò)浮點(diǎn)指令。那么,SSE性能如何呢,它是否有能力同3DNow一拚高低?我想,很難說(shuō) 那一個(gè)更好一些,因?yàn)樗鼈兌加兄芨叩男阅?。不過(guò)單從指令集上看,SSE還是要略勝一 籌的。畢竟是新增了8個(gè)128位的寄存器,而且指令的功能也要強(qiáng)大一些。3DNow使用MMX 指令的寄存器,可以借助MMX指令的強(qiáng)大功能,不必設(shè)計(jì)太多的新功能,不需要操作系 統(tǒng)提供專門的支持,而且口碑頗佳!從流水線的設(shè)計(jì)上看,雙方也是各有所長(zhǎng)。Pentiu m III每個(gè)時(shí)鐘周期最多可以解碼3條指令,執(zhí)行5個(gè)微操作,它把一些重要的微操作(例 如乘法和加法)分派到不同的端口去執(zhí)行。 3DNow則是在兩條流水線間共享3DNow的執(zhí)行 單元和部分MMX的執(zhí)行單元,所有的3DNow指令都是有兩個(gè)時(shí)鐘周期的延遲,并且完全被 流水線化。 最近,AMD的處理器似乎有了很大的變化,我看過(guò)一些有關(guān)它的64位處理器的資料,也是 添加了一堆寄存器,不過(guò)我沒有仔細(xì)看,畢竟沒有哪個(gè)緣份一睹芳顏。intel公司當(dāng)然也 沒有閑著,它的64位處理器則不能用“變化”二字來(lái)形容了,那簡(jiǎn)直可以說(shuō)是脫胎換骨 ,全新的指令,全新的體系!不過(guò),咱們老百姓恐怕不會(huì)在短時(shí)間內(nèi)用上這種處理器, 擁有三百多個(gè)寄存器的CPU肯定會(huì)處于我無(wú)法接受的價(jià)位。這樣也還是有一個(gè)好處的,那 就是SSE指令集在短時(shí)間內(nèi)不會(huì)過(guò)時(shí),畢竟,轉(zhuǎn)移到64位陣營(yíng)還是要經(jīng)歷一個(gè)漫長(zhǎng)的過(guò)程 。而且,在IA-64體系中專門提供了三條指令在32位代碼和64位代碼之間進(jìn)行跳轉(zhuǎn),也就 是說(shuō),你可以在程序中任意使用兩種代碼。 所以,如果你想針對(duì)intel系列的處理器進(jìn)行優(yōu)化的話,就努力學(xué)好SSE吧,在相當(dāng)長(zhǎng)的 時(shí)間里都會(huì)大有用處的。 本文不會(huì)詳細(xì)介紹每一個(gè)SSE指令,只是討論一些重要的,常用的,能夠?qū)π阅墚a(chǎn)生較大 影響的指令。如果你想更全面的了解SSE,請(qǐng)參閱 SSE指令簡(jiǎn)明參考。你可以從中查到每 條SSE指令的功能。 通過(guò)程序來(lái)討論指令的用法是最好的辦法。以前寫過(guò)的兩篇文章,一篇是關(guān)于浮點(diǎn)指令 優(yōu)化的,一篇是關(guān)于3DNow指令優(yōu)化的,這兩篇文章都是使用了矩陣相乘作為例子程序, 因此本文還是以矩陣相乘為例,看一看SSE究竟有什么優(yōu)勢(shì)!它與單純使用浮點(diǎn)指令的程 序相比效率能提高多少! 準(zhǔn)備工作 選擇合適的編譯器 目前我還沒有發(fā)現(xiàn)哪個(gè)編譯器能夠?qū)SE提供內(nèi)聯(lián)支持,據(jù)intel聲稱,它的C++ 編譯器 可以做到,但是,恐怕沒有幾個(gè)人用過(guò)。建議大家使用MASM6.14,它支持SSE和3DNow。 大家可以從本站下載MASM6.14。高級(jí)語(yǔ)言的編譯器也是要有的,我使用的是VC6.0。因?yàn)?br> VC在浮點(diǎn)程序方面比 C++ Builder優(yōu)化的更好,這樣就可以與匯編的優(yōu)化結(jié)果進(jìn)行比較 了。 設(shè)置編譯器 在匯編程序里,應(yīng)該加入偽指令來(lái)指示編譯器支持何種指令集。“.xmm”表示要求編譯 器支持SSE指令集。“.k3d”則是要求編譯器支持3DNow指令集。 VC提供了一些支持,可以自動(dòng)的編譯匯編文件,你可以按照以下步驟進(jìn)行: 在菜單中選擇“Project | Setting” 選中指定的匯編文件(單擊即可) 選中Custom Build頁(yè) 在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盤,則要作相應(yīng)的修改。 學(xué)習(xí)指令 你首先應(yīng)該對(duì)SSE指令有所了解才能更好的閱讀本文。SSE指令集是一個(gè)比較新的體系, 如果你沒有學(xué)過(guò)MMX或者3DNow,還是有一定困難的。在全面優(yōu)化Pentium III一文中對(duì)P entium III 的體系有比較全面的闡述。 優(yōu)化方針 針對(duì)SSE優(yōu)化還是比較困難的,下面提出一些方法,以供參考: 擺脫高級(jí)語(yǔ)言的桎梏,根據(jù)硬件的特點(diǎn),指令的功能,量體裁衣地設(shè)計(jì)算法。要知道, 匯編語(yǔ)言的算法與高級(jí)語(yǔ)言是有很大的不同的,只有重新設(shè)計(jì)的算法才有可能發(fā)揮出處 理器的最大潛力。 熟練使用一些常用的指令,知道它們的延遲和吞吐量是多少。本文的例子中所用的一些 重要的指令有:ADDPS,MULPS,SHUFPS,MOVSS,MOVAPS。關(guān)于它們的執(zhí)行單元的相關(guān)數(shù) 據(jù)可以查閱處理器執(zhí)行單元列表。 充分利用新增加的八個(gè)寄存器,減小內(nèi)存的壓力;設(shè)計(jì)并行算法,減輕流水線的延遲。 綜合考慮解碼器,流水線,執(zhí)行端口等多方面因素,盡量增強(qiáng)處理器的并行處理的能力 。 舉例詳解 下面的程序是一個(gè)矩陣相乘的函數(shù)。在三維圖形空間變換中,要用到4乘4的浮點(diǎn)矩陣, 而矩陣相乘的運(yùn)算是很常用的。下面的函數(shù)的參數(shù)都是4乘4的浮點(diǎn)矩陣。寫成這種形式 是為了保持比較強(qiáng)的伸縮性。 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的優(yōu)化能力是很強(qiáng)的,象上面這樣的比較常規(guī)的算法,你很難做出比它快得多的代碼 。不過(guò)使用SSE以后就不一樣了。下面是一個(gè)匯編函數(shù),使用SSE 指令進(jìn)行計(jì)算。注意, 這個(gè)函數(shù)只能運(yùn)行于32位的環(huán)境中。“.xmm”指示編譯器使用SSE指令集進(jìn)行編譯。 函數(shù)的C語(yǔ)言原型是這樣的: extern "C" { void __stdcall MatMul_xmm(float *dest, float *m1, float *m2); } 對(duì)于一些不太常用匯編語(yǔ)言編程的朋友來(lái)說(shuō),下面的程序可能比較難于理解。我將對(duì)一 些常識(shí)性的東西做一下簡(jiǎn)單介紹。 在C語(yǔ)言中,代碼段都是以“_TEXT”作為段名的。“use32”告訴編譯器將代碼編譯為3 2位。 有些人看到“_MatMul_xmm@12”這個(gè)函數(shù)名以后可能會(huì)產(chǎn)生疑問(wèn)。其實(shí)這只是遵循了VC 所采用的命名規(guī)范。在VC中,所有標(biāo)志為“__stdcall” 調(diào)用的,采用“C”鏈接的函數(shù) 都要加下劃線作為前綴,并且加上“@N”作為后綴,其中,“N”為參數(shù)的字節(jié)數(shù)。注意 ,上面的函數(shù)是采用“C”鏈接的,如果是“C++”鏈接,命名規(guī)范就太復(fù)雜了。如果你 使用的是C++ Builder,命名規(guī)范就十分簡(jiǎn)單了,照搬函數(shù)名就行了。不同的調(diào)用規(guī)范將 采用不同的命名方法,即使對(duì)相同的調(diào)用規(guī)范,不同的編譯器也不一定兼容。有一種調(diào) 用格式是每一個(gè)C++編譯器都支持并且兼容的,那就是“__cdecl”。 各種調(diào)用格式所采用的堆棧操作也不太一樣。使用“__stdcall”時(shí),參數(shù)從右向左依次 入棧,參數(shù)的彈出需要函數(shù)自己來(lái)處理。這種做法和“__cdecl” 調(diào)用方式不太一樣, “__cdecl”的參數(shù)彈出需要調(diào)用者來(lái)處理。現(xiàn)在很流行的一種調(diào)用格式是“__fastcal l”,也就是寄存器調(diào)用。這種調(diào)用方式通過(guò)寄存器“EAX”,“ECX”,“EDX”傳遞參 數(shù),不過(guò)很可惜,這種調(diào)用也不是在各個(gè)編譯器中兼容的。Inprise在C++ Builder中提 供了一個(gè)關(guān)鍵字“__msfastcall” 用來(lái)和微軟兼容,如果你采用這種調(diào)用規(guī)范就可以在 多個(gè)編譯器中正常調(diào)用了。不過(guò)還有一件事讓人很受打擊,VC沒有對(duì)“__fastcall”提 供很好的優(yōu)化,使用這種調(diào)用反而會(huì)降低效率。 并不是所有的寄存器都能夠隨意使用的,多數(shù)32位寄存器都要先保存的。你可以不必保 存的32位寄存器只有三個(gè)----“EAX”,“ECX”,“EDX”,其它的就只好“PUSH”,“ POP”了。另外,浮點(diǎn)堆棧寄存器是不必保存的;MMX 寄存器和浮點(diǎn)堆棧共享,也是不必 保存的;XMM寄存器不必保存。 很多SSE指令都會(huì)加上“ps”或“ss”后綴。“ps”表示“Packed Single-FP”,即打包 的浮點(diǎn)數(shù),帶這種后綴的指令通常是一次性對(duì)四個(gè)數(shù)進(jìn)行操作的。“ss” 表示“Scala r Single-FP”,帶這種后綴的指令通常是對(duì)最低位的單精度數(shù)進(jìn)行操作的。 下面這個(gè)匯編函數(shù)是一行一行計(jì)算的,咱們先用類似于C的語(yǔ)法簡(jiǎn)述一下第一行的計(jì)算過(guò) 程: 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; 上面的代碼可讀性還是比較好的,因?yàn)橹贿M(jìn)行了第一行的計(jì)算。實(shí)際運(yùn)算中,為了增強(qiáng) 并行度,為了減小指令的延遲,實(shí)際上是兩行并行計(jì)算的。而且,運(yùn)算過(guò)程并不是象算 法描述那樣寫得那么有規(guī)律。 .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] ;讀入第一行的數(shù)據(jù) 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] ;讀入第二行的數(shù)據(jù) 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] ;讀入第三行的數(shù)據(jù) 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] ;讀入第四行的數(shù)據(jù) 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 上面的代碼幾乎沒有加什么注釋,只是在讀入每行第一個(gè)數(shù)據(jù)時(shí)作了標(biāo)記。因?yàn)?,SSE 的指令可讀性還是比較好的,除了要加上一些后綴以外,它們和普通的整數(shù)運(yùn)算指令很 相似。 一些關(guān)鍵性的指令有必要解釋一下: movss和movaps: movss是將一個(gè)單精度數(shù)傳輸?shù)絰mm寄存器的低32位,而movaps則是一次性向寄存器中寫 入四個(gè)單精度數(shù)。也許有些人會(huì)認(rèn)為movaps效率更高一些,其實(shí)并不一定是這樣。從處 理器執(zhí)行單元列表中,你可以查到這些指令的延遲。如果都是從寄存器中讀取數(shù)據(jù),兩 個(gè)指令的延遲是一樣的。如果是從內(nèi)存中讀取數(shù)據(jù),movss只有一個(gè)時(shí)鐘周期的延遲,而 movaps卻有四個(gè)時(shí)鐘周期的延遲。 上面的匯編代碼混合使用了這兩條指令。那么,應(yīng)該在什么時(shí)候選擇哪一條指令呢?這 要看你對(duì)數(shù)據(jù)的需求了。如果你希望能夠盡快地使用數(shù)據(jù),就應(yīng)當(dāng)首選movss,因?yàn)樗鼛?br> 乎能夠讓你立即使用數(shù)據(jù)。如果你并不急于使用某些數(shù)據(jù),只是想先把它讀入寄存器, 那么毫無(wú)疑問(wèn)movaps是你的最佳選擇。 movaps使用端口2讀取數(shù)據(jù),如果在它執(zhí)行完畢 之前你不去使用它的數(shù)據(jù),這條指令的實(shí)際延遲就只有一個(gè)時(shí)鐘周期??紤]到處理器能 夠在5個(gè)端口并行執(zhí)行微操作,那么這條指令的延遲可能還不到一個(gè)時(shí)鐘周期。 從上面的代碼中,你可以看到,每一條movaps指令和它的相關(guān)指令之間都至少插入了四 條指令,這樣可以基本上避免延遲。 雖然movss指令只有一個(gè)時(shí)鐘周期的延遲,但是這也并不意味著你可以把這條指令和它的 相關(guān)指令寫在一起,因?yàn)檫@有可能會(huì)影響處理器的并行度。雖然 Pentium III有著強(qiáng)大 的亂序執(zhí)行的能力,可是這畢竟是不太保險(xiǎn)的,還是自己動(dòng)手,豐衣足食吧。 SHUFPS 這是一條可以將操作數(shù)打亂順序的指令。這一條指令有很多種用法,它根據(jù)常量參數(shù)的 不同執(zhí)行不同的功能。本文中只使用了一種用法: shufps xmmreg, xmmreg, 00h 這條指令的作用是把某個(gè)寄存器的最低位的單精度數(shù)傳輸?shù)皆摷拇嫫鞯钠渌齻€(gè)部分。 在某些時(shí)候,shufps和unpcklps(或unpckhps)可以執(zhí)行相同的功能。這時(shí),推薦使用 shufps,因?yàn)檫@條指令有兩個(gè)時(shí)鐘周期的延遲。unpcklps和unpckhps 都是有三個(gè)時(shí)鐘周 期的延遲。 ADDPS和MULPS 這兩條指令是很重要的計(jì)算指令,有必要弄清楚它們的執(zhí)行情況。 addps有4個(gè)時(shí)鐘周期的延遲,mulps有5個(gè)時(shí)鐘周期的延遲,我們應(yīng)該根據(jù)這些數(shù)據(jù)考慮 清楚,究竟在它們的相關(guān)代碼中插入多少條指令。 這兩條指令都是每?jī)蓚€(gè)時(shí)鐘周期才允許執(zhí)行一次,如果你把相同的兩條這樣的指令寫在 一起,第二條指令就有可能被延誤一個(gè)時(shí)鐘周期。應(yīng)該插入一些其它指令來(lái)掩蓋這段延 遲。 mulps在端口0執(zhí)行,addps在端口1執(zhí)行,如果你的代碼把乘法和加法指令寫在一起,它 們會(huì)被分配到不同的端口并行執(zhí)行,這比只有一條流水線的FPU要高效的多。 優(yōu)化思路: 下面將解釋一下上面代碼的優(yōu)化思路。 打亂指令 在算法描述中,各條操作寫得非常有規(guī)律,但是在真正編程的時(shí)候卻不是這樣。為了保 證流水線的流暢運(yùn)作,就要把相關(guān)的代碼分離開來(lái),盡量避免或減輕指令的延遲。這樣 就要打亂指令,在兩條相關(guān)指令之間插入一些其它的指令,同時(shí)也要考慮指令之間是否 存在資源的競(jìng)爭(zhēng)。 并行算法 多個(gè)數(shù)據(jù)并行計(jì)算是解決指令延遲問(wèn)題的有效方法。我們不能傻傻地等待一條指令的計(jì) 算結(jié)果,而是要在等待的過(guò)程中進(jìn)行其它數(shù)據(jù)的計(jì)算。在上面程序的算法中,每當(dāng)寄存 器有了空閑,就馬上從內(nèi)存中讀入新的數(shù)據(jù),盡量保證有兩組數(shù)據(jù)在寄存器中并行計(jì)算 。 內(nèi)存訪問(wèn) 訪問(wèn)內(nèi)存的指令不要過(guò)于密集,這一方面可以減輕對(duì)帶寬的需求,另一方面也會(huì)提高解 碼的效率。訪問(wèn)內(nèi)存的指令至少有兩個(gè)微操作,這樣的指令只能每個(gè)時(shí)鐘周期解碼一條 ,而Pentium III的解碼極限可是每個(gè)時(shí)鐘周期三條指令啊。為了提高處理器的并行度, 有必要在內(nèi)存訪問(wèn)指令上下功夫。在我的代碼中,內(nèi)存訪問(wèn)指令的排布還是比較有規(guī)律 的,差不多是每隔三條指令訪問(wèn)一次內(nèi)存。當(dāng)然,在計(jì)算第一行數(shù)據(jù)時(shí),因?yàn)橐x取一 些初始化的數(shù)據(jù),內(nèi)存訪問(wèn)比后面的代碼要頻繁。 靈活性 矩陣的運(yùn)算是一行一行進(jìn)行的,每一行數(shù)據(jù)只被讀取一次。這就意味著,我們可以把運(yùn) 算結(jié)果保存在任何一個(gè)矩陣?yán)铮幢4嬖趍1或者m2中,因?yàn)檫@兩個(gè)矩陣中的數(shù)據(jù)已經(jīng)不 會(huì)被再次讀取了,也就不用擔(dān)心破壞數(shù)據(jù)。這種靈活性可以是我們輕而易舉地完成矩陣 左乘或者右乘的代碼。在Direct3D中,空間變換是按照如下方式進(jìn)行計(jì)算的: 在進(jìn)行多次變換時(shí),只要在原有的矩陣上右乘一個(gè)變換矩陣就可以了。下面的代碼就是 這樣的一個(gè)例子: MatMul_xmm(m1, m1, m2); 如果使用高級(jí)語(yǔ)言來(lái)實(shí)現(xiàn)恐怕就要麻煩一些,你要使用一些中間變量,程序如下所示: void MatMul_Right_cpp(float *dest, float *m) { float tmp[16]; MatMul_cpp(tmp, dest, m) memcpy(dest, tmp, 16*4); } Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1270109 |
|