匯編優(yōu)化提示
更新時(shí)間:2012年07月31日 15:42:29 作者:
暑假瞄了一些匯編優(yōu)化的文章,感覺(jué)這篇有點(diǎn)意思。盡管英文水平不咋地,還是倔起牛勁翻譯了下??隙ㄓ胁缓玫牡胤?,大家海涵~英文原文附件給出~如果有什么錯(cuò)誤還望批評(píng)指正~另外,如果admin感覺(jué)可以加精的話就麻煩下了
你需記住的最重要的事情就是代碼花費(fèi)的時(shí)間!不同的方法可以加速你的代碼或者不能,所以你要多多嘗試。因而計(jì)算代碼花費(fèi)的時(shí)間來(lái)看看你嘗試的每個(gè)方法是否可以提速是件很重要的事情。
;=========================初級(jí)=========================
<1>釋放所有8-CPU寄存器,以使它們用于你自己的代碼中
push ebx
push esi
push edi
push ebp ;必須在改變ESP之前完成
;裝載ESI、EDI和其他傳遞棧中值的寄存器,這必須在釋放ESP之前完成。
movd mm0,esp ; 沒(méi)有入棧/出棧操作超過(guò)此處,
xor ebx,ebx ; 所以你怎樣保存呢?一個(gè)變量嘛!
mov esp,5
inner_loop:
mov [eax_save],eax ; eax_save是一個(gè)全局變量不可能是局
; 部的,因?yàn)槟且驟BP來(lái)指向它。
add ebx,eax
mov eax,[eax_save] ; 存儲(chǔ) eax
movd esp,mm0 ; 必須在做POPs之前完成
pop ebp
pop edi
pop esi
pop ebx
ret
<2>寄存器的最大化使用
多數(shù)高級(jí)點(diǎn)的編譯器會(huì)產(chǎn)生非常多的到內(nèi)存變量的訪問(wèn)。我通常避免那樣做,因?yàn)槲冶M力把這些變量放在寄存器中。所以要使8-CPU寄存器自由使用,以備燃眉之急~
<3>復(fù)雜指令
避免復(fù)雜指令(lods, stos, movs, cmps, scas, loop, xadd, enter, leave)。復(fù)雜指令就是那些可以做許多事情的指令。例如,stosb向內(nèi)存寫1字節(jié),同時(shí)增加EDI。 這些復(fù)雜指令阻止早期的Pentium快速運(yùn)行,因?yàn)樘幚砥鞯陌l(fā)展趨勢(shì)是力使自己更像精簡(jiǎn)指令集計(jì)算機(jī)(RISC)。使用rep和字符串指令依舊很快----而這是唯一的例外。
<4>不要再P4上使用INC/DEC
在P4上用ADD/SUB代替INC/DEC。通常前者更快些。ADD/SUB耗費(fèi)0.5 cycle而INC/DEC耗費(fèi)1 cycle.
<5>循環(huán)移位(rotating)
避免用寄存器作循環(huán)移位計(jì)數(shù)器;立即數(shù)作計(jì)數(shù)器時(shí),除了1,不要用其他值。
<6>消除不必要的比較指令
通過(guò)合理運(yùn)用條件跳轉(zhuǎn)指令來(lái)消除不必要的比較指令,但是,這些條件跳轉(zhuǎn)指令時(shí)基于標(biāo)志位的。而這些標(biāo)志位已經(jīng)被前面的算術(shù)指令設(shè)置了。
dec ecx
cmp ecx,0
jnz loop_again
優(yōu)化為:
dec ecx
jnz loop_again
<7>lea指令的使用
lea非???,除了在P4上它表現(xiàn)得慢一點(diǎn)。你可以在這一條指令中執(zhí)行許多數(shù)學(xué)運(yùn)算,并且它不會(huì)影響標(biāo)志寄存器。所以你可以把它放在一個(gè)正在被修改的寄存器和一個(gè)涉及標(biāo)志位的條件跳轉(zhuǎn)指令中間。
top_of_loop:
dec eax
lea edx,[edx*4+3] ; 乘4加3。不影響標(biāo)志位
jnz top_of_loop ; 所以jnz判斷的是dec eax執(zhí)行后的狀態(tài)字
<8>ADC和SBB的使用
許多編譯器都不充分利用ADC和SBB。你可以用它們很好地提速,如:把兩個(gè)64-bit的數(shù)加在一起或者兩個(gè)大數(shù)相加。記牢:ADC和SBB在P4上慢。當(dāng)你手頭有類似工作時(shí),你可以使用addq和用MMX來(lái)完成。所以第二個(gè)優(yōu)化建議是:用MMX做這樣的加減法。但你的處理器必須支持MMX。
add eax,[fred]
adc edx,[fred+4]
;下面的3條指令完成同樣的功能
movd mm0,[fred] ;得到MM0中的32-bit的值
movd mm1,[fred+4] ;得到MM1中的32-bit的值
paddq mm0,mm1 這是一種未優(yōu)化的方法,實(shí)際上,你可以在前面的循環(huán)中預(yù)取MM0和MM1。我這樣做是為了好理解。
<9>ROL, ROR, RCL, RCR 和 BSWAP
使用BSWAP指令把Big Endian數(shù)據(jù)轉(zhuǎn)換成Little Endian格式是很酷的一種方法。你也可以使用它臨時(shí)存儲(chǔ)寄存器的高位的的值(16-bit或8-bit)。類似地,你可以使用ROL/ROR來(lái)存儲(chǔ)8-bit或16-bit值。這也是獲得更多“寄存器”的一種方法。如果你處理的都是16-bit的值,你可以把你的8個(gè)32-bit的寄存器轉(zhuǎn)換成16個(gè)16-bit的寄存器。這樣你就有更多寄存器可以使用了。RCL和RCR可以很容易地被使用在計(jì)算寄存器中比特位(0 or 1)的多少。牢記:P4上ROL, ROR, RCL, RCR 和 BSWAP速度慢。循環(huán)移位指令大約比BSWAP快2倍。所以,如果你必須在P4機(jī)上使用上述指令,還是選循環(huán)移位指令吧。
xor edx,edx ; 設(shè)置兩個(gè)16-bit的寄存器為0
mov dx,234 ; 設(shè)置低位寄存器為234
bswap edx ; 高低位寄存器互換
mov dx,345 ; 設(shè)置當(dāng)前低位寄存器為345
bswap edx ; 當(dāng)前高低位互換
add dx,5 ; 當(dāng)前低位寄存器加5
bswap edx ; 高低位互換
add dx,7 ; 當(dāng)前低位寄存器加7
<10>字符串指令
許多編譯器沒(méi)有充分利用字符串指令( scas, cmps, stos, movs和 lods)。所以檢測(cè)這些指令寫成的函數(shù)是否比一些庫(kù)函數(shù)速度快是非常有意義的。例如:當(dāng)我在看VC++的strlen()函數(shù)時(shí),我真的很驚訝。在radix40代碼中,處理一個(gè)100字節(jié)的字符串,它竟跑了416 cycles ?。。∥艺J(rèn)為這慢地荒誕?。?!
<11>以乘代除
If you have a full 32-bit number and you need to divide, you can simply do a multiply
and take the top 32-bit half as the result. This is faster because multiplication is
faster than division. ( thanks to pdixon for the tip).
如果你有一個(gè)滿32-bit(full 32-bit)的數(shù)要做除法,你可以簡(jiǎn)單地做乘法,然后取高32-bit部分作為結(jié)果。這樣會(huì)快些,因?yàn)槌朔ū瘸欤?感謝pdixon提供了這個(gè)tip)(譯者注:由于水平有限,此tip翻譯尚待商討,而且不能給出一個(gè)例子。還仰仗各位幫忙。)
<12>被常數(shù)除
這兒有一些很好的信息----怎樣被常數(shù)除(在Agner Fog的pentopt.pdf中)。我(Mark Larson)寫了一個(gè)小程序:可以根據(jù)你輸入的除數(shù)自動(dòng)產(chǎn)生匯編代碼序列。我會(huì)繼續(xù)探究探究,然后貼出來(lái)。這是Agner的文檔的鏈接。Agner's Pentopt PDF (http://www.agner.org/assem/)
<13>展開(kāi)(Unrolling)
這是一個(gè)指導(dǎo)方針。在一般優(yōu)化策略里,“展開(kāi)”往往被忽略,因此我想為它加個(gè)注腳。我總是用數(shù)值總數(shù)相等的宏來(lái)建立我的“展開(kāi)”結(jié)構(gòu)。那樣的話,你可以嘗試不同的值來(lái)看看哪種是最容易的。你希望這個(gè)展開(kāi)“安身”于L1代碼緩存(或追蹤緩存,trace cache)。使用等價(jià)(equate)語(yǔ)句可以很容易地嘗試不同的展開(kāi)值來(lái)達(dá)到最快的速度。
UNROLL_AMT equ 16 ; # 展開(kāi)循環(huán)的次數(shù)
UNROLL_NUM_BYTES equ 4 ; # 一次循環(huán)處理的字節(jié)數(shù)
mov ecx,1024
looper:
offset2 = 0
REPEAT UNROLL_AMT
add eax,[edi+offset2]
offset2 = offset2 + UNROLL_NUM_BYTES
add edi,UNROLL_AMT * UNROLL_NUM_BYTES ; 按16*4字節(jié)來(lái)處理
sub ecx,UNROLL_AMT ; 從循環(huán)計(jì)數(shù)器中減去展開(kāi)循環(huán)的次數(shù)
jnz looper
<14>MOVZX指令
使用MOVZX來(lái)避免偏愛(ài)的寄存器受阻。我使用MOVZX非常多。許多人首先將滿32-bit寄存器(full 32-bit register)XOR一下。但是,MOVZX無(wú)需這個(gè)多余的XOR指令而可以完成同樣的事情。而且你必須提前做XOR運(yùn)算,之后才能使用寄存器,并且這個(gè)操作是花費(fèi)一定時(shí)間的。但有MOVZX在,你就無(wú)需擔(dān)心啦。
<15>用MOVZX來(lái)避免SHIFT和AND指令的使用
我用匯編來(lái)對(duì)C代碼中的位運(yùn)算提速。the_array是一個(gè)dword的數(shù)組。代碼功能是得到數(shù)組中一個(gè)dword的任意字節(jié)。Pass是一個(gè)值為0-3的變量。因此這個(gè)功能有如下C代碼:
unsigned char c = ((the_array[i])>>(Pass<<3)) & 0xFF;
; 我為了避免Pass變量的使用,展開(kāi)了循環(huán)4次。所以我得到了如下更多的代碼:
unsigned char c = (the_array[i])>>0) & 0xFF;
unsigned char c = (the_array[i])>>8) & 0xFF;
unsigned char c = (the_array[i])>>16) & 0xFF;
unsigned char c = (the_array[i])>>24) & 0xFF;
在匯編中,我摒棄SHIFT和AND會(huì)發(fā)生什么呢?節(jié)約了我2個(gè)指令!更不用提P4上SHIFT指令非常慢(4 cycles!!!)的事實(shí)。所以如果可能,盡量避免使用SHIFT指令。我們以第三行為例,所以只需右移16位即可:
mov eax,[esi] ; esi指向the_array數(shù)組
shr eax,16
and eax,0FFh
; 那么我們?cè)鯓颖苊釹HR和AND的使用呢?我們舉例對(duì)dword中的第三個(gè)字節(jié)做MOVZX運(yùn)算。
movzx eax,byte ptr [esi+2] ;unsigned char c = (the_array[i])>>16) & 0xFF;
<16>align指令
該偽指令非常重要,它可以對(duì)齊你的代碼和數(shù)據(jù)來(lái)很好地提速。對(duì)代碼,我通常按4字節(jié)邊界對(duì)齊。對(duì)于數(shù)據(jù)呢,2字節(jié)數(shù)據(jù)按2字節(jié)邊界對(duì)齊,4字節(jié)數(shù)據(jù)按4字節(jié)邊界對(duì)齊,8字節(jié)數(shù)據(jù)按8字節(jié)邊界對(duì)齊,16字節(jié)數(shù)據(jù)按16字節(jié)邊界對(duì)齊。一般情況,如果不以一個(gè)16-bit邊界對(duì)齊SSE或SSE2數(shù)據(jù)的話,將會(huì)產(chǎn)生一個(gè)異常。如果有VC++開(kāi)發(fā)包,你可以在VC++環(huán)境中對(duì)齊你的數(shù)據(jù)。VC++加入了對(duì)靜態(tài)數(shù)據(jù)和動(dòng)態(tài)內(nèi)存的支持。對(duì)于靜態(tài)數(shù)據(jù),你可以使用__declspec(align(4))來(lái)按4字節(jié)邊界對(duì)齊。
<17>用于2的冪的BSR指令
你可以把BSR命令用于變量的2的最高冪的計(jì)數(shù)。
<18>用XOR置寄存器為0
這是非常陳舊的常識(shí)了,但是我依然要重提一下。它也有一個(gè)側(cè)面好處----消除對(duì)寄存器的依賴性。這就是人們使用寄存器之前用XOR置零成風(fēng)的原因。但我寧愿使用MOVZX,因?yàn)閄OR很狡猾~(看看我前面對(duì)MOVZX的討論)在P4上,也增加了PXOR支持,以消除對(duì)XOR的依賴性。我認(rèn)為P3做了同樣的事情。
<19>使用XOR和DIV
如果你確定你的數(shù)據(jù)在做除法時(shí)是無(wú)符號(hào)數(shù),使用XOR EDX, EDX,然后DIV。它比CDQ和IDIV快。
<20>盡量避免明顯的依賴關(guān)系
如果你修改一個(gè)寄存器,然后在緊跟的下一行中讓它和某個(gè)值進(jìn)行比較,相反地,你最好在兩者之間插入其他的寄存器的修改指令。依賴就是任何時(shí)間你修改一個(gè)寄存器,然后緊跟著對(duì)它讀或?qū)憽?譯者注:其實(shí)就是AGI延遲; AGI: Address Generation Interlock)
inc edi
inc eax
cmp eax,1 ;這行依賴于上一行,所以產(chǎn)生延遲
jz fred
;移動(dòng)周圍的指令到這兒可以打破這種依賴
inc eax
inc edi
cmp eax,1
jz fred
<21>P4機(jī)上避免使用的指令
在P4機(jī)上避免使用如下指令: adc, sbb, rotate指令, shift指令, inc, dec, lea, 和任何耗費(fèi)多于 4 uops(微指令)的指令。你怎么知道當(dāng)前運(yùn)行代碼的處理器是P4?CPUID命令!
<22>使用查找表
在P4機(jī)上,你可以通過(guò)建立查找表,來(lái)規(guī)避長(zhǎng)時(shí)間延遲的指令(前面已列出來(lái))。幸而P4機(jī)的內(nèi)存速度很快,所以如果建立的查找表不在cache里,它也并不會(huì)很大地影響性能。
<23>使用指針而不是計(jì)算下標(biāo)(索引)
許多時(shí)候,C語(yǔ)言中的循環(huán)會(huì)出現(xiàn)兩個(gè)非冪數(shù)的乘法。你可以很容易地用加法來(lái)完成。下面是一個(gè)使用結(jié)構(gòu)體的例子:
typedef struct fred
{
int fred;
char bif;
}freddy_type;
freddy_type charmin[80];
freddy_type結(jié)構(gòu)的大小是5字節(jié)。如果你想在循環(huán)中訪問(wèn)它們,編譯器會(huì)產(chǎn)生此種代碼----對(duì)每個(gè)數(shù)組元素的訪問(wèn)都乘以5?。?!Ewwwwwwwwwwwww(譯者注:語(yǔ)氣詞)?。?!那么我們應(yīng)該怎樣做呢?
for ( int t = 0; t < 80; t++)
{
charmin[t].fred = rand(); // 編譯器為了得到偏移量,竟乘以了5,EWWWWWWWW!
charmin[t].bif = (char)(rand() % 256);
}
在匯編中,我們以偏移量0開(kāi)始,這指向了數(shù)據(jù)第一個(gè)元素。然后我們每次把循環(huán)迭代器加5來(lái)避免MUL指令的使用。
mov esi,offset charmin
mov ecx,80
fred_loop:
;...freddy_type結(jié)構(gòu)中FRED和BIF元素的處理命令
add esi,5 ;指向下一個(gè)結(jié)構(gòu)的入口地址
dec ecx
jnz fred_loop
MUL指令的規(guī)避也應(yīng)用在循環(huán)中。我曾見(jiàn)過(guò)人們?cè)谘h(huán)中以乘來(lái)實(shí)現(xiàn)變量增加或者終止條件。相反,你應(yīng)盡量用加法。
<24>遵從默認(rèn)分支預(yù)測(cè)
盡量設(shè)計(jì)你的代碼使向后的條件跳轉(zhuǎn)經(jīng)常發(fā)生,并且向前的條件跳轉(zhuǎn)幾乎從不發(fā)生。這顯然與分支預(yù)測(cè)有關(guān)。CPU中的靜態(tài)分支預(yù)測(cè)器(static branch predictor)只使用簡(jiǎn)單的規(guī)則來(lái)猜測(cè)一個(gè)條件跳轉(zhuǎn)是否發(fā)生。所以應(yīng)使向后跳轉(zhuǎn)的循環(huán)分支靠近結(jié)束。然后讓同一循環(huán)的特殊退出條件(exit condition)執(zhí)行一個(gè)向前的跳轉(zhuǎn)(這個(gè)跳轉(zhuǎn)只在此跳轉(zhuǎn)不經(jīng)常發(fā)生的這個(gè)特定的條件下退出)。
<25>消除分支
如果可能,消除分支!這是顯然的,但我見(jiàn)過(guò)許多人在他們的匯編代碼中使用了太多的分支。保持程序的簡(jiǎn)單。使用盡可能少的分支。
<26>使用CMOVcc來(lái)消除分支
我曾見(jiàn)到過(guò)CMOVcc指令確實(shí)比條件跳轉(zhuǎn)指令快。所以我建議使用CMOVcc而非條件跳轉(zhuǎn)指令。當(dāng)在你的跳轉(zhuǎn)不容易被分支預(yù)測(cè)邏輯猜到的情況下,它可能會(huì)更快。如果你遇到那種情況,設(shè)定基準(zhǔn)點(diǎn)(benchmark)看看!
<27>局部變量vs全局變量
在一個(gè)過(guò)程模塊(子函數(shù))中使用局部變量而不是全局變量。如果你使用局部變量,你會(huì)得到更少的緩存未命中(Cache miss)。
<28>地址計(jì)算
在你需要某地址之前計(jì)算它。不得不說(shuō)你為了得到某一特定地址必須計(jì)算一些令人討厭的東西。例如地址乘20。你可以在需要該地址的代碼之前預(yù)算它。
<29>小點(diǎn)兒的寄存器
有些時(shí)候,使用小點(diǎn)兒的寄存器可以提升速度。我在radix40代碼中驗(yàn)證了它。如果你使用EDX來(lái)修改下面的代碼,它跑得會(huì)慢些。
movzx edx,byte ptr [esi] ;從ascii數(shù)組取數(shù)據(jù)
test dl,ILLEGAL ;位測(cè)試
jnz skip_illegal
<30>指令長(zhǎng)度
盡量使你的指令大小保持在8字節(jié)以下。
<31>使用寄存器傳遞參數(shù)
如果可能,嘗試用寄存器傳遞參數(shù)而不是棧。如果你有3個(gè)變量要壓棧作為參數(shù),至少有6個(gè)內(nèi)存讀操作和3個(gè)內(nèi)存寫操作。你不得不把每個(gè)變量由內(nèi)存讀入CPU寄存器然后把它們壓入棧。這是3個(gè)內(nèi)存讀操作。然后壓向棧頂產(chǎn)生3個(gè)寫操作。然后為什么你會(huì)壓你從不使用的參數(shù)呢?所以,又出現(xiàn)了最少3個(gè)讀棧操作。(譯者注:兩內(nèi)存變量不能直接傳遞數(shù)據(jù))
<32>不要向棧傳遞大數(shù)據(jù)
不要向棧傳遞64-bit或128-bit的數(shù)據(jù)(或者更大)。相反,你應(yīng)該傳遞指向該數(shù)據(jù)的指針。
;=========================中級(jí)=========================
<33>加法方向
寄存器加向內(nèi)存比內(nèi)存加向寄存器速度更快。這與指令耗費(fèi)的微指令(micro-ops)的多少有關(guān)。所以,你知道該怎么做了。
add eax,[edi] ;如果可能,不要這樣做
add [edi],eax ;這才是首選
<34>指令選擇
盡量選取產(chǎn)生最少微指令和最少延遲的指令。
<35>未對(duì)齊字節(jié)數(shù)據(jù)流的雙字(dword)對(duì)齊處理
對(duì)于沒(méi)有4字節(jié)邊界對(duì)齊的緩沖區(qū),一次分解出一個(gè)dword會(huì)使性能湮沒(méi)。(譯者注:因?yàn)榈刂?2-bit對(duì)齊時(shí),處理速度最快)你可以通過(guò)處理開(kāi)始的X(0-3)字節(jié)直到遇到一個(gè)4字節(jié)對(duì)齊的邊界處的方法來(lái)規(guī)避這種情況。
<36>使用CMOVcc來(lái)復(fù)位一個(gè)無(wú)限循環(huán)指針
如果你想多次傳遞一個(gè)數(shù)組,并當(dāng)達(dá)到數(shù)組末端的時(shí)候復(fù)位到開(kāi)始處,你可以使用CMOVcc指令。
dec ecx ; 減少數(shù)組索引
cmovz ecx,MAX_COUNT ; 如果在開(kāi)始處,復(fù)位索引為 MAX_COUNT(結(jié)尾處)。
<37>以減法來(lái)代替乘以0.5的乘法
這可能不會(huì)對(duì)所有情況奏效除了real4變量乘以0.5,或者被2除(real4變量是浮點(diǎn)數(shù)),你只需把指數(shù)減1即可。但對(duì)0.0不奏效。對(duì)real8變量,要減去00100000h。(這個(gè)tip是donkey貼出來(lái)的,donkey posted this)
.data
somefp real4 2.5
.code
sub dword ptr [somefp],00800000h ;用2除real4
<38>自修改代碼
P4優(yōu)化手冊(cè)建議避免自修改代碼的使用。我曾經(jīng)見(jiàn)到過(guò)幾次自修改代碼可以跑得更快的情況。但是每次使用前你都要明確它在當(dāng)前情況下會(huì)更快。(譯者注:自修改代碼即INC/DEC)
<39>MMX, SSE, SSE2指令
許多編譯器對(duì)MMX,SSE和SSE2不會(huì)產(chǎn)生很好的代碼。GCC和Intel編譯器對(duì)此做了更多工作,所以較其他產(chǎn)品好一些。但是你自己的"大腦+手"編譯器在這項(xiàng)工作中的應(yīng)用仍然是很大的成功。
<40>使用EMMS
EMMS在Intel處理器上傾向?yàn)楹苈闹噶?。在AMD上它比較快。通常我不會(huì)在例行的基礎(chǔ)程序中使用它,因?yàn)樗?。我很少在有許多MMX指令的程序中再使用很多浮點(diǎn)指令。反之依然(vice versa)。所以我經(jīng)常在做任何浮點(diǎn)運(yùn)算之前等著執(zhí)行EMMS。如果你有許多浮點(diǎn)和很少的MMX,那么應(yīng)在你調(diào)用的所有的MMX子程序(如果有的話)執(zhí)行完再執(zhí)行EMMS。然而,在每個(gè)執(zhí)行MMX的子程序中加入EMMS會(huì)使代碼變慢。
<41>轉(zhuǎn)換到MMX,SSE,SSE2
你的代碼能轉(zhuǎn)換到MMX, SSE, 或者SSE2嗎?如果能,你可以并行處理來(lái)極大的提升速度。
<42>預(yù)取數(shù)據(jù)
這往往未被充分利用。如果你處理一個(gè)非常大的數(shù)組(256KB或更大),在P3及以后處理器上使用PREFETCXH指令可以使你的代碼無(wú)論在什么地方都可以提速10-30%。但事實(shí)上,如果你沒(méi)有合理使用的話,代碼性能還可能降級(jí)。在這方面,“展開(kāi)”表現(xiàn)良好,因?yàn)槲野阉归_(kāi)為用此指令預(yù)取的字節(jié)的整數(shù)倍。在P3上,此倍數(shù)為32,而在P4上,它是128。這就意味著你可以在P4機(jī)上很容易地展開(kāi)循環(huán)來(lái)一次處理128字節(jié),你可以從預(yù)取和展開(kāi)中得到好處。但并不是在每種情況下展開(kāi)為128字節(jié)都會(huì)有最好的提速。你可以嘗試不同的字節(jié)數(shù)。
UNROLL_NUM_BYTES equ 4 ; 一次循環(huán)要處理的字節(jié)數(shù)
UNROLL_AMT equ 128/UNROLL_NUM_BYTES ;我們想展開(kāi)循環(huán)以使它每次處理128字節(jié)
mov ecx,1024
looper:
offset2 = 0
REPEAT UNROLL_AMT
prefetchnta [edi+offset2+128] ; 在我們需要之前預(yù)取128字節(jié)到L1緩存
add eax,[edi+offset2]
offset2 = offset2 + UNROLL_NUM_BYTES
add edi,UNROLL_AMT * UNROLL_NUM_BYTES ; 我們處理16*4字節(jié)
sub ecx,UNROLL_AMT ; 調(diào)整迭代器到下一循環(huán)
jnz looper
<43>緩存模塊化(Cache Blocking)
不得不說(shuō),你必須對(duì)內(nèi)存中的大數(shù)組調(diào)用許多過(guò)程(子函數(shù))。你最好把數(shù)組分成塊兒并裝入緩存(Cache)來(lái)減少緩存未命中(cache miss)。例如:你正在執(zhí)行3D代碼,第一個(gè)過(guò)程可能傳遞坐標(biāo),第二個(gè)過(guò)程可能是規(guī)模大小,第三個(gè)可能是變換方式。所以與其在整個(gè)的的大數(shù)組里翻箱倒柜似地找,你應(yīng)該把數(shù)據(jù)大塊(chunk)分成適于緩存(Cache)的小塊。然后調(diào)用此3個(gè)過(guò)程。然后進(jìn)行下面數(shù)據(jù)大塊的類似操作。
<44>TLB啟動(dòng)(TLB Priming)
TLB就是旁路轉(zhuǎn)換緩沖,或稱頁(yè)表緩沖(Translation Lookaside Buffer),是虛擬內(nèi)存地址到物理內(nèi)存地址的轉(zhuǎn)換部件。它通過(guò)對(duì)頁(yè)表入口點(diǎn)的快速訪問(wèn)來(lái)提升性能。未在TLB緩存的代碼或數(shù)據(jù)會(huì)促使緩存未命中,這就降低了代碼速度。解決方法就是:在你必須讀某頁(yè)之前預(yù)讀該頁(yè)的一個(gè)數(shù)據(jù)字節(jié)。我會(huì)在后面的tips中提供一個(gè)例子。
<45>混入代碼來(lái)消除依賴
在C代碼中,C編譯器把不同的代碼塊分別對(duì)待。當(dāng)進(jìn)入?yún)R編語(yǔ)言級(jí)別時(shí),你可以混入(intermix)它們來(lái)消除依賴。
<46>并行處理
許多編譯器沒(méi)有充分利用CPU有2個(gè)ALU流水線的事實(shí),而ALU是人們使用的大部分。在P4機(jī)上你會(huì)更得意----如果操作得當(dāng),你可以在一個(gè)指令周期執(zhí)行4個(gè)ALU運(yùn)算指令。如果你把任務(wù)分配,并行處理之,這也會(huì)消除依賴。真是一箭雙雕!在循環(huán)中采取這個(gè)策略。
looper:
mov eax,[esi]
xor eax,0E5h ;依賴上一行
add [edi],eax ;依賴上一行
add esi,4
add edi,4
dec ecx
jnz looper
;那么我們?nèi)绾问顾⑿谢⑶覝p少依賴呢?
looper:
mov eax,[esi]
mov ebx,[esi+4]
xor eax,0E5
xor ebx,0E5
add [edi],eax
add [edi+4],ebx
add esi,8
add edi,8
sub ecx,2
jnz looper
<47>避免內(nèi)存訪問(wèn)
重新構(gòu)建代碼來(lái)避免內(nèi)存訪問(wèn)(或者其他I/O操作)。一種方法就是在向內(nèi)存寫一個(gè)值的時(shí)候,先在一個(gè)寄存器中累加它。下面給出一個(gè)例子。在這個(gè)例子里,假設(shè)每次循環(huán)我們從源數(shù)組向目的數(shù)組(元素為dword大小)連續(xù)相加3個(gè)字節(jié)的值。目的數(shù)組已經(jīng)置0。
mov ecx,AMT_TO_LOOP
looper:
movzx byte ptr eax,[esi]
add [edi],eax
movzx byte ptr eax,[esi+1]
add [edi],eax
movzx byte ptr eax,[esi+3]
add [edi],eax
add edi,4
add esi,3
dec ecx
jnz looper
;我們可以在寄存器中累加結(jié)果,然后只需向內(nèi)存寫一下即可。
mov ecx,AMT_TO_LOOP
looper:
xor edx,edx ;置0以存儲(chǔ)結(jié)果
movzx byte ptr eax,[esi]
add edx,eax
movzx byte ptr eax,[esi+1]
add edx,eax
movzx byte ptr eax,[esi+3]
add edx,eax
add esi,3
mov [edi],edx
add edi,4
dec ecx
jnz looper
<48>何時(shí)轉(zhuǎn)換call為jump
如果子程序的最后一個(gè)語(yǔ)句是一個(gè)call,考慮把它轉(zhuǎn)換為一個(gè)jump來(lái)減少一個(gè)call/ret。
<49>使用數(shù)組作為數(shù)據(jù)結(jié)構(gòu)
(這個(gè)tip不是只針對(duì)匯編的,但匯編表現(xiàn)更優(yōu)異)你可以使用一個(gè)數(shù)組來(lái)實(shí)現(xiàn)數(shù)據(jù)的結(jié)構(gòu)(例如樹和鏈表)。通過(guò)使用數(shù)組,內(nèi)存會(huì)無(wú)縫連接,代碼會(huì)因更少的緩存未命中而提速。
;=========================高級(jí)=========================
<50>避免前綴
盡量避免使用前綴。段超越(segment overrides),分支標(biāo)志(branch hints),操作數(shù)大小強(qiáng)制轉(zhuǎn)換(operand-size override),地址大小強(qiáng)制轉(zhuǎn)換(address-size override),封鎖數(shù)據(jù)指令(LOCKs),重復(fù)前綴(REPs)等都會(huì)產(chǎn)生前綴。前綴會(huì)增加指令的長(zhǎng)度,執(zhí)行時(shí)間也有所延長(zhǎng)。
<51>將代碼中的讀/寫操作分組
如果bus總線上有許多交互的讀命令和寫命令,考慮分組。同一時(shí)間處理更多的讀和寫命令。下面的是我們要避免的:
mov eax,[esi]
mov [edi],eax
mov eax,[esi+4]
mov [edi+4],eax
mov eax,[esi+8]
mov [edi+8],eax
mov eax,[esi+12]
mov [edi+12],eax
;將讀和寫指令分組
mov eax,[esi]
mov ebx,[esi+4]
mov ecx,[esi+8]
mov edx,[esi+12]
mov [edi],eax
mov [edi+4],ebx
mov [edi+8],ecx
mov [edi+12],edx
<52>充分利用CPU執(zhí)行單元(EU,execution units)來(lái)加速代碼
選擇在不同處理單元執(zhí)行的命令。如果你能合理地這樣做的話,執(zhí)行代碼的時(shí)間會(huì)等于吞吐時(shí)間(throughput time)而沒(méi)有延遲時(shí)間(latency time)。對(duì)許多指令來(lái)說(shuō),吞吐時(shí)間是較少的。
<53>交錯(cuò)2個(gè)循環(huán)以同步執(zhí)行
你可以展開(kāi)一個(gè)循環(huán)2次,而不是一條接另一條的運(yùn)行命令,你可以同步運(yùn)行它們。這為什么很有用?有2個(gè)原因。第一,有時(shí)你要執(zhí)行必須使用某個(gè)寄存器的指令并且這些指令有很長(zhǎng)的延遲。例如MUL/DIV,兩個(gè)連在一起的MUL指令會(huì)產(chǎn)生對(duì)EDX:EAX的依賴和爭(zhēng)用。第二,有時(shí)一些指令本身就有很長(zhǎng)的延遲。所以,你自然想嘗試在在一個(gè)循環(huán)后面放置一些來(lái)自另一個(gè)循環(huán)的指令來(lái)減少延遲直到它返回結(jié)果。P4機(jī)上的許多MMX, SSE和SSE2指令都采取這個(gè)策略。這兒有一個(gè)例子循環(huán)。
loop:
A1 ; instruction 1 loop 1
D2 ; instruction 4 loop 2
B1 ; instruction 2 loop 1
A2 ; instruction 1 loop 2
C1 ; instruction 3 loop 1
B2 ; instruction 2 loop 2
D1 ; instruction 4 loop 1
C2 ; instruction 3
loop 2
<54>使用MMX/SSE/SSE2時(shí)比較指令設(shè)置的標(biāo)志
當(dāng)與MMX/SSE/SSE2打交道時(shí),比較指令會(huì)產(chǎn)生對(duì)標(biāo)志位的設(shè)置。在某些情況下,當(dāng)你搜索一個(gè)文件中的模式(例如換行符)時(shí),這會(huì)很有用。所以你可以使用它來(lái)搜索模式,而不僅僅來(lái)做數(shù)學(xué)運(yùn)算。你可以使用MMX/SSE/SSE2中比較指令產(chǎn)生的標(biāo)志來(lái)控制部分MMX或SSE寄存器上的數(shù)學(xué)運(yùn)算。例如下面的代碼片段:如果有5,把9加到它MMX寄存器的dword部分。
; if (fredvariable == 5)
; fredvariable += 9;
;-------------------------------
movq mm5,[two_fives] ;mm5有兩個(gè)DWORD 5在里面
movq mm6,[two_nines] ;mm6有兩個(gè)DWORD 9在里面
movq mm0,[array_in_memory] ;取值
movq mm1,mm0 ;回寫
pcmpeqd mm1,mm5 ;mm1現(xiàn)在在每個(gè)DWORD位置都為FFFFFFFF
;在MM1有一個(gè)5,其他所有位置都為0
pand mm1,mm6 ;把MM6中不為5的位置置0
paddd mm0,mm1 ;只向MM0中值為5的位置加9
<55>PSHUFD和PUSHFW指令
在P4的MMX,SSE和SSE2中,移動(dòng)指令(MOV系列)速度慢。你可以在SSE和SSE2中使用"pushfd",MMX中使用"pushfw"來(lái)避免如上情況。它快2指令周期呢。但有一個(gè)警告:它是與微指令被分配加載到哪個(gè)流水線有關(guān)的。而沒(méi)有掌握更多技術(shù)的時(shí)候,有時(shí)使用慢點(diǎn)的"MOVDQA"會(huì)比替代它的"PUSHFD"快。所以你要對(duì)你的代碼精打細(xì)算。
pushfd xmm0,[edi],0E4h ;拷貝EDI指向位置的16字節(jié)到XMM0。0E4h會(huì)直接拷貝。
pushfw mm0,[edi],0E4h ;拷貝EDI指向位置的8字節(jié)到MM0。0E4h會(huì)直接拷貝。
<56>直接寫內(nèi)存---繞開(kāi)緩存(cache)
這是另一個(gè)優(yōu)化內(nèi)存處理的策略。如果你必須向許多內(nèi)存空間(256KB及以上)進(jìn)行寫操作,繞開(kāi)緩存直接向內(nèi)存寫更快!如果你的CPU是P3,你可以使用"movntq"或"movntps"指令。前者執(zhí)行8字節(jié)的寫操作,而后者是16字節(jié)。16-byte寫需要16字節(jié)對(duì)齊。在P4上,你還可以使用"movntdq",它也可以用于16字節(jié),但必須16字節(jié)對(duì)齊。這個(gè)方法在內(nèi)存填充和內(nèi)存拷貝中均適用,二者都做寫操作。這里有一些樣本代碼。我必須自己動(dòng)手并行使用8個(gè)XMM寄存器來(lái)幫助消除P4機(jī)MOVDQA指令的一些延遲。然而,為了幫助理解,我沒(méi)那么做。
mov ecx,16384 ;寫16384個(gè)16-bit值,16384*16 = 256KB
;所以我們正在拷貝一個(gè)256KB的數(shù)組
mov esi,offset src_arr ;指向必須以16-bit對(duì)齊的源數(shù)組的指針,否則會(huì)產(chǎn)生異常
mov edi,offset dst_arr ;指向必須以16-bit對(duì)齊的目的數(shù)組的指針,否則會(huì)產(chǎn)生異常
looper:
movdqa xmm0,[esi] ;工作在P3及以上
movntps [edi],xmm0 ;工作在P3及以上
add esi,16
add edi,16
dec ecx
jnz looper
<57>使用MMX/SSE/SSE2時(shí)每個(gè)循環(huán)處理2個(gè)事件
在P4上,MMS/SSE/SSE2指令的延遲那么長(zhǎng)以至于我總是每個(gè)循環(huán)處理2個(gè)事件或者提前讀取一個(gè)循環(huán)。如果你有足夠的寄存器,可以多于2個(gè)事件。所有的各種各樣的MOVE(包括MOVD)指令在P4上的速度都慢。所以2個(gè)32-bit的數(shù)字?jǐn)?shù)組相加運(yùn)算在P4上比P3上還慢。一個(gè)快點(diǎn)兒的方法可能就是每個(gè)循環(huán)(這個(gè)循環(huán)在FRED標(biāo)號(hào)之前預(yù)讀循環(huán)初始值MM0和MM1)處理兩個(gè)事件。你必須做的只是在數(shù)組元素個(gè)數(shù)為奇時(shí)進(jìn)行特殊的處理;在最后檢查一下,如果為奇數(shù),加一個(gè)額外的dword。這兒有個(gè)并沒(méi)有提前讀取值的代碼段。我想,把它改為提前讀取值是很容易的,所以我沒(méi)有兩個(gè)都貼出。下面的代碼可以:在P4機(jī)上避免ADC這個(gè)速度慢的指令來(lái)把兩個(gè)數(shù)組相加。
pxor mm7,mm7 ; the previous loops carry stays in here
fred:
movd mm0,[esi] ; esi points to src1
movd mm1,[edi] ; edi points to src2, also to be used as the destination
paddq mm0,mm1 ; add both values together
paddq mm0,mm7 ; add in remainder from last add
movd [edi],mm0 ; save value to memory
movq mm7,mm0
psrlq mm7,32 ; shift the carry over to bit 0
add esi,8
add edi,8
sub ecx,1
jnz fred
movd [edi],mm7 ; save carry
<58>預(yù)讀MMX或XMM寄存器來(lái)規(guī)避長(zhǎng)時(shí)間的延遲
在需要之前預(yù)讀一個(gè)SSE2寄存器會(huì)提升速度。這是因?yàn)镸OVDQA指令在P4上花費(fèi)6 cycles。這確實(shí)慢。鑒于它有如此長(zhǎng)之延遲,我想在確定不會(huì)產(chǎn)生阻礙的地方提前讀取它。這里有一個(gè)例子。
movdqa xmm1,[edi+16] ;在我們需要之前讀取入XMM1,P4上花費(fèi)6 cycles,不包括從緩存取的時(shí)間。
por xmm5,xmm0 ;做OR運(yùn)算,XMM0已預(yù)讀。P4上花費(fèi)2 cycles。
pand xmm6,xmm0 ;做AND運(yùn)算,XMM0已預(yù)讀。P4上花費(fèi)2 cycles。
movdqa xmm2,[edi+32] ;在我們需要之前預(yù)讀入XMM2,P4上花費(fèi)6 cycles,不包括從緩存取的時(shí)間。
por xmm5,xmm1 ;做OR運(yùn)算,XMM1已預(yù)讀。P4上花費(fèi)2 cycles。
pand xmm6,xmm1 ;做AND運(yùn)算,XMM1已預(yù)讀。P4上花費(fèi)2 cycles。
<59>在一個(gè)或多個(gè)寄存器中累加一個(gè)結(jié)果來(lái)避免執(zhí)行慢的指令
在一個(gè)或多個(gè)寄存器中累加一個(gè)結(jié)果來(lái)避免執(zhí)行慢的指令。我用這個(gè)策略加速用SSE2寫的比較/讀循環(huán)。比較慢的指令是PMOVMSKB。所以,我累加結(jié)果在一個(gè)寄存器中而不是每次循環(huán)都執(zhí)行這個(gè)指令。對(duì)每個(gè)4KB的內(nèi)存讀操作,我會(huì)用PMOVMSKB,它會(huì)很大地提速。下面我們通過(guò)分析一個(gè)使用PREFETCH和TLB啟動(dòng)的例子來(lái)證明。下面的代碼有2個(gè)循環(huán)。內(nèi)層循環(huán)被展開(kāi)來(lái)處理128字節(jié)(P4機(jī)上PREFETCH指令的預(yù)取字節(jié)數(shù))。另一個(gè)循環(huán)被展開(kāi)為4KB。所以我可以使用TLB啟動(dòng)。如果你使用的系統(tǒng)沒(méi)有使用4KB頁(yè)大小,你不得不適當(dāng)?shù)匦薷哪愕拇a。在擁有最大6.4 GB/s內(nèi)存帶寬的戴爾服務(wù)器(Dell Server)系統(tǒng)上,我測(cè)試了這段代碼。我能夠以5.55 GB/s做讀和比較操作(在沒(méi)有Windows環(huán)境下。在Windows環(huán)境下會(huì)運(yùn)行地慢點(diǎn))。我遺漏標(biāo)號(hào)"compare_failed"的代碼有2個(gè)原因:1)剪切/粘貼的代碼已經(jīng)夠多了;2)它沒(méi)有論證任何我要展現(xiàn)的技術(shù)。"compare_failed"的代碼只是簡(jiǎn)單地(在PCMPEQD找到失敗地址所屬的最近的4KB內(nèi)存塊后)做一個(gè)REP SCASD來(lái)找到失敗的地址。這個(gè)例子有非常巨大的代碼量,所以我把它放在最后以免你讀它的時(shí)候睡著;)(譯者注:感覺(jué)下面的代碼注釋翻譯出來(lái)有點(diǎn)別扭,而且原文也不難理解。故略。)
read_compare_pattern_sse2 proc near
mov edi,[start_addr] ;Starting Address
mov ecx,[stop_addr] ;Last addr to NOT test.
mov ebx,0FFFFFFFFh ;AND mask
movd xmm6,ebx ;AND mask
pshufd xmm6,xmm6,00000000b ;AND mask
movdqa xmm0,[edi] ;Get first 16 bytes
mov eax,[pattern] ;EAX holds pattern
pxor xmm5,xmm5 ;OR mask
movd xmm7,eax ;Copy EAX to XMM7
pshufd xmm7,xmm7,00000000b ;Blast to all DWORDS
outer_loop:
mov ebx,32 ;128 32 byte blocks
mov esi,edi ;save start of block
if DO_TLB_PRIMING
mov eax,[edi+4096] ;TLB priming
endif ;if DO_TLB_PRIMING
fred_loop:
movdqa xmm1,[edi+16] ;read 16 bytes
por xmm5,xmm0 ;OR into mask
pand xmm6,xmm0 ;AND into mask
movdqa xmm2,[edi+32] ;read 16 bytes
por xmm5,xmm1 ;OR into mask
pand xmm6,xmm1 ;AND into mask
movdqa xmm3,[edi+48] ;read 16 bytes
por xmm5,xmm2 ;OR into mask
pand xmm6,xmm2 ;AND into mask
movdqa xmm0,[edi+64] ;read 16 bytes
por xmm5,xmm3 ;OR into mask
pand xmm6,xmm3 ;AND into mask
movdqa xmm1,[edi+80] ;read 16 bytes
por xmm5,xmm0 ;OR into mask
pand xmm6,xmm0 ;AND into mask
movdqa xmm2,[edi+96] ;read 16 bytes
por xmm5,xmm1 ;OR into mask
pand xmm6,xmm1 ;AND into mask
movdqa xmm3,[edi+112] ;read 16 bytes
por xmm5,xmm2 ;OR into mask
pand xmm6,xmm2 ;AND into mask
por xmm5,xmm3 ;OR into mask
prefetchnta [edi+928] ;Prefetch 928 ahead
pand xmm6,xmm3 ;AND into mask
add edi,128 ;Go next 128byteblock
cmp edi,ecx ;At end?
jae do_compare ;No, jump
movdqa xmm0,[edi] ;read 16 bytes
sub ebx,1 ;Incr for inner loop
jnz fred_loop
do_compare:
pcmpeqd xmm5,xmm7 ;Equal?
pmovmskb eax,xmm5 ;Grab high bits in EAX
cmp eax,0FFFFh ;all set?
jne compare_failed ;No, exit failure
mov edx,0FFFFFFFFh ;AND mask
pxor xmm5,xmm5
pcmpeqd xmm6,xmm7 ;Equal?
pmovmskb eax,xmm6 ;Grab high bits in EAX
cmp eax,0FFFFh ;All Set?
jne compare_failed ;No, exit failure
movd xmm6,edx ;AND mask
pshufd xmm6,xmm6,00000000b ;AND mask
cmp edi,ecx ;We at end of range
jb outer_loop ;No, loop back up
jmp compare_passed ;Done!!! Success!!!
<60>在循環(huán)內(nèi)預(yù)取距離和位置
你會(huì)注意到,在上面的例子中,我之前預(yù)取了928字節(jié)而不是128字節(jié)(128是P4機(jī)上的預(yù)取字節(jié)數(shù))。為什么?Intel建議在循環(huán)開(kāi)始前預(yù)取128字節(jié)(2 cache lines)。但兩種不同取法(在循環(huán)開(kāi)始處或提前預(yù)取128字節(jié))都會(huì)出錯(cuò)。我既沒(méi)有在循環(huán)開(kāi)始時(shí)預(yù)取也沒(méi)之前預(yù)取128字節(jié)。為什么?當(dāng)我研究這段代碼時(shí),我發(fā)現(xiàn)把PREFETCH指令放到循環(huán)周圍并且改變它預(yù)取的偏移量可以使它運(yùn)行得更快。所以反常得是,我寫代碼來(lái)嘗試所有的循環(huán)內(nèi)預(yù)取指令的位置和開(kāi)始預(yù)取的偏移量的組合情況。這段代碼寫成一個(gè)匯編文件,而且把周圍的PREFETCH指令移到循環(huán)內(nèi),同時(shí)修改開(kāi)始預(yù)取的偏移量。然后一個(gè)bat文件編譯這個(gè)修改的代碼并且運(yùn)行一個(gè)基準(zhǔn)點(diǎn)(benchmark)。我運(yùn)行了這個(gè)基準(zhǔn)點(diǎn)幾個(gè)小時(shí)來(lái)嘗試不同的組合情況(我在預(yù)取距離為32時(shí)開(kāi)始,逐步增加距離直到距離達(dá)到1024)。在此系統(tǒng)上,我寫的基于928字節(jié)而不是128字節(jié)的代碼執(zhí)行地更快。并且,幾乎在循環(huán)結(jié)束處預(yù)取是最快的(在do_compare標(biāo)號(hào)之前,PREFETCHNTA指令大約8條line)。
THE END.
txt打包下載 huibianyouhuats_jb51
;=========================初級(jí)=========================
<1>釋放所有8-CPU寄存器,以使它們用于你自己的代碼中
復(fù)制代碼 代碼如下:
push ebx
push esi
push edi
push ebp ;必須在改變ESP之前完成
;裝載ESI、EDI和其他傳遞棧中值的寄存器,這必須在釋放ESP之前完成。
movd mm0,esp ; 沒(méi)有入棧/出棧操作超過(guò)此處,
xor ebx,ebx ; 所以你怎樣保存呢?一個(gè)變量嘛!
mov esp,5
inner_loop:
mov [eax_save],eax ; eax_save是一個(gè)全局變量不可能是局
; 部的,因?yàn)槟且驟BP來(lái)指向它。
add ebx,eax
mov eax,[eax_save] ; 存儲(chǔ) eax
movd esp,mm0 ; 必須在做POPs之前完成
pop ebp
pop edi
pop esi
pop ebx
ret
<2>寄存器的最大化使用
多數(shù)高級(jí)點(diǎn)的編譯器會(huì)產(chǎn)生非常多的到內(nèi)存變量的訪問(wèn)。我通常避免那樣做,因?yàn)槲冶M力把這些變量放在寄存器中。所以要使8-CPU寄存器自由使用,以備燃眉之急~
<3>復(fù)雜指令
避免復(fù)雜指令(lods, stos, movs, cmps, scas, loop, xadd, enter, leave)。復(fù)雜指令就是那些可以做許多事情的指令。例如,stosb向內(nèi)存寫1字節(jié),同時(shí)增加EDI。 這些復(fù)雜指令阻止早期的Pentium快速運(yùn)行,因?yàn)樘幚砥鞯陌l(fā)展趨勢(shì)是力使自己更像精簡(jiǎn)指令集計(jì)算機(jī)(RISC)。使用rep和字符串指令依舊很快----而這是唯一的例外。
<4>不要再P4上使用INC/DEC
在P4上用ADD/SUB代替INC/DEC。通常前者更快些。ADD/SUB耗費(fèi)0.5 cycle而INC/DEC耗費(fèi)1 cycle.
<5>循環(huán)移位(rotating)
避免用寄存器作循環(huán)移位計(jì)數(shù)器;立即數(shù)作計(jì)數(shù)器時(shí),除了1,不要用其他值。
<6>消除不必要的比較指令
通過(guò)合理運(yùn)用條件跳轉(zhuǎn)指令來(lái)消除不必要的比較指令,但是,這些條件跳轉(zhuǎn)指令時(shí)基于標(biāo)志位的。而這些標(biāo)志位已經(jīng)被前面的算術(shù)指令設(shè)置了。
dec ecx
cmp ecx,0
jnz loop_again
優(yōu)化為:
dec ecx
jnz loop_again
<7>lea指令的使用
lea非???,除了在P4上它表現(xiàn)得慢一點(diǎn)。你可以在這一條指令中執(zhí)行許多數(shù)學(xué)運(yùn)算,并且它不會(huì)影響標(biāo)志寄存器。所以你可以把它放在一個(gè)正在被修改的寄存器和一個(gè)涉及標(biāo)志位的條件跳轉(zhuǎn)指令中間。
top_of_loop:
dec eax
lea edx,[edx*4+3] ; 乘4加3。不影響標(biāo)志位
jnz top_of_loop ; 所以jnz判斷的是dec eax執(zhí)行后的狀態(tài)字
<8>ADC和SBB的使用
許多編譯器都不充分利用ADC和SBB。你可以用它們很好地提速,如:把兩個(gè)64-bit的數(shù)加在一起或者兩個(gè)大數(shù)相加。記牢:ADC和SBB在P4上慢。當(dāng)你手頭有類似工作時(shí),你可以使用addq和用MMX來(lái)完成。所以第二個(gè)優(yōu)化建議是:用MMX做這樣的加減法。但你的處理器必須支持MMX。
add eax,[fred]
adc edx,[fred+4]
;下面的3條指令完成同樣的功能
movd mm0,[fred] ;得到MM0中的32-bit的值
movd mm1,[fred+4] ;得到MM1中的32-bit的值
paddq mm0,mm1 這是一種未優(yōu)化的方法,實(shí)際上,你可以在前面的循環(huán)中預(yù)取MM0和MM1。我這樣做是為了好理解。
<9>ROL, ROR, RCL, RCR 和 BSWAP
使用BSWAP指令把Big Endian數(shù)據(jù)轉(zhuǎn)換成Little Endian格式是很酷的一種方法。你也可以使用它臨時(shí)存儲(chǔ)寄存器的高位的的值(16-bit或8-bit)。類似地,你可以使用ROL/ROR來(lái)存儲(chǔ)8-bit或16-bit值。這也是獲得更多“寄存器”的一種方法。如果你處理的都是16-bit的值,你可以把你的8個(gè)32-bit的寄存器轉(zhuǎn)換成16個(gè)16-bit的寄存器。這樣你就有更多寄存器可以使用了。RCL和RCR可以很容易地被使用在計(jì)算寄存器中比特位(0 or 1)的多少。牢記:P4上ROL, ROR, RCL, RCR 和 BSWAP速度慢。循環(huán)移位指令大約比BSWAP快2倍。所以,如果你必須在P4機(jī)上使用上述指令,還是選循環(huán)移位指令吧。
復(fù)制代碼 代碼如下:
xor edx,edx ; 設(shè)置兩個(gè)16-bit的寄存器為0
mov dx,234 ; 設(shè)置低位寄存器為234
bswap edx ; 高低位寄存器互換
mov dx,345 ; 設(shè)置當(dāng)前低位寄存器為345
bswap edx ; 當(dāng)前高低位互換
add dx,5 ; 當(dāng)前低位寄存器加5
bswap edx ; 高低位互換
add dx,7 ; 當(dāng)前低位寄存器加7
<10>字符串指令
許多編譯器沒(méi)有充分利用字符串指令( scas, cmps, stos, movs和 lods)。所以檢測(cè)這些指令寫成的函數(shù)是否比一些庫(kù)函數(shù)速度快是非常有意義的。例如:當(dāng)我在看VC++的strlen()函數(shù)時(shí),我真的很驚訝。在radix40代碼中,處理一個(gè)100字節(jié)的字符串,它竟跑了416 cycles ?。。∥艺J(rèn)為這慢地荒誕?。?!
<11>以乘代除
If you have a full 32-bit number and you need to divide, you can simply do a multiply
and take the top 32-bit half as the result. This is faster because multiplication is
faster than division. ( thanks to pdixon for the tip).
如果你有一個(gè)滿32-bit(full 32-bit)的數(shù)要做除法,你可以簡(jiǎn)單地做乘法,然后取高32-bit部分作為結(jié)果。這樣會(huì)快些,因?yàn)槌朔ū瘸欤?感謝pdixon提供了這個(gè)tip)(譯者注:由于水平有限,此tip翻譯尚待商討,而且不能給出一個(gè)例子。還仰仗各位幫忙。)
<12>被常數(shù)除
這兒有一些很好的信息----怎樣被常數(shù)除(在Agner Fog的pentopt.pdf中)。我(Mark Larson)寫了一個(gè)小程序:可以根據(jù)你輸入的除數(shù)自動(dòng)產(chǎn)生匯編代碼序列。我會(huì)繼續(xù)探究探究,然后貼出來(lái)。這是Agner的文檔的鏈接。Agner's Pentopt PDF (http://www.agner.org/assem/)
<13>展開(kāi)(Unrolling)
這是一個(gè)指導(dǎo)方針。在一般優(yōu)化策略里,“展開(kāi)”往往被忽略,因此我想為它加個(gè)注腳。我總是用數(shù)值總數(shù)相等的宏來(lái)建立我的“展開(kāi)”結(jié)構(gòu)。那樣的話,你可以嘗試不同的值來(lái)看看哪種是最容易的。你希望這個(gè)展開(kāi)“安身”于L1代碼緩存(或追蹤緩存,trace cache)。使用等價(jià)(equate)語(yǔ)句可以很容易地嘗試不同的展開(kāi)值來(lái)達(dá)到最快的速度。
UNROLL_AMT equ 16 ; # 展開(kāi)循環(huán)的次數(shù)
UNROLL_NUM_BYTES equ 4 ; # 一次循環(huán)處理的字節(jié)數(shù)
mov ecx,1024
looper:
offset2 = 0
REPEAT UNROLL_AMT
add eax,[edi+offset2]
offset2 = offset2 + UNROLL_NUM_BYTES
add edi,UNROLL_AMT * UNROLL_NUM_BYTES ; 按16*4字節(jié)來(lái)處理
sub ecx,UNROLL_AMT ; 從循環(huán)計(jì)數(shù)器中減去展開(kāi)循環(huán)的次數(shù)
jnz looper
<14>MOVZX指令
使用MOVZX來(lái)避免偏愛(ài)的寄存器受阻。我使用MOVZX非常多。許多人首先將滿32-bit寄存器(full 32-bit register)XOR一下。但是,MOVZX無(wú)需這個(gè)多余的XOR指令而可以完成同樣的事情。而且你必須提前做XOR運(yùn)算,之后才能使用寄存器,并且這個(gè)操作是花費(fèi)一定時(shí)間的。但有MOVZX在,你就無(wú)需擔(dān)心啦。
<15>用MOVZX來(lái)避免SHIFT和AND指令的使用
我用匯編來(lái)對(duì)C代碼中的位運(yùn)算提速。the_array是一個(gè)dword的數(shù)組。代碼功能是得到數(shù)組中一個(gè)dword的任意字節(jié)。Pass是一個(gè)值為0-3的變量。因此這個(gè)功能有如下C代碼:
unsigned char c = ((the_array[i])>>(Pass<<3)) & 0xFF;
; 我為了避免Pass變量的使用,展開(kāi)了循環(huán)4次。所以我得到了如下更多的代碼:
unsigned char c = (the_array[i])>>0) & 0xFF;
unsigned char c = (the_array[i])>>8) & 0xFF;
unsigned char c = (the_array[i])>>16) & 0xFF;
unsigned char c = (the_array[i])>>24) & 0xFF;
在匯編中,我摒棄SHIFT和AND會(huì)發(fā)生什么呢?節(jié)約了我2個(gè)指令!更不用提P4上SHIFT指令非常慢(4 cycles!!!)的事實(shí)。所以如果可能,盡量避免使用SHIFT指令。我們以第三行為例,所以只需右移16位即可:
mov eax,[esi] ; esi指向the_array數(shù)組
shr eax,16
and eax,0FFh
; 那么我們?cè)鯓颖苊釹HR和AND的使用呢?我們舉例對(duì)dword中的第三個(gè)字節(jié)做MOVZX運(yùn)算。
movzx eax,byte ptr [esi+2] ;unsigned char c = (the_array[i])>>16) & 0xFF;
<16>align指令
該偽指令非常重要,它可以對(duì)齊你的代碼和數(shù)據(jù)來(lái)很好地提速。對(duì)代碼,我通常按4字節(jié)邊界對(duì)齊。對(duì)于數(shù)據(jù)呢,2字節(jié)數(shù)據(jù)按2字節(jié)邊界對(duì)齊,4字節(jié)數(shù)據(jù)按4字節(jié)邊界對(duì)齊,8字節(jié)數(shù)據(jù)按8字節(jié)邊界對(duì)齊,16字節(jié)數(shù)據(jù)按16字節(jié)邊界對(duì)齊。一般情況,如果不以一個(gè)16-bit邊界對(duì)齊SSE或SSE2數(shù)據(jù)的話,將會(huì)產(chǎn)生一個(gè)異常。如果有VC++開(kāi)發(fā)包,你可以在VC++環(huán)境中對(duì)齊你的數(shù)據(jù)。VC++加入了對(duì)靜態(tài)數(shù)據(jù)和動(dòng)態(tài)內(nèi)存的支持。對(duì)于靜態(tài)數(shù)據(jù),你可以使用__declspec(align(4))來(lái)按4字節(jié)邊界對(duì)齊。
<17>用于2的冪的BSR指令
你可以把BSR命令用于變量的2的最高冪的計(jì)數(shù)。
<18>用XOR置寄存器為0
這是非常陳舊的常識(shí)了,但是我依然要重提一下。它也有一個(gè)側(cè)面好處----消除對(duì)寄存器的依賴性。這就是人們使用寄存器之前用XOR置零成風(fēng)的原因。但我寧愿使用MOVZX,因?yàn)閄OR很狡猾~(看看我前面對(duì)MOVZX的討論)在P4上,也增加了PXOR支持,以消除對(duì)XOR的依賴性。我認(rèn)為P3做了同樣的事情。
<19>使用XOR和DIV
如果你確定你的數(shù)據(jù)在做除法時(shí)是無(wú)符號(hào)數(shù),使用XOR EDX, EDX,然后DIV。它比CDQ和IDIV快。
<20>盡量避免明顯的依賴關(guān)系
如果你修改一個(gè)寄存器,然后在緊跟的下一行中讓它和某個(gè)值進(jìn)行比較,相反地,你最好在兩者之間插入其他的寄存器的修改指令。依賴就是任何時(shí)間你修改一個(gè)寄存器,然后緊跟著對(duì)它讀或?qū)憽?譯者注:其實(shí)就是AGI延遲; AGI: Address Generation Interlock)
inc edi
inc eax
cmp eax,1 ;這行依賴于上一行,所以產(chǎn)生延遲
jz fred
;移動(dòng)周圍的指令到這兒可以打破這種依賴
inc eax
inc edi
cmp eax,1
jz fred
<21>P4機(jī)上避免使用的指令
在P4機(jī)上避免使用如下指令: adc, sbb, rotate指令, shift指令, inc, dec, lea, 和任何耗費(fèi)多于 4 uops(微指令)的指令。你怎么知道當(dāng)前運(yùn)行代碼的處理器是P4?CPUID命令!
<22>使用查找表
在P4機(jī)上,你可以通過(guò)建立查找表,來(lái)規(guī)避長(zhǎng)時(shí)間延遲的指令(前面已列出來(lái))。幸而P4機(jī)的內(nèi)存速度很快,所以如果建立的查找表不在cache里,它也并不會(huì)很大地影響性能。
<23>使用指針而不是計(jì)算下標(biāo)(索引)
許多時(shí)候,C語(yǔ)言中的循環(huán)會(huì)出現(xiàn)兩個(gè)非冪數(shù)的乘法。你可以很容易地用加法來(lái)完成。下面是一個(gè)使用結(jié)構(gòu)體的例子:
typedef struct fred
{
int fred;
char bif;
}freddy_type;
freddy_type charmin[80];
freddy_type結(jié)構(gòu)的大小是5字節(jié)。如果你想在循環(huán)中訪問(wèn)它們,編譯器會(huì)產(chǎn)生此種代碼----對(duì)每個(gè)數(shù)組元素的訪問(wèn)都乘以5?。?!Ewwwwwwwwwwwww(譯者注:語(yǔ)氣詞)?。?!那么我們應(yīng)該怎樣做呢?
for ( int t = 0; t < 80; t++)
{
charmin[t].fred = rand(); // 編譯器為了得到偏移量,竟乘以了5,EWWWWWWWW!
charmin[t].bif = (char)(rand() % 256);
}
在匯編中,我們以偏移量0開(kāi)始,這指向了數(shù)據(jù)第一個(gè)元素。然后我們每次把循環(huán)迭代器加5來(lái)避免MUL指令的使用。
mov esi,offset charmin
mov ecx,80
fred_loop:
;...freddy_type結(jié)構(gòu)中FRED和BIF元素的處理命令
add esi,5 ;指向下一個(gè)結(jié)構(gòu)的入口地址
dec ecx
jnz fred_loop
MUL指令的規(guī)避也應(yīng)用在循環(huán)中。我曾見(jiàn)過(guò)人們?cè)谘h(huán)中以乘來(lái)實(shí)現(xiàn)變量增加或者終止條件。相反,你應(yīng)盡量用加法。
<24>遵從默認(rèn)分支預(yù)測(cè)
盡量設(shè)計(jì)你的代碼使向后的條件跳轉(zhuǎn)經(jīng)常發(fā)生,并且向前的條件跳轉(zhuǎn)幾乎從不發(fā)生。這顯然與分支預(yù)測(cè)有關(guān)。CPU中的靜態(tài)分支預(yù)測(cè)器(static branch predictor)只使用簡(jiǎn)單的規(guī)則來(lái)猜測(cè)一個(gè)條件跳轉(zhuǎn)是否發(fā)生。所以應(yīng)使向后跳轉(zhuǎn)的循環(huán)分支靠近結(jié)束。然后讓同一循環(huán)的特殊退出條件(exit condition)執(zhí)行一個(gè)向前的跳轉(zhuǎn)(這個(gè)跳轉(zhuǎn)只在此跳轉(zhuǎn)不經(jīng)常發(fā)生的這個(gè)特定的條件下退出)。
<25>消除分支
如果可能,消除分支!這是顯然的,但我見(jiàn)過(guò)許多人在他們的匯編代碼中使用了太多的分支。保持程序的簡(jiǎn)單。使用盡可能少的分支。
<26>使用CMOVcc來(lái)消除分支
我曾見(jiàn)到過(guò)CMOVcc指令確實(shí)比條件跳轉(zhuǎn)指令快。所以我建議使用CMOVcc而非條件跳轉(zhuǎn)指令。當(dāng)在你的跳轉(zhuǎn)不容易被分支預(yù)測(cè)邏輯猜到的情況下,它可能會(huì)更快。如果你遇到那種情況,設(shè)定基準(zhǔn)點(diǎn)(benchmark)看看!
<27>局部變量vs全局變量
在一個(gè)過(guò)程模塊(子函數(shù))中使用局部變量而不是全局變量。如果你使用局部變量,你會(huì)得到更少的緩存未命中(Cache miss)。
<28>地址計(jì)算
在你需要某地址之前計(jì)算它。不得不說(shuō)你為了得到某一特定地址必須計(jì)算一些令人討厭的東西。例如地址乘20。你可以在需要該地址的代碼之前預(yù)算它。
<29>小點(diǎn)兒的寄存器
有些時(shí)候,使用小點(diǎn)兒的寄存器可以提升速度。我在radix40代碼中驗(yàn)證了它。如果你使用EDX來(lái)修改下面的代碼,它跑得會(huì)慢些。
movzx edx,byte ptr [esi] ;從ascii數(shù)組取數(shù)據(jù)
test dl,ILLEGAL ;位測(cè)試
jnz skip_illegal
<30>指令長(zhǎng)度
盡量使你的指令大小保持在8字節(jié)以下。
<31>使用寄存器傳遞參數(shù)
如果可能,嘗試用寄存器傳遞參數(shù)而不是棧。如果你有3個(gè)變量要壓棧作為參數(shù),至少有6個(gè)內(nèi)存讀操作和3個(gè)內(nèi)存寫操作。你不得不把每個(gè)變量由內(nèi)存讀入CPU寄存器然后把它們壓入棧。這是3個(gè)內(nèi)存讀操作。然后壓向棧頂產(chǎn)生3個(gè)寫操作。然后為什么你會(huì)壓你從不使用的參數(shù)呢?所以,又出現(xiàn)了最少3個(gè)讀棧操作。(譯者注:兩內(nèi)存變量不能直接傳遞數(shù)據(jù))
<32>不要向棧傳遞大數(shù)據(jù)
不要向棧傳遞64-bit或128-bit的數(shù)據(jù)(或者更大)。相反,你應(yīng)該傳遞指向該數(shù)據(jù)的指針。
;=========================中級(jí)=========================
<33>加法方向
寄存器加向內(nèi)存比內(nèi)存加向寄存器速度更快。這與指令耗費(fèi)的微指令(micro-ops)的多少有關(guān)。所以,你知道該怎么做了。
add eax,[edi] ;如果可能,不要這樣做
add [edi],eax ;這才是首選
<34>指令選擇
盡量選取產(chǎn)生最少微指令和最少延遲的指令。
<35>未對(duì)齊字節(jié)數(shù)據(jù)流的雙字(dword)對(duì)齊處理
對(duì)于沒(méi)有4字節(jié)邊界對(duì)齊的緩沖區(qū),一次分解出一個(gè)dword會(huì)使性能湮沒(méi)。(譯者注:因?yàn)榈刂?2-bit對(duì)齊時(shí),處理速度最快)你可以通過(guò)處理開(kāi)始的X(0-3)字節(jié)直到遇到一個(gè)4字節(jié)對(duì)齊的邊界處的方法來(lái)規(guī)避這種情況。
<36>使用CMOVcc來(lái)復(fù)位一個(gè)無(wú)限循環(huán)指針
如果你想多次傳遞一個(gè)數(shù)組,并當(dāng)達(dá)到數(shù)組末端的時(shí)候復(fù)位到開(kāi)始處,你可以使用CMOVcc指令。
dec ecx ; 減少數(shù)組索引
cmovz ecx,MAX_COUNT ; 如果在開(kāi)始處,復(fù)位索引為 MAX_COUNT(結(jié)尾處)。
<37>以減法來(lái)代替乘以0.5的乘法
這可能不會(huì)對(duì)所有情況奏效除了real4變量乘以0.5,或者被2除(real4變量是浮點(diǎn)數(shù)),你只需把指數(shù)減1即可。但對(duì)0.0不奏效。對(duì)real8變量,要減去00100000h。(這個(gè)tip是donkey貼出來(lái)的,donkey posted this)
.data
somefp real4 2.5
.code
sub dword ptr [somefp],00800000h ;用2除real4
<38>自修改代碼
P4優(yōu)化手冊(cè)建議避免自修改代碼的使用。我曾經(jīng)見(jiàn)到過(guò)幾次自修改代碼可以跑得更快的情況。但是每次使用前你都要明確它在當(dāng)前情況下會(huì)更快。(譯者注:自修改代碼即INC/DEC)
<39>MMX, SSE, SSE2指令
許多編譯器對(duì)MMX,SSE和SSE2不會(huì)產(chǎn)生很好的代碼。GCC和Intel編譯器對(duì)此做了更多工作,所以較其他產(chǎn)品好一些。但是你自己的"大腦+手"編譯器在這項(xiàng)工作中的應(yīng)用仍然是很大的成功。
<40>使用EMMS
EMMS在Intel處理器上傾向?yàn)楹苈闹噶?。在AMD上它比較快。通常我不會(huì)在例行的基礎(chǔ)程序中使用它,因?yàn)樗?。我很少在有許多MMX指令的程序中再使用很多浮點(diǎn)指令。反之依然(vice versa)。所以我經(jīng)常在做任何浮點(diǎn)運(yùn)算之前等著執(zhí)行EMMS。如果你有許多浮點(diǎn)和很少的MMX,那么應(yīng)在你調(diào)用的所有的MMX子程序(如果有的話)執(zhí)行完再執(zhí)行EMMS。然而,在每個(gè)執(zhí)行MMX的子程序中加入EMMS會(huì)使代碼變慢。
<41>轉(zhuǎn)換到MMX,SSE,SSE2
你的代碼能轉(zhuǎn)換到MMX, SSE, 或者SSE2嗎?如果能,你可以并行處理來(lái)極大的提升速度。
<42>預(yù)取數(shù)據(jù)
這往往未被充分利用。如果你處理一個(gè)非常大的數(shù)組(256KB或更大),在P3及以后處理器上使用PREFETCXH指令可以使你的代碼無(wú)論在什么地方都可以提速10-30%。但事實(shí)上,如果你沒(méi)有合理使用的話,代碼性能還可能降級(jí)。在這方面,“展開(kāi)”表現(xiàn)良好,因?yàn)槲野阉归_(kāi)為用此指令預(yù)取的字節(jié)的整數(shù)倍。在P3上,此倍數(shù)為32,而在P4上,它是128。這就意味著你可以在P4機(jī)上很容易地展開(kāi)循環(huán)來(lái)一次處理128字節(jié),你可以從預(yù)取和展開(kāi)中得到好處。但并不是在每種情況下展開(kāi)為128字節(jié)都會(huì)有最好的提速。你可以嘗試不同的字節(jié)數(shù)。
UNROLL_NUM_BYTES equ 4 ; 一次循環(huán)要處理的字節(jié)數(shù)
UNROLL_AMT equ 128/UNROLL_NUM_BYTES ;我們想展開(kāi)循環(huán)以使它每次處理128字節(jié)
mov ecx,1024
looper:
offset2 = 0
REPEAT UNROLL_AMT
prefetchnta [edi+offset2+128] ; 在我們需要之前預(yù)取128字節(jié)到L1緩存
add eax,[edi+offset2]
offset2 = offset2 + UNROLL_NUM_BYTES
add edi,UNROLL_AMT * UNROLL_NUM_BYTES ; 我們處理16*4字節(jié)
sub ecx,UNROLL_AMT ; 調(diào)整迭代器到下一循環(huán)
jnz looper
<43>緩存模塊化(Cache Blocking)
不得不說(shuō),你必須對(duì)內(nèi)存中的大數(shù)組調(diào)用許多過(guò)程(子函數(shù))。你最好把數(shù)組分成塊兒并裝入緩存(Cache)來(lái)減少緩存未命中(cache miss)。例如:你正在執(zhí)行3D代碼,第一個(gè)過(guò)程可能傳遞坐標(biāo),第二個(gè)過(guò)程可能是規(guī)模大小,第三個(gè)可能是變換方式。所以與其在整個(gè)的的大數(shù)組里翻箱倒柜似地找,你應(yīng)該把數(shù)據(jù)大塊(chunk)分成適于緩存(Cache)的小塊。然后調(diào)用此3個(gè)過(guò)程。然后進(jìn)行下面數(shù)據(jù)大塊的類似操作。
<44>TLB啟動(dòng)(TLB Priming)
TLB就是旁路轉(zhuǎn)換緩沖,或稱頁(yè)表緩沖(Translation Lookaside Buffer),是虛擬內(nèi)存地址到物理內(nèi)存地址的轉(zhuǎn)換部件。它通過(guò)對(duì)頁(yè)表入口點(diǎn)的快速訪問(wèn)來(lái)提升性能。未在TLB緩存的代碼或數(shù)據(jù)會(huì)促使緩存未命中,這就降低了代碼速度。解決方法就是:在你必須讀某頁(yè)之前預(yù)讀該頁(yè)的一個(gè)數(shù)據(jù)字節(jié)。我會(huì)在后面的tips中提供一個(gè)例子。
<45>混入代碼來(lái)消除依賴
在C代碼中,C編譯器把不同的代碼塊分別對(duì)待。當(dāng)進(jìn)入?yún)R編語(yǔ)言級(jí)別時(shí),你可以混入(intermix)它們來(lái)消除依賴。
<46>并行處理
許多編譯器沒(méi)有充分利用CPU有2個(gè)ALU流水線的事實(shí),而ALU是人們使用的大部分。在P4機(jī)上你會(huì)更得意----如果操作得當(dāng),你可以在一個(gè)指令周期執(zhí)行4個(gè)ALU運(yùn)算指令。如果你把任務(wù)分配,并行處理之,這也會(huì)消除依賴。真是一箭雙雕!在循環(huán)中采取這個(gè)策略。
looper:
mov eax,[esi]
xor eax,0E5h ;依賴上一行
add [edi],eax ;依賴上一行
add esi,4
add edi,4
dec ecx
jnz looper
;那么我們?nèi)绾问顾⑿谢⑶覝p少依賴呢?
looper:
mov eax,[esi]
mov ebx,[esi+4]
xor eax,0E5
xor ebx,0E5
add [edi],eax
add [edi+4],ebx
add esi,8
add edi,8
sub ecx,2
jnz looper
<47>避免內(nèi)存訪問(wèn)
重新構(gòu)建代碼來(lái)避免內(nèi)存訪問(wèn)(或者其他I/O操作)。一種方法就是在向內(nèi)存寫一個(gè)值的時(shí)候,先在一個(gè)寄存器中累加它。下面給出一個(gè)例子。在這個(gè)例子里,假設(shè)每次循環(huán)我們從源數(shù)組向目的數(shù)組(元素為dword大小)連續(xù)相加3個(gè)字節(jié)的值。目的數(shù)組已經(jīng)置0。
mov ecx,AMT_TO_LOOP
looper:
movzx byte ptr eax,[esi]
add [edi],eax
movzx byte ptr eax,[esi+1]
add [edi],eax
movzx byte ptr eax,[esi+3]
add [edi],eax
add edi,4
add esi,3
dec ecx
jnz looper
;我們可以在寄存器中累加結(jié)果,然后只需向內(nèi)存寫一下即可。
mov ecx,AMT_TO_LOOP
looper:
xor edx,edx ;置0以存儲(chǔ)結(jié)果
movzx byte ptr eax,[esi]
add edx,eax
movzx byte ptr eax,[esi+1]
add edx,eax
movzx byte ptr eax,[esi+3]
add edx,eax
add esi,3
mov [edi],edx
add edi,4
dec ecx
jnz looper
<48>何時(shí)轉(zhuǎn)換call為jump
如果子程序的最后一個(gè)語(yǔ)句是一個(gè)call,考慮把它轉(zhuǎn)換為一個(gè)jump來(lái)減少一個(gè)call/ret。
<49>使用數(shù)組作為數(shù)據(jù)結(jié)構(gòu)
(這個(gè)tip不是只針對(duì)匯編的,但匯編表現(xiàn)更優(yōu)異)你可以使用一個(gè)數(shù)組來(lái)實(shí)現(xiàn)數(shù)據(jù)的結(jié)構(gòu)(例如樹和鏈表)。通過(guò)使用數(shù)組,內(nèi)存會(huì)無(wú)縫連接,代碼會(huì)因更少的緩存未命中而提速。
;=========================高級(jí)=========================
<50>避免前綴
盡量避免使用前綴。段超越(segment overrides),分支標(biāo)志(branch hints),操作數(shù)大小強(qiáng)制轉(zhuǎn)換(operand-size override),地址大小強(qiáng)制轉(zhuǎn)換(address-size override),封鎖數(shù)據(jù)指令(LOCKs),重復(fù)前綴(REPs)等都會(huì)產(chǎn)生前綴。前綴會(huì)增加指令的長(zhǎng)度,執(zhí)行時(shí)間也有所延長(zhǎng)。
<51>將代碼中的讀/寫操作分組
如果bus總線上有許多交互的讀命令和寫命令,考慮分組。同一時(shí)間處理更多的讀和寫命令。下面的是我們要避免的:
mov eax,[esi]
mov [edi],eax
mov eax,[esi+4]
mov [edi+4],eax
mov eax,[esi+8]
mov [edi+8],eax
mov eax,[esi+12]
mov [edi+12],eax
;將讀和寫指令分組
mov eax,[esi]
mov ebx,[esi+4]
mov ecx,[esi+8]
mov edx,[esi+12]
mov [edi],eax
mov [edi+4],ebx
mov [edi+8],ecx
mov [edi+12],edx
<52>充分利用CPU執(zhí)行單元(EU,execution units)來(lái)加速代碼
選擇在不同處理單元執(zhí)行的命令。如果你能合理地這樣做的話,執(zhí)行代碼的時(shí)間會(huì)等于吞吐時(shí)間(throughput time)而沒(méi)有延遲時(shí)間(latency time)。對(duì)許多指令來(lái)說(shuō),吞吐時(shí)間是較少的。
<53>交錯(cuò)2個(gè)循環(huán)以同步執(zhí)行
你可以展開(kāi)一個(gè)循環(huán)2次,而不是一條接另一條的運(yùn)行命令,你可以同步運(yùn)行它們。這為什么很有用?有2個(gè)原因。第一,有時(shí)你要執(zhí)行必須使用某個(gè)寄存器的指令并且這些指令有很長(zhǎng)的延遲。例如MUL/DIV,兩個(gè)連在一起的MUL指令會(huì)產(chǎn)生對(duì)EDX:EAX的依賴和爭(zhēng)用。第二,有時(shí)一些指令本身就有很長(zhǎng)的延遲。所以,你自然想嘗試在在一個(gè)循環(huán)后面放置一些來(lái)自另一個(gè)循環(huán)的指令來(lái)減少延遲直到它返回結(jié)果。P4機(jī)上的許多MMX, SSE和SSE2指令都采取這個(gè)策略。這兒有一個(gè)例子循環(huán)。
loop:
A1 ; instruction 1 loop 1
D2 ; instruction 4 loop 2
B1 ; instruction 2 loop 1
A2 ; instruction 1 loop 2
C1 ; instruction 3 loop 1
B2 ; instruction 2 loop 2
D1 ; instruction 4 loop 1
C2 ; instruction 3
loop 2
<54>使用MMX/SSE/SSE2時(shí)比較指令設(shè)置的標(biāo)志
當(dāng)與MMX/SSE/SSE2打交道時(shí),比較指令會(huì)產(chǎn)生對(duì)標(biāo)志位的設(shè)置。在某些情況下,當(dāng)你搜索一個(gè)文件中的模式(例如換行符)時(shí),這會(huì)很有用。所以你可以使用它來(lái)搜索模式,而不僅僅來(lái)做數(shù)學(xué)運(yùn)算。你可以使用MMX/SSE/SSE2中比較指令產(chǎn)生的標(biāo)志來(lái)控制部分MMX或SSE寄存器上的數(shù)學(xué)運(yùn)算。例如下面的代碼片段:如果有5,把9加到它MMX寄存器的dword部分。
; if (fredvariable == 5)
; fredvariable += 9;
;-------------------------------
movq mm5,[two_fives] ;mm5有兩個(gè)DWORD 5在里面
movq mm6,[two_nines] ;mm6有兩個(gè)DWORD 9在里面
movq mm0,[array_in_memory] ;取值
movq mm1,mm0 ;回寫
pcmpeqd mm1,mm5 ;mm1現(xiàn)在在每個(gè)DWORD位置都為FFFFFFFF
;在MM1有一個(gè)5,其他所有位置都為0
pand mm1,mm6 ;把MM6中不為5的位置置0
paddd mm0,mm1 ;只向MM0中值為5的位置加9
<55>PSHUFD和PUSHFW指令
在P4的MMX,SSE和SSE2中,移動(dòng)指令(MOV系列)速度慢。你可以在SSE和SSE2中使用"pushfd",MMX中使用"pushfw"來(lái)避免如上情況。它快2指令周期呢。但有一個(gè)警告:它是與微指令被分配加載到哪個(gè)流水線有關(guān)的。而沒(méi)有掌握更多技術(shù)的時(shí)候,有時(shí)使用慢點(diǎn)的"MOVDQA"會(huì)比替代它的"PUSHFD"快。所以你要對(duì)你的代碼精打細(xì)算。
pushfd xmm0,[edi],0E4h ;拷貝EDI指向位置的16字節(jié)到XMM0。0E4h會(huì)直接拷貝。
pushfw mm0,[edi],0E4h ;拷貝EDI指向位置的8字節(jié)到MM0。0E4h會(huì)直接拷貝。
<56>直接寫內(nèi)存---繞開(kāi)緩存(cache)
這是另一個(gè)優(yōu)化內(nèi)存處理的策略。如果你必須向許多內(nèi)存空間(256KB及以上)進(jìn)行寫操作,繞開(kāi)緩存直接向內(nèi)存寫更快!如果你的CPU是P3,你可以使用"movntq"或"movntps"指令。前者執(zhí)行8字節(jié)的寫操作,而后者是16字節(jié)。16-byte寫需要16字節(jié)對(duì)齊。在P4上,你還可以使用"movntdq",它也可以用于16字節(jié),但必須16字節(jié)對(duì)齊。這個(gè)方法在內(nèi)存填充和內(nèi)存拷貝中均適用,二者都做寫操作。這里有一些樣本代碼。我必須自己動(dòng)手并行使用8個(gè)XMM寄存器來(lái)幫助消除P4機(jī)MOVDQA指令的一些延遲。然而,為了幫助理解,我沒(méi)那么做。
mov ecx,16384 ;寫16384個(gè)16-bit值,16384*16 = 256KB
;所以我們正在拷貝一個(gè)256KB的數(shù)組
mov esi,offset src_arr ;指向必須以16-bit對(duì)齊的源數(shù)組的指針,否則會(huì)產(chǎn)生異常
mov edi,offset dst_arr ;指向必須以16-bit對(duì)齊的目的數(shù)組的指針,否則會(huì)產(chǎn)生異常
looper:
movdqa xmm0,[esi] ;工作在P3及以上
movntps [edi],xmm0 ;工作在P3及以上
add esi,16
add edi,16
dec ecx
jnz looper
<57>使用MMX/SSE/SSE2時(shí)每個(gè)循環(huán)處理2個(gè)事件
在P4上,MMS/SSE/SSE2指令的延遲那么長(zhǎng)以至于我總是每個(gè)循環(huán)處理2個(gè)事件或者提前讀取一個(gè)循環(huán)。如果你有足夠的寄存器,可以多于2個(gè)事件。所有的各種各樣的MOVE(包括MOVD)指令在P4上的速度都慢。所以2個(gè)32-bit的數(shù)字?jǐn)?shù)組相加運(yùn)算在P4上比P3上還慢。一個(gè)快點(diǎn)兒的方法可能就是每個(gè)循環(huán)(這個(gè)循環(huán)在FRED標(biāo)號(hào)之前預(yù)讀循環(huán)初始值MM0和MM1)處理兩個(gè)事件。你必須做的只是在數(shù)組元素個(gè)數(shù)為奇時(shí)進(jìn)行特殊的處理;在最后檢查一下,如果為奇數(shù),加一個(gè)額外的dword。這兒有個(gè)并沒(méi)有提前讀取值的代碼段。我想,把它改為提前讀取值是很容易的,所以我沒(méi)有兩個(gè)都貼出。下面的代碼可以:在P4機(jī)上避免ADC這個(gè)速度慢的指令來(lái)把兩個(gè)數(shù)組相加。
pxor mm7,mm7 ; the previous loops carry stays in here
fred:
movd mm0,[esi] ; esi points to src1
movd mm1,[edi] ; edi points to src2, also to be used as the destination
paddq mm0,mm1 ; add both values together
paddq mm0,mm7 ; add in remainder from last add
movd [edi],mm0 ; save value to memory
movq mm7,mm0
psrlq mm7,32 ; shift the carry over to bit 0
add esi,8
add edi,8
sub ecx,1
jnz fred
movd [edi],mm7 ; save carry
<58>預(yù)讀MMX或XMM寄存器來(lái)規(guī)避長(zhǎng)時(shí)間的延遲
在需要之前預(yù)讀一個(gè)SSE2寄存器會(huì)提升速度。這是因?yàn)镸OVDQA指令在P4上花費(fèi)6 cycles。這確實(shí)慢。鑒于它有如此長(zhǎng)之延遲,我想在確定不會(huì)產(chǎn)生阻礙的地方提前讀取它。這里有一個(gè)例子。
movdqa xmm1,[edi+16] ;在我們需要之前讀取入XMM1,P4上花費(fèi)6 cycles,不包括從緩存取的時(shí)間。
por xmm5,xmm0 ;做OR運(yùn)算,XMM0已預(yù)讀。P4上花費(fèi)2 cycles。
pand xmm6,xmm0 ;做AND運(yùn)算,XMM0已預(yù)讀。P4上花費(fèi)2 cycles。
movdqa xmm2,[edi+32] ;在我們需要之前預(yù)讀入XMM2,P4上花費(fèi)6 cycles,不包括從緩存取的時(shí)間。
por xmm5,xmm1 ;做OR運(yùn)算,XMM1已預(yù)讀。P4上花費(fèi)2 cycles。
pand xmm6,xmm1 ;做AND運(yùn)算,XMM1已預(yù)讀。P4上花費(fèi)2 cycles。
<59>在一個(gè)或多個(gè)寄存器中累加一個(gè)結(jié)果來(lái)避免執(zhí)行慢的指令
在一個(gè)或多個(gè)寄存器中累加一個(gè)結(jié)果來(lái)避免執(zhí)行慢的指令。我用這個(gè)策略加速用SSE2寫的比較/讀循環(huán)。比較慢的指令是PMOVMSKB。所以,我累加結(jié)果在一個(gè)寄存器中而不是每次循環(huán)都執(zhí)行這個(gè)指令。對(duì)每個(gè)4KB的內(nèi)存讀操作,我會(huì)用PMOVMSKB,它會(huì)很大地提速。下面我們通過(guò)分析一個(gè)使用PREFETCH和TLB啟動(dòng)的例子來(lái)證明。下面的代碼有2個(gè)循環(huán)。內(nèi)層循環(huán)被展開(kāi)來(lái)處理128字節(jié)(P4機(jī)上PREFETCH指令的預(yù)取字節(jié)數(shù))。另一個(gè)循環(huán)被展開(kāi)為4KB。所以我可以使用TLB啟動(dòng)。如果你使用的系統(tǒng)沒(méi)有使用4KB頁(yè)大小,你不得不適當(dāng)?shù)匦薷哪愕拇a。在擁有最大6.4 GB/s內(nèi)存帶寬的戴爾服務(wù)器(Dell Server)系統(tǒng)上,我測(cè)試了這段代碼。我能夠以5.55 GB/s做讀和比較操作(在沒(méi)有Windows環(huán)境下。在Windows環(huán)境下會(huì)運(yùn)行地慢點(diǎn))。我遺漏標(biāo)號(hào)"compare_failed"的代碼有2個(gè)原因:1)剪切/粘貼的代碼已經(jīng)夠多了;2)它沒(méi)有論證任何我要展現(xiàn)的技術(shù)。"compare_failed"的代碼只是簡(jiǎn)單地(在PCMPEQD找到失敗地址所屬的最近的4KB內(nèi)存塊后)做一個(gè)REP SCASD來(lái)找到失敗的地址。這個(gè)例子有非常巨大的代碼量,所以我把它放在最后以免你讀它的時(shí)候睡著;)(譯者注:感覺(jué)下面的代碼注釋翻譯出來(lái)有點(diǎn)別扭,而且原文也不難理解。故略。)
read_compare_pattern_sse2 proc near
mov edi,[start_addr] ;Starting Address
mov ecx,[stop_addr] ;Last addr to NOT test.
mov ebx,0FFFFFFFFh ;AND mask
movd xmm6,ebx ;AND mask
pshufd xmm6,xmm6,00000000b ;AND mask
movdqa xmm0,[edi] ;Get first 16 bytes
mov eax,[pattern] ;EAX holds pattern
pxor xmm5,xmm5 ;OR mask
movd xmm7,eax ;Copy EAX to XMM7
pshufd xmm7,xmm7,00000000b ;Blast to all DWORDS
outer_loop:
mov ebx,32 ;128 32 byte blocks
mov esi,edi ;save start of block
if DO_TLB_PRIMING
mov eax,[edi+4096] ;TLB priming
endif ;if DO_TLB_PRIMING
fred_loop:
movdqa xmm1,[edi+16] ;read 16 bytes
por xmm5,xmm0 ;OR into mask
pand xmm6,xmm0 ;AND into mask
movdqa xmm2,[edi+32] ;read 16 bytes
por xmm5,xmm1 ;OR into mask
pand xmm6,xmm1 ;AND into mask
movdqa xmm3,[edi+48] ;read 16 bytes
por xmm5,xmm2 ;OR into mask
pand xmm6,xmm2 ;AND into mask
movdqa xmm0,[edi+64] ;read 16 bytes
por xmm5,xmm3 ;OR into mask
pand xmm6,xmm3 ;AND into mask
movdqa xmm1,[edi+80] ;read 16 bytes
por xmm5,xmm0 ;OR into mask
pand xmm6,xmm0 ;AND into mask
movdqa xmm2,[edi+96] ;read 16 bytes
por xmm5,xmm1 ;OR into mask
pand xmm6,xmm1 ;AND into mask
movdqa xmm3,[edi+112] ;read 16 bytes
por xmm5,xmm2 ;OR into mask
pand xmm6,xmm2 ;AND into mask
por xmm5,xmm3 ;OR into mask
prefetchnta [edi+928] ;Prefetch 928 ahead
pand xmm6,xmm3 ;AND into mask
add edi,128 ;Go next 128byteblock
cmp edi,ecx ;At end?
jae do_compare ;No, jump
movdqa xmm0,[edi] ;read 16 bytes
sub ebx,1 ;Incr for inner loop
jnz fred_loop
do_compare:
pcmpeqd xmm5,xmm7 ;Equal?
pmovmskb eax,xmm5 ;Grab high bits in EAX
cmp eax,0FFFFh ;all set?
jne compare_failed ;No, exit failure
mov edx,0FFFFFFFFh ;AND mask
pxor xmm5,xmm5
pcmpeqd xmm6,xmm7 ;Equal?
pmovmskb eax,xmm6 ;Grab high bits in EAX
cmp eax,0FFFFh ;All Set?
jne compare_failed ;No, exit failure
movd xmm6,edx ;AND mask
pshufd xmm6,xmm6,00000000b ;AND mask
cmp edi,ecx ;We at end of range
jb outer_loop ;No, loop back up
jmp compare_passed ;Done!!! Success!!!
<60>在循環(huán)內(nèi)預(yù)取距離和位置
你會(huì)注意到,在上面的例子中,我之前預(yù)取了928字節(jié)而不是128字節(jié)(128是P4機(jī)上的預(yù)取字節(jié)數(shù))。為什么?Intel建議在循環(huán)開(kāi)始前預(yù)取128字節(jié)(2 cache lines)。但兩種不同取法(在循環(huán)開(kāi)始處或提前預(yù)取128字節(jié))都會(huì)出錯(cuò)。我既沒(méi)有在循環(huán)開(kāi)始時(shí)預(yù)取也沒(méi)之前預(yù)取128字節(jié)。為什么?當(dāng)我研究這段代碼時(shí),我發(fā)現(xiàn)把PREFETCH指令放到循環(huán)周圍并且改變它預(yù)取的偏移量可以使它運(yùn)行得更快。所以反常得是,我寫代碼來(lái)嘗試所有的循環(huán)內(nèi)預(yù)取指令的位置和開(kāi)始預(yù)取的偏移量的組合情況。這段代碼寫成一個(gè)匯編文件,而且把周圍的PREFETCH指令移到循環(huán)內(nèi),同時(shí)修改開(kāi)始預(yù)取的偏移量。然后一個(gè)bat文件編譯這個(gè)修改的代碼并且運(yùn)行一個(gè)基準(zhǔn)點(diǎn)(benchmark)。我運(yùn)行了這個(gè)基準(zhǔn)點(diǎn)幾個(gè)小時(shí)來(lái)嘗試不同的組合情況(我在預(yù)取距離為32時(shí)開(kāi)始,逐步增加距離直到距離達(dá)到1024)。在此系統(tǒng)上,我寫的基于928字節(jié)而不是128字節(jié)的代碼執(zhí)行地更快。并且,幾乎在循環(huán)結(jié)束處預(yù)取是最快的(在do_compare標(biāo)號(hào)之前,PREFETCHNTA指令大約8條line)。
THE END.
txt打包下載 huibianyouhuats_jb51
相關(guān)文章
YOLO v4常見(jiàn)的非線性激活函數(shù)詳解
這篇文章主要介紹了YOLO v4常見(jiàn)的非線性激活函數(shù),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-05-05淺談測(cè)試驅(qū)動(dòng)開(kāi)發(fā)TDD之爭(zhēng)
在軟件行業(yè)中,神仙打架的名場(chǎng)面,那就不得不提的是2014年的那場(chǎng)——測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD)之爭(zhēng)。2021-05-05IntelliJ IDEA插件EasyCode安裝方法與使用技巧
這篇文章主要介紹了IntelliJ IDEA插件EasyCode安裝方法與使用技巧,需要的朋友可以參考下2020-03-03一個(gè)能生成精美代碼圖片的工具Carbon!讓看代碼成為一種享受
當(dāng)我們想展示自己寫的代碼給別人看的時(shí)候,希望讓代碼保持原有的格式,并且要美觀一點(diǎn),有一個(gè)強(qiáng)大的在線工具可以幫助我們實(shí)現(xiàn),而且支持多種主題,他就是Carbon,需要的朋友可以參考下2021-05-05PyCharm2022激活碼破解補(bǔ)丁一鍵安裝免費(fèi)分享(2022年持續(xù)更新)
PyCharm2022最新激活碼分享(持續(xù)更新),PyCharm激活補(bǔ)丁一鍵安裝簡(jiǎn)單方便,無(wú)需手動(dòng)修改文件,MAC,linux,Windows系統(tǒng)都可使用2022-07-07一文讀懂modbus slave和modbus poll使用說(shuō)明
modbus poll和modbus slave是一款實(shí)用的modbus開(kāi)發(fā)和調(diào)試工具,可以非常方便的進(jìn)行modbus調(diào)試,是非常有用的Modbus主機(jī)/從機(jī)模擬程序,這篇文章給大家介紹modbus slave和modbus poll使用說(shuō)明,感興趣的朋友一起看看吧2021-04-04vscode安裝git及項(xiàng)目開(kāi)發(fā)過(guò)程
這篇文章主要介紹了vscode安裝git及項(xiàng)目開(kāi)發(fā)過(guò)程,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03