小小的 likely 背后却大有玄机!
作者 | 張彥飛allen
來源 | 開發(fā)內(nèi)功修煉
今天我給大家分享一個內(nèi)核中常用的提升性能的小技巧。理解了它對你一定大有好處。
在內(nèi)核中很多地方都充斥著 likely、unlikely 這一對兒函數(shù)的使用。隨便揪兩處,比如在 TCP 連接建立的過程中的這兩個函數(shù)。
//file:?net/ipv4/tcp_ipv4.c int?tcp_v4_conn_request(struct?sock?*sk,?struct?sk_buff?*skb) {if?(likely(!do_fastopen))?... } //file:?net/ipv4/tcp_input.c int?tcp_rcv_established(struct?sock?*sk,?...) {if?(unlikely(sk->sk_rx_dst?==?NULL))...... }在剛開始看源碼的時候,我也沒弄懂這一對兒函數(shù)的玄機。后來才搞懂它原來對性能提升是有幫助的。今天我就來和大家聊聊 likely、unlikely 是如何幫助性能提升的。
1. likely 和 unlikely
咱們先來挖挖這對兒函數(shù)的底層實現(xiàn)。
//file:?include/linux/compiler.h #define?likely(x)???__builtin_expect(!!(x),1) #define?unlikely(x)?__builtin_expect(!!(x),0)可以看到其實它們就是對 __builtin_expect 的一個封裝而已。__builtin_expect 這個指令是 gcc 引入的。該函數(shù)作用是允許程序員將最有可能執(zhí)行的分支告訴編譯器,來輔助系統(tǒng)進行分支預(yù)測。
(參見 https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)
它的用法為:__builtin_expect(EXP, N)。意思是:EXP == N的概率很大。那么上面 likely 和 unlikely 這兩句的具體含義就是:
__builtin_expect(!!(x),1) x 為真的可能性更大
__builtin_expect(!!(x),0) x 為假的可能性更大
當(dāng)正確地使用了__builtin_expect后,編譯器在編譯過程中會根據(jù)程序員的指令,將可能性更大的代碼緊跟著前面的代碼,從而減少指令跳轉(zhuǎn)帶來的性能上的下降。
2. 實際動手驗證
那我們實際動手寫兩個例子,來窺探一下編譯器是如何進行優(yōu)化的。為此我寫了一段簡單的測試代碼,如下:
#include?<stdio.h> #define?likely(x)?__builtin_expect(!!(x),?1) #define?unlikely(x)??__builtin_expect(!!(x),?0)int?main(int?argc,?char?*argv[]) {int?n;n?=?atoi(argv[1]);if?(likely(n?==?10)){n?=?n?+?2;}?else?{n?=?n?-?2;}printf("%d\n",?n);return?0; }這段代碼非常簡單,對用戶的輸入做一個 if else 判斷。在 if 中使用了 likely,也就是假設(shè)這個條件為真的概率更大。那么我們來看看它編譯后的匯編碼來看看。
圖中上面紅框內(nèi)是對 if 的匯編結(jié)果,可見它使用的是 jne 指令。它的作用是看它的上一句比較結(jié)果,如果不相等則跳轉(zhuǎn)到 4004bc 處,相等的話則繼續(xù)執(zhí)行后面的指令。
在 jne 指令后面緊挨著的是 n = n + 2 對應(yīng)的匯編代碼, 也就是說它把 n = n + 2 這段代碼邏輯放在了緊挨著自己身邊。而把 n = n - 2 的執(zhí)行邏輯放在了離當(dāng)前指令較遠的 4004bc 處。
我們再把 likey 換成 unlikey 看一下,發(fā)現(xiàn)結(jié)果是正好相反,這次把 n = n - 2 的執(zhí)行邏輯放在前面了,如圖。
注意,編譯時需要加 -O2 選項,使用 objdump -S 來查看匯編指令。為了方便大家使用,我把它寫到 makefile 里了,和測試代碼一起放到了咱們的 Github 上了。
大家想動手的可以玩玩,地址:
https://github.com/yanfeizhang/coder-kung-fu/tree/main/tests/cpu/test01
3. 性能提升原理
那這么做為什么就能夠提升程序運行性能呢?原因有兩個。
首先第一個原因就是 CPU 高速緩存。現(xiàn)代的 CPU 一般都有三級的緩存,用來解決內(nèi)存訪問過慢的問題。訪問數(shù)據(jù)時優(yōu)先從 L1 讀取,讀取不到再讀 L2、還讀不到就讀 L3,最后用內(nèi)存兜底。
這里要注意的是,每一次緩存數(shù)據(jù)的單位都是以一個 CacheLine 64 字節(jié)為單位進行存儲的。假如說要查詢的數(shù)據(jù)在 L1 中不存在,那么 CPU 的做法是一次性從 L2 中把要訪問的數(shù)據(jù)及其后面的 64 個字節(jié)全部緩存進來。
假如下一次再執(zhí)行的時候要訪問的指令在上一次已經(jīng)在 L1 中存在了,那么就直接訪問 L1,就不必再從 L2 來讀取了。回到上面的例子,假如說我們執(zhí)行完了 cmp 對比指令以后,判斷確實是要進加法分支,那緊接著就會執(zhí)行 jne 后面的 mov xor 等指令大概率就可以從 L1 中取到了。
假如說 cmp 對比執(zhí)行后,發(fā)現(xiàn)是要跳到 4004bc 處的指令執(zhí)行。那大概率該位置處的指令還沒有被加載到緩存中(實踐中一個分支下可能會包含很多代碼,而不是像我這個例子中簡單的兩三行),就避免不了從 L2 L3 甚至是速度更慢的 L3 去讀取指令。
除了高速緩存這個原因以外,還有一個更底層的原理。那就是 CPU 的流水線技術(shù)。CPU 在執(zhí)行程序指令的時候,并不是先執(zhí)行一個,執(zhí)行完了再運行下一個這樣的。而是把每個指令都分成了多個階段,并讓不同指令的各步操作重疊,從而實現(xiàn)幾條指令并行處理。
還拿上面編譯出來的匯編碼來舉例,程序中 cmp、jne、mov 幾個指令是挨著的,那么 CPU 在執(zhí)行的時候?qū)嶋H是大概這樣的。
當(dāng) jne 指令正在執(zhí)行的時候,后面的兩個 mov 指令都已經(jīng)分別進入到譯碼和取址階段了都。假如說分支預(yù)測失敗,那么這工作就白干了。
4. 小結(jié)
總結(jié)一下,今天分享的?likely 和 unlikely 其實是屬于是輔助 CPU 分支預(yù)測的性能優(yōu)化方法。這就是 likely 和 unlikely 背后的這點小秘密。它是為了讓 CPU 的高速緩存命中率更高,同時也為了讓 CPU 的流水線更好地工作。
Linux 作為一個基礎(chǔ)程序,在性能上真的是考慮到了極致。內(nèi)核的作者們內(nèi)功都是非常的深厚,都深諳計算機的底層工作原理。為了極致的性能追求精心打磨每一個細節(jié),非常值得我們學(xué)習(xí)和借鑒。
不過這里要說的一點是,likely 和 unlikely 的概率判斷務(wù)必準(zhǔn)確。如果寫反了,反而會起到反作用。
往期推薦
如果讓你來設(shè)計網(wǎng)絡(luò)
寫時復(fù)制就這么幾行代碼,還是不會?
JavaScript 數(shù)組你都掰扯不明白,還敢說精通 JavaScript ?
明明還有大量內(nèi)存,為啥報錯“無法分配內(nèi)存”?
點分享
點收藏
點點贊
點在看
總結(jié)
以上是生活随笔為你收集整理的小小的 likely 背后却大有玄机!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 设计方案,拿来吧你!
- 下一篇: 数据结构是如何装入 CPU 寄存器的?