志位轉(zhuǎn)整數(shù)與無(wú)分支編程)
寫代碼這么多年我見過太多人在 x86 匯編里栽在同一個(gè)地方明明CMP指令已經(jīng)把兩個(gè)數(shù)比完了標(biāo)志位也擺在那里結(jié)果想拿這個(gè)比較結(jié)果做邏輯運(yùn)算時(shí)卻只能笨拙地跳轉(zhuǎn)、分支、再跳回來(lái)。直到你把SETcc這一族條件置位指令弄明白才會(huì)意識(shí)到——原來(lái)標(biāo)志位可以直接變成 0 或 1 的整數(shù)根本不需要打斷控制流。這篇內(nèi)容適合正在讀匯編、寫編譯器后端、做逆向分析、或者只是好奇xor eax, eax; cmp ...; setl al為什么天天出現(xiàn)在反匯編窗口里的朋友。我會(huì)從 EFLAGS 標(biāo)志位的基本功講起把無(wú)符號(hào)和有符號(hào)比較的判定邏輯拆開再帶上幾個(gè)能直接抄的實(shí)戰(zhàn)代碼和經(jīng)典踩坑現(xiàn)場(chǎng)。1. 為什么需要 SETxx標(biāo)志位是“狀態(tài)”不是“數(shù)值”1.1 CMP/TEST 只改標(biāo)志不寫結(jié)果先看一個(gè)基礎(chǔ)問題CMP EAX, EBX執(zhí)行完以后EAX 和 EBX 的值變了嗎沒有。CMP本質(zhì)上就是做一次減法EAX - EBX但減法結(jié)果直接被丟棄唯一留下的痕跡就是 EFLAGS 寄存器里那幾位標(biāo)志位。TEST同理它做按位與也丟棄結(jié)果只更新標(biāo)志。這就是 x86 的一個(gè)設(shè)計(jì)特點(diǎn)比較操作把“結(jié)論”存在了狀態(tài)寄存器里而不是放在通用寄存器里。狀態(tài)標(biāo)志是隱式的、瞬時(shí)的你沒法直接把它賦值給某個(gè) C 語(yǔ)言變量。所以當(dāng)你寫下int r (a b);這行 C 代碼時(shí)編譯器必須想辦法把標(biāo)志位翻譯成一個(gè)真正的整數(shù) 0 或 1。最直接的做法是跳轉(zhuǎn):cmp edi, esi jge .Lfalse mov eax, 1 ret .Lfalse: xor eax, eax ret這當(dāng)然可行但它把線性代碼切成了兩個(gè)基本塊。如果這個(gè)比較結(jié)果要參與后續(xù)算術(shù)運(yùn)算而分支又很難預(yù)測(cè)流水線會(huì)被打亂。1.2 從 Jcc 到 SETcc控制流與數(shù)據(jù)流的轉(zhuǎn)換SETcc的作用是把標(biāo)志位的狀態(tài)“捕獲”成一個(gè)字節(jié)。它本身不做任何跳轉(zhuǎn)不改變執(zhí)行順序只是根據(jù)條件碼決定目標(biāo)字節(jié)是寫成 1 還是 0。這就把一個(gè)控制流問題轉(zhuǎn)變成了純粹的數(shù)據(jù)流問題布爾值終于可以像普通整數(shù)一樣參與加減、移位、掩碼運(yùn)算。x86 里的條件碼在跳轉(zhuǎn)指令和置位指令之間是高度統(tǒng)一的。JE/JZ和SETE/SETZ關(guān)心的都是 ZFJB/SETB關(guān)心的都是 CFJGE/SETGE關(guān)心的都是 SF 與 OF 的關(guān)系。區(qū)別只在于Jcc 改變 EIPSETcc 改變目標(biāo)字節(jié)。1.3 編譯器眼中的布爾值無(wú)處不在的 setcc 模式如果你用 objdump 看過 GCC 或 Clang 開啟優(yōu)化后的輸出一定經(jīng)常遇到下面這種三連xor eax, eax cmp edi, esi setl al三行指令完成了一整個(gè)“無(wú)分支比較返回布爾值”。第一行先把 EAX 清零保證 AL 之外的高位是干凈的第二行做比較設(shè)置標(biāo)志位第三行setl al根據(jù)“有符號(hào)小于”的條件把 AL 置成 1 或保持 0。這樣生成的代碼沒有跳轉(zhuǎn)、沒有分支預(yù)測(cè)失敗的風(fēng)險(xiǎn)所有 CPU 前端流水線都保持線性執(zhí)行在比較結(jié)果不可預(yù)測(cè)的場(chǎng)景下遠(yuǎn)比跳轉(zhuǎn)方案穩(wěn)定。理解 SETcc本質(zhì)上就是理解編譯器為什么這么喜歡它。2. EFLAGS 標(biāo)志位速查先讀懂 ZF、CF、SF、OF在討論SETcc之前必須把 EFLAGS 里那幾個(gè)關(guān)鍵位徹底搞清楚。很多人背指令表時(shí)只知道“SETL 是有符號(hào)小于”但遇到實(shí)際數(shù)據(jù)就懵了因?yàn)樗磺宄?SF 和 OF 為什么要一起看。2.1 狀態(tài)寄存器里的 8 個(gè)關(guān)鍵位x86 的狀態(tài)標(biāo)志位分布在 EFLAGS/RFLAGS 中真正影響 SETcc 條件判斷的主要有以下這些標(biāo)志位名稱含義典型觸發(fā)場(chǎng)景CF進(jìn)位標(biāo)志無(wú)符號(hào)運(yùn)算產(chǎn)生進(jìn)位或借位移位時(shí)移出的位ADD最高位進(jìn)位、CMP無(wú)符號(hào)借位PF奇偶標(biāo)志結(jié)果低 8 位中 1 的個(gè)數(shù)為偶數(shù)時(shí)為 1很少用主要用于串口/BCD校驗(yàn)ZF零標(biāo)志結(jié)果為零時(shí)為 1CMP兩數(shù)相等、TEST為零SF符號(hào)標(biāo)志結(jié)果的最高位為 1 時(shí)為 1有符號(hào)視角為負(fù)算術(shù)結(jié)果最高位為 1OF溢出標(biāo)志有符號(hào)運(yùn)算結(jié)果超出可表示范圍時(shí)為 1正加正得負(fù)、負(fù)減正得正等還有 AF 輔助進(jìn)位標(biāo)志主要給 BCD 指令用日常SETcc幾乎碰不到可以先略過。2.2 CF 與 OF無(wú)符號(hào)和有符號(hào)的“溢出”不是一回事這是最容易混淆的一對(duì)我見過很多人在這個(gè)地方翻車。CF 站在無(wú)符號(hào)視角。8 位無(wú)符號(hào)數(shù)的范圍是 0 到 255當(dāng)0xFF 0x01時(shí)結(jié)果變成 0x100超出了 8 位能容納的上限那個(gè)多出來(lái)的 1 就被放進(jìn) CF。所以ADD之后 CF1 代表無(wú)符號(hào)加法有進(jìn)位CMP之后 CF1 代表無(wú)符號(hào)減法的被減數(shù)小于減數(shù)發(fā)生了借位。OF 站在有符號(hào)視角。8 位有符號(hào)數(shù)的范圍是 -128 到 127當(dāng)0x7F 0x01時(shí)按數(shù)學(xué)期望是 128但 128 超出了有符號(hào)范圍結(jié)果被解釋成 -128符號(hào)完全反了這時(shí)候 OF1。反過來(lái)兩個(gè)負(fù)數(shù)相加得到正數(shù)也一樣會(huì)把 OF 置 1。舉個(gè)具體例子你就明白了。設(shè)AL 0x80BL 0x01執(zhí)行CMP AL, BL無(wú)符號(hào)視角0x80 是 128128 - 1 127沒有借位CF0。有符號(hào)視角0x80 是 -128(-128) - 1 -129超過了 8 位有符號(hào)范圍結(jié)果截?cái)嗪笞兂?127符號(hào)翻轉(zhuǎn)所以 OF1。同一個(gè)減法CF 和 OF 給出完全不同的結(jié)論這不是矛盾而是它們分別屬于兩套解釋體系。2.3 TEST 與 CMP 到底改了什么TEST執(zhí)行按位與所以它永遠(yuǎn)不產(chǎn)生進(jìn)位執(zhí)行后 CF 和 OF 一定被清 0SF 反映按位與的最高位結(jié)果ZF 反映按位與結(jié)果是否為零PF 反映結(jié)果的奇偶性。CMP執(zhí)行減法CF、OF、SF、ZF、PF 都會(huì)被合理更新。記住一個(gè)常識(shí)普通MOV指令不影響標(biāo)志位LEA也不影響但絕大多數(shù)算術(shù)邏輯指令A(yù)DD、SUB、AND、OR、XOR、INC、DEC、SHL等都會(huì)影響。這意味著你不能在CMP之后隨便插入一條算術(shù)指令否則之前的比較結(jié)果就作廢了。這一點(diǎn)在后面避坑部分還會(huì)展開。3. SETxx 完整指令家族條件碼分組與判定邏輯3.1 按標(biāo)志位直接取值Z/S/O/P 四組有些SETcc指令非常直觀它們只讀取某一個(gè)標(biāo)志位邏輯上就是“把這一個(gè)布爾值搬運(yùn)到字節(jié)里”。SETE / SETZZF1 時(shí)為 1。常用于判斷相等、判斷結(jié)果為零。SETNE / SETNZZF0 時(shí)為 1是SETE的反義。SETSSF1 時(shí)為 1即結(jié)果為負(fù)。SETNSSF0 時(shí)為 1。SETOOF1 時(shí)為 1即有符號(hào)溢出。SETNOOF0 時(shí)為 1。SETP / SETPEPF1 時(shí)為 1低 8 位 1 的個(gè)數(shù)為偶數(shù)。SETNP / SETPOPF0 時(shí)為 1。這四組沒什么需要推導(dǎo)的地方讀標(biāo)志位、寫目標(biāo)字節(jié)僅此而已。真正需要?jiǎng)幽X筋的是下面兩組比較類條件碼。3.2 無(wú)符號(hào)比較組CF 告訴你“誰(shuí)大誰(shuí)小”無(wú)符號(hào)數(shù)沒有符號(hào)位大小關(guān)系完全由借位情況和相等情況決定。CMP A, B執(zhí)行后A BA 減 B 會(huì)產(chǎn)生借位CF1。A B沒有借位CF0。A B差值為 0ZF1。A B要么相等要么產(chǎn)生了借位即 ZF1 或 CF1。A B既不相等也沒有借位即 ZF0 且 CF0。對(duì)應(yīng)到指令表就是下面這批指令判定條件含義SETB / SETC / SETNAECF1無(wú)符號(hào)低于 (Below)SETAE / SETNB / SETNCCF0無(wú)符號(hào)高于等于 (Above or Equal)SETBE / SETNACF1 或 ZF1無(wú)符號(hào)低于等于SETA / SETNBECF0 且 ZF0無(wú)符號(hào)高于記憶技巧B 是 BelowA 是 Above。Below 就是“在下面”對(duì)應(yīng)小于Above 就是“在上面”對(duì)應(yīng)大于。帶 E 的是加上 Equal等于。3.3 有符號(hào)比較組SF 和 OF 的組合判定有符號(hào)數(shù)的大小關(guān)系不能只看 CF因?yàn)?CF 只反映無(wú)符號(hào)的借位。比如0x80和0x01做減法CF0 表示無(wú)符號(hào)視角下 128 大于 1但站在有符號(hào)視角0x80是 -128它明明小于 1。所以有符號(hào)比較必須繞道 SF 和 OF。核心結(jié)論是CMP A, B之后若SF ! OF則 A 有符號(hào)小于 B。為什么當(dāng) OF0 時(shí)減法沒有發(fā)生有符號(hào)溢出結(jié)果符號(hào)就是真實(shí)符號(hào)。如果 SF1說(shuō)明差為負(fù)即 A B如果 SF0說(shuō)明差非負(fù)即 A B。當(dāng) OF1 時(shí)減法發(fā)生了溢出結(jié)果符號(hào)被“翻轉(zhuǎn)”了。也就是說(shuō)真實(shí)差值為負(fù)但結(jié)果看起來(lái)為正或者真實(shí)差值為正但結(jié)果看起來(lái)為負(fù)。此時(shí)判斷 A B 的條件反而變成 SF0。無(wú)論哪種情況A B都等價(jià)于SF ! OF。這是一條非常優(yōu)美的規(guī)律也解釋了為什么SETL的條件碼視圖是“SF 異或 OF”的風(fēng)格。完整的有符號(hào)比較組如下指令判定條件含義SETL / SETNGESF ! OF有符號(hào)小于 (Less)SETGE / SETNLSF OF有符號(hào)大于等于 (Greater or Equal)SETLE / SETNGZF1 或 SF ! OF有符號(hào)小于等于SETG / SETNLEZF0 且 SF OF有符號(hào)大于如果你把這兩張表對(duì)比看會(huì)發(fā)現(xiàn)無(wú)符號(hào)和有符號(hào)只是把 CF 換成了 SF/OF 的組合其他結(jié)構(gòu)完全對(duì)稱。3.4 與 Jcc 的對(duì)照表同一套條件碼兩種用途SETcc和Jcc共享同一套條件碼定義。我平時(shí)查指令時(shí)最常用的就是這張對(duì)照表SETccJcc條件含義SETE / SETZJE / JZZF1相等 / 結(jié)果為零SETNE / SETNZJNE / JNZZF0不相等SETBJB / JCCF1無(wú)符號(hào)小于SETAEJAE / JNCCF0無(wú)符號(hào)大于等于SETBEJBECF1 或 ZF1無(wú)符號(hào)小于等于SETAJACF0 且 ZF0無(wú)符號(hào)大于SETLJLSF ! OF有符號(hào)小于SETGEJGESF OF有符號(hào)大于等于SETLEJLEZF1 或 SF ! OF有符號(hào)小于等于SETGJGZF0 且 SF OF有符號(hào)大于SETSJSSF1結(jié)果為負(fù)SETOJOOF1有符號(hào)溢出SETPJPPF1低8位偶校驗(yàn)看完這張表你會(huì)發(fā)現(xiàn)只要你學(xué)過 JccSETcc 的語(yǔ)義就已經(jīng)會(huì)了一半剩下的就是習(xí)慣“把條件寫到字節(jié)里”這個(gè)動(dòng)作。補(bǔ)充一個(gè)編碼層面的細(xì)節(jié)機(jī)器碼層面SETcc的 Opcode 是0F 90到0F 9F條件后綴直接編碼在低位順序和條件碼編號(hào)完全一致。如果你在做反匯編器或者手工看二進(jìn)制發(fā)現(xiàn)0F 9D就是SETGE / SETNL0F 9C就是SETL / SETNGE不需要每次都翻手冊(cè)。4. 實(shí)戰(zhàn)從 C 語(yǔ)言條件到 SETcc 匯編的完整鏈路4.1 最簡(jiǎn)單的例子return a b假設(shè)你在 x86-64 Linux 環(huán)境下有這樣一個(gè)函數(shù)int is_less(int a, int b) { return a b; }GCC 開啟優(yōu)化后可能生成如下代碼System V 調(diào)用約定下a 在 edib 在 esi返回值放 eaxis_less: xor eax, eax ; 把 eax 清零順便保證高位干凈 cmp edi, esi ; 計(jì)算 edi - esi修改標(biāo)志位 setl al ; al (SF ! OF) ? 1 : 0 ret注意xor eax, eax必須放在cmp之前因?yàn)閤or本身會(huì)改變標(biāo)志位。如果把xor放在cmp之后等于把剛算好的 ZF、SF、OF 全部清掉了setl al會(huì)依賴錯(cuò)誤的標(biāo)志狀態(tài)。編譯器這樣編排不是隨意的。同樣如果改成return a b;末尾那條指令會(huì)變成setle al改成return a b;則會(huì)變成sete al。這套對(duì)應(yīng)關(guān)系練習(xí)幾遍就熟了。4.2 把標(biāo)志位“存起來(lái)”進(jìn)位捕獲與多精度算術(shù)SETcc最有價(jià)值的功能之一是把標(biāo)志位轉(zhuǎn)成整數(shù)保存下來(lái)供后續(xù)邏輯使用。典型場(chǎng)景是 32 位平臺(tái)上做 64 位加法。低 32 位相加后產(chǎn)生的進(jìn)位在 CF 里但你不能直接拿 CF 去參與 C 語(yǔ)言邏輯于是先把它抓出來(lái)mov eax, [a_low] add eax, [b_low] ; eax 低32位之和CF 是否產(chǎn)生進(jìn)位 mov ecx, 0 setc cl ; ecx 0 或 1保存進(jìn)位 mov edx, [a_high] adc edx, [b_high] ; edx 高32位之和 CF這里setc cl扮演了“快照”角色。雖然下面的adc也會(huì)讀 CF但如果你需要同時(shí)保留進(jìn)位給后續(xù)數(shù)組、或者要返回進(jìn)位標(biāo)記先把 CF 變成整數(shù)是必要的。相比之下LAHF雖然也能把標(biāo)志位送回 AH但位的位置很別扭不如setc直觀。4.3 branchless 編程用 setcc 構(gòu)造掩碼實(shí)現(xiàn)無(wú)分支最大值SETcc生成的是 0 或 1而 0 或 1 本身不是很好用的掩碼。這時(shí)候配合NEG把它變成全 0 或全 1就能派上大用場(chǎng)。看一個(gè)無(wú)分支求最大值的例子; int max(int a, int b)a 在 edib 在 esi xor eax, eax cmp edi, esi setg al ; al (edi esi) ? 1 : 0 neg eax ; eax 0 或 0xFFFFFFFF mov ecx, eax and ecx, edi ; 如果 edi esi保留 edi否則清零 not eax and eax, esi ; 如果 edi esi清零否則保留 esi or eax, ecx ; 合并得到 max ret整個(gè)流程沒有一條條件跳轉(zhuǎn)指令。setg把比較結(jié)果變成 1neg把它擴(kuò)展成全 1 掩碼隨后兩個(gè)and分別從兩個(gè)源里各取所需最后or拼接。這類代碼在哈希表探測(cè)、狀態(tài)機(jī)索引、飽和度運(yùn)算里非常常見。編譯器在遇到一些布爾表達(dá)式時(shí)也會(huì)自己生成類似的掩碼邏輯。如果你想要的是“選一個(gè)”而不是“算最大值”也可以換用CMOVcc但CMOVcc適合在兩個(gè)現(xiàn)成值之間選擇SETcc 掩碼的組合適合把比較結(jié)果本身當(dāng)作數(shù)值參與運(yùn)算兩者分工不同。5. 五個(gè)高頻踩坑現(xiàn)場(chǎng)SETcc 使用的邊界與陷阱5.1 坑一只寫低 8 位留下的高位殘留SETcc的目標(biāo)操作數(shù)只能是 8 位寄存器或字節(jié)內(nèi)存這是指令格式的硬性限制。很多人寫完setl al以為整個(gè) EAX 變成了 0 或 1其實(shí) AL 之外的位原封不動(dòng)??催@個(gè)錯(cuò)誤示范mov eax, 0x12345678 cmp edi, esi setl al ; 此時(shí) eax 0x123456??并不是 0x00000001修復(fù)方法有兩種。第一種是提前清零xor eax, eax cmp edi, esi setl al第二種是事后擴(kuò)展cmp edi, esi setl al movzx eax, al兩種都正確但第一種在部分舊微架構(gòu)上更友好因?yàn)樗鼜母瓷媳苊饬恕跋葘懖糠旨拇嫫?、再讀完整寄存器”的依賴問題。編譯器輸出里兩種風(fēng)格都存在遇到哪個(gè)都別意外。5.2 坑二CMP 和 SETcc 之間插入了“不干凈”的指令標(biāo)志位是瞬時(shí)的很多指令會(huì)無(wú)意中修改它。看這段代碼cmp edi, esi inc ecx ; 這里破壞了 ZF、SF、OF雖然它不改 CF setl al ; 此時(shí)比較結(jié)果已經(jīng)丟了就算用的是一條看起來(lái)和比較無(wú)關(guān)的指令只要它屬于算術(shù)邏輯類就可能覆蓋標(biāo)志位。有一批指令不影響標(biāo)志位比如MOV、LEA、PUSH、POP、JMP但ADD、SUB、INC、DEC、AND、OR、XOR都會(huì)改寫標(biāo)志。別以為INC和DEC不影響 CF 就安全它們對(duì) ZF、SF、OF 的影響足以毀掉一次SETL判斷。最穩(wěn)妥的習(xí)慣是讓CMP/TEST和SETcc緊挨著中間不插入任何可能影響標(biāo)志的指令。如果必須在兩者之間處理數(shù)據(jù)先把標(biāo)志位保存出去再用。5.3 坑三無(wú)符號(hào)和有符號(hào)條件混用同一個(gè)比較無(wú)符號(hào)和有符號(hào)可能得到完全相反的結(jié)論。設(shè)eax 0x80000000ebx 0x00000001執(zhí)行cmp eax, ebxsetb al無(wú)符號(hào)視角下2147483648 1沒有借位CF0所以 AL0。setl al有符號(hào)視角下-2147483648 1溢出使得 SF0、OF1SF ! OF所以 AL1。你寫代碼前必須明確數(shù)據(jù)語(yǔ)義這兩個(gè)數(shù)到底該按無(wú)符號(hào)解釋還是有符號(hào)解釋數(shù)組下標(biāo)、地址運(yùn)算、位掩碼通常按無(wú)符號(hào)業(yè)務(wù)邏輯里的整數(shù)比較通常按有符號(hào)?;煊脳l件碼是逆向上經(jīng)??吹降倪壿嫹囱菰颉?.4 坑四操作數(shù)順序?qū)Ρ容^結(jié)果的影響CMP A, B計(jì)算的是 A 減 B它表達(dá)的含義是“A 與 B 比較”。如果你要判斷if (x y)應(yīng)該寫cmp x, y; setl al表示“x 小于 y 為真”。如果寫反了變成cmp y, x; setl al得到的其實(shí)是y x也就是x y。這種錯(cuò)誤在簡(jiǎn)單場(chǎng)景下通常肉眼能看出來(lái)但在復(fù)雜表達(dá)式拆分后很容易被忽略。一個(gè)通用口訣CMP 第一操作數(shù), 第二操作數(shù)條件判斷的語(yǔ)義就是“第一操作數(shù) 條件 第二操作數(shù)”。cmp x, y; jg等價(jià)于if (x y)和SETcc完全一致。5.5 坑五舊微架構(gòu)上的部分寄存器停頓從 Pentium 時(shí)代到較早的 Core 架構(gòu)x86 CPU 在“寫 8 位寄存器 AL然后讀完整寄存器 EAX”時(shí)可能會(huì)產(chǎn)生額外停頓因?yàn)樗鼰o(wú)法高效跟蹤 AL 與 EAX 高位的合并關(guān)系。這類問題被稱為部分寄存器停頓partial register stall?,F(xiàn)代 CPU 大多通過寄存器重命名緩解了這個(gè)問題但在兼容性和低功耗微架構(gòu)上它仍然可能咬你一口。我寫高性能代碼的習(xí)慣仍然是要么xor eax, eax提前清要么movzx eax, al顯式擴(kuò)展。這樣寫不僅安全而且語(yǔ)義一目了然。6. 性能與替代方案用 SETcc 寫出更快的分支消除代碼6.1 SETcc 在現(xiàn)代 CPU 上的指令成本在較新的主流微架構(gòu)上絕大多數(shù)SETcc指令都被解碼為單個(gè) μop延遲大約 1 到 2 個(gè)周期吞吐非??捎^。加上前面的CMP和后面的MOVZX整個(gè)“比較并把結(jié)果轉(zhuǎn)成整數(shù)”的序列大概只有三到四條指令流水線完全線性沒有任何分支預(yù)測(cè)器參與。對(duì)比傳統(tǒng)分支版本一條Jcc如果預(yù)測(cè)正確開銷很小但如果預(yù)測(cè)失敗代價(jià)往往是 15 到 20 個(gè)周期以上的流水線清空。所以當(dāng)比較結(jié)果接近五五開、無(wú)法預(yù)測(cè)時(shí)SETcc的無(wú)分支方案幾乎總是更優(yōu)。這也是編譯器傾向于把它用在“返回布爾值”函數(shù)里的原因。6.2 寄存器清零的兩種常見姿勢(shì)先 XOR 還是后 MOVZX前面提到過把 AL 擴(kuò)展成完整 EAX 有兩種常見寫法; 姿勢(shì) A先清后置 xor eax, eax cmp edi, esi setl al ; 姿勢(shì) B后擴(kuò)展 cmp edi, esi setl al movzx eax, al兩者的指令數(shù)差不多。姿勢(shì) A 在寄存器重命名資源上更省因?yàn)?CPU 從一開始就認(rèn)為 EAX 是一個(gè)完整定義姿勢(shì) B 的結(jié)構(gòu)更清晰適合在已經(jīng)有現(xiàn)成寄存器值時(shí)使用。我建議跟隨編譯器慣例簡(jiǎn)單的布爾函數(shù)優(yōu)先用姿勢(shì) A需要保留舊值時(shí)用姿勢(shì) B。不要在cmp之后插xor那會(huì)破壞標(biāo)志位。6.3 從 SETcc 到 SBB 技巧另一種把標(biāo)志位傳播為整數(shù)的方法如果你看過老手寫的極致短代碼可能撞見過利用SBB傳播 CF 的方法。比如把無(wú)符號(hào)比較a b轉(zhuǎn)成 0/1xor eax, eax cmp edi, esi sbb eax, eax ; CF1 時(shí) eax -1CF0 時(shí) eax 0 inc eax ; CF1 時(shí) eax 0CF0 時(shí) eax 1原理是SBB計(jì)算dst dst - src - CF。當(dāng)src是同一個(gè)寄存器時(shí)結(jié)果就是-CFCF1 得到 0xFFFFFFFFCF0 得到 0。這個(gè)技巧可以生成 32 位掩碼避免setcc movzx的兩步操作。但我不建議在日常代碼里刻意賣弄它?,F(xiàn)代編譯器基本不會(huì)用這種寫法因?yàn)閟etae已經(jīng)足夠快而且語(yǔ)義更直白。了解它的意義在于讓你理解“CF 可以被算術(shù)指令消費(fèi)”這件事這對(duì)調(diào)試和分析手寫匯編很有幫助。6.4 什么情況下不該用 SETcc分支預(yù)測(cè)仍有價(jià)值分支消除聽起來(lái)很美好但它不是銀彈。如果分支具有極強(qiáng)的偏向性比如循環(huán)里 99% 的情況都不滿足條件現(xiàn)代分支預(yù)測(cè)器的正確率能輕松超過 99%此時(shí)一個(gè)預(yù)測(cè)正確的條件跳轉(zhuǎn)幾乎不花時(shí)間。強(qiáng)行改成SETcc反而多算了多條算術(shù)指令白白增加指令數(shù)。我在優(yōu)化代碼時(shí)會(huì)先問自己這個(gè)條件的分布是怎樣的如果是隨機(jī)數(shù)據(jù)、哈希值、外部輸入SETcc無(wú)分支是穩(wěn)妥選擇如果是邊界檢查、特殊錯(cuò)誤分支、循環(huán)結(jié)束判斷保留分支通常更高效。不要拿著setcc到處替換所有分支性能決策要靠數(shù)據(jù)說(shuō)話不是靠信仰。有經(jīng)驗(yàn)的工程師拿到一段陌生匯編時(shí)往往先掃一眼有沒有setcc出現(xiàn)——它意味著這段代碼在把比較結(jié)果當(dāng)作數(shù)值使用。反過來(lái)如果你是自己寫匯編掌握好SETcc的語(yǔ)義和邊界你的代碼風(fēng)格會(huì)干凈一個(gè)檔次也更容易被編譯器優(yōu)化到同一水平。我個(gè)人的習(xí)慣是凡是需要布爾結(jié)果的地方先寫無(wú)分支的setcc版本再用實(shí)際數(shù)據(jù)跑一輪 profile多數(shù)時(shí)候它能贏偶爾輸給預(yù)測(cè)器但至少你手里多了一把可用的刀。