NOP指令會打斷CPU流水線嗎?

NOP指令是一個空指令,可用於指令對齊,防止一條指令未操作完,下一個指令就開始操作相同的寄存器。這樣是不是為打亂CPU流水線,從而影響CPU指令處理效率?


很有趣的問題, 題干說對了, 可是結論不對.

實際上NOP指令的目的, 並非是為了打斷流水線, 也不會打斷它, 單獨出現的NOP會影響CPU的性能, 但不大. 實際應用中的NOP都是大量群居出現的, 這樣的代碼使CPU執行無用操作, 的確降低了效率, 但是這並非大量使用NOP的目的. 為了說清楚這個問題, 要從軟體和硬體分別解釋.

軟體上來說, NOP有什麼用?

參看Stack Exchange上:Purpose of NOP instruction and align statement in x86 assembly

實際的NOP, 主要在編寫Shellcode時用到. Shellcode是一種惡意操作, 目的是使程序出現Buffer overflow或者Uncontrolled format string這樣的Bug. 其背後的原因是, 有些高級語言並沒有針對程序溢出設計保護機制, 這樣的漏洞使得黑客可以通過注入大量的無效操作使得目標程序溢出到其他程序的代碼段, 從而取得程序的控制許可權. 一部分Unix操作系統甚至可以利用這樣的bug讓惡意代碼取得系統許可權. 事實上, 有些硬體設備的破解就是通過這樣的手段達到的, 比如psp 3000.

除此以外呢? 實際的彙編代碼中, NOP操作還多作為一種輔助機制. 比如:

  1. 假設程序已經設計好了一個相對轉移, 轉移對象是100 bytes forward. 半年後, 程序員決定在99 bytes forward的地方刪減1行代碼, 但是這樣做的話, 相對轉移的對象地址不就變化了么? 為了避免這種問題, 只要不刪減代碼, 而是把想刪減的代碼改寫成NOP就可以了.
  2. Nop sled. 這是一種代碼滑行, 假設一個程序想要跳轉, 但是只知道目標地址是250 bytes forward左右, 並不確定. 這個時候一個好辦法就是跳轉到200 bytes forward, 另外將目標代碼段之前的100 bytes的代碼全部都填滿NOP指令. 這樣無論跳轉到哪一條NOP指令, 程序都會順勢滑行到目標代碼段了
  3. 有的時候, 代碼中出現了條件轉移, 會先判斷一個標誌位, 根據結果再判斷是否轉移. 現在假設某一個程序員想測試這段彙編代碼, 去掉這個"判斷"操作, 那麼只要把那條判斷指令改寫成NOP就可以了. (這也是破解正版軟體的常見招數)

可以看到, NOP的兩種應用場景: 製造錯誤/輔助編程, 都不是為了使CPU產生stall, 它的作用是"佔位"! 佔了一個地址, 但是什麼都不做.

硬體上看NOP的原理:

參考Intel的IA-32軟體開發者手冊: (https://www.cs.uaf.edu/2011/fall/cs301/intel_x64_megadoc_2011.pdf)

Nop有1-9Byte9種寫法, 以方便對齊指令:

1-byte: XCHG EAX, EAX

2-byte: 66 NOP

..

9-byte: NOP WORD PTR [EAX + EAX*1 + 0] (32-bit displacement)

因為CPU需要指令取址-解碼這樣的過程, 對齊過的指令, 解碼速度是更快的. 有些CPU對跨邊界的指令需要額外的周期來做解碼工作的. 這種情況下使用Nop對齊邊界, 可以提升特定型號CPU的解碼效率.

功能上, NOP指令只讓EIP+1, 即Program Counter +1. 不影響任何標誌位, 有一個異常, #UD.

設計上, NOP作為一條有效的指令, 是需要完成整個CPU流水線過程的! 它需要Decode, 需要Dispatch, 需要Execution, 需要Retire. 執行它的單元一般是ALU, 但是它不改變任何ARF的值. 所以Nop的出現會影響CPU效率.

但PC+1這樣的事並不值得讓CPU為它花費一個周期, 這個過程有很大的優化空間.這裡舉兩個CPU優化Nop操作的栗子:

栗子一, 如果程序這樣寫:

ADD EAX, EBX; #pc+1
NOP; #pc+1

那就這樣執行:

ADD EAX, EBX; #pc+2

可以看到這裡NOP不會影響執行效率, 這樣的優化其實涉及融合的概念, 實現起來很容易, 但融合對指令順序是有要求的.

栗子二, 如果程序這樣寫:

NOP; #pc+1
NOP; #pc+1
NOP; #pc+1
NOP; #pc+1

那就這樣執行:

NOP; #pc+4

仍然是融合, 雖然這裡大段的NOP雖然能被壓縮在一起, 提升了執行效率, 但是NOP還是佔用了大段CPU解碼帶寬, 這種情況下, CPU執行部分可能產生飢餓.

總之, 硬體上看NOP的作用, 主要是對齊指令, NOP的出現肯定會降低CPU的性能, 但是CPU仍然有優化NOP的餘地.

這裡附帶解釋一個誤區:

以 @水無痕提到的例子來看, 這是一個RAW:

LW $1, 4($2);
NOP;
SUB $4, $1, $5;

因為第三條要等第一條, 所以中間產生了stall. 但是, 這裡是否插入NOP根本不重要.現代CPU早就在結構上做好了各種優化, 能應對各種RAW, WAW, WAR等hazards. RAW產生的stall在CPU內部就會消化掉, 這是Out-Of-Order流水線的強大之處. 並不需要人為的加入NOP去打斷CPU. 上面的代碼和下面的代碼對CPU來說執行時間是一樣的:

LW $1, 4($2);
SUB $4, $1, $5;

類似, 題主提到的, 不同指令使用同一寄存器的問題, 是不需要人為增加NOP解決的.

總結:

使用Nop的目的主要是佔位和對齊. 佔位是為了方便軟體編寫, 對齊是為了提升解碼效率(部分CPU). 但是NOP本身也需要被執行, 因此會降低CPU的有效執行效率. 但是現代CPU已經有良好的亂序處理能力, 並不需要人為使用Nop來打斷流水線.


nop好像有些地方就直接mov eax,eax了吧。


推薦閱讀:

為什麼感覺好多計算機大神都很喜歡日漫甚至有好多女裝大佬?
CPU的瞬時功耗是由什麼決定的?
學校開設關於編程的課程明顯不足,我需要補充哪些課程或者自學哪些姿勢技能來提高自己的編程水平?
哪些學習數據結構與演算法的書籍值得推薦?

TAG:中央處理器CPU | 計算機 | 計算機科學 | 精簡指令集RISC |