你确定不反编译 likely 看看?
大家好,我是寫代碼的籃球球癡,下面推薦飛哥的一篇文章,覺得對大家很有幫助,希望大家先看看再扔到收藏夾吃灰,轉發不轉發看大爺們心情咯!
今天我給大家分享一個內核中常用的提升性能的小技巧。理解了它對你一定大有好處。
在內核中很多地方都充斥著 likely、unlikely 這一對兒函數的使用。隨便揪兩處,比如在 TCP 連接建立的過程中的這兩個函數。
//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))...... }在剛開始看源碼的時候,我也沒弄懂這一對兒函數的玄機。后來才搞懂它原來對性能提升是有幫助的。今天我就來和大家聊聊 likely、unlikely 是如何幫助性能提升的。
1. likely 和 unlikely
咱們先來挖挖這對兒函數的底層實現。
//file:?include/linux/compiler.h #define?likely(x)???__builtin_expect(!!(x),1) #define?unlikely(x)?__builtin_expect(!!(x),0)可以看到其實它們就是對 __builtin_expect 的一個封裝而已。__builtin_expect 這個指令是 gcc 引入的。該函數作用是允許程序員將最有可能執行的分支告訴編譯器,來輔助系統進行分支預測。(參見 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 為假的可能性更大
當正確地使用了__builtin_expect后,編譯器在編譯過程中會根據程序員的指令,將可能性更大的代碼緊跟著前面的代碼,從而減少指令跳轉帶來的性能上的下降。
2. 實際動手驗證
那我們實際動手寫兩個例子,來窺探一下編譯器是如何進行優化的。為此我寫了一段簡單的測試代碼,如下:
#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,也就是假設這個條件為真的概率更大。那么我們來看看它編譯后的匯編碼來看看。
圖中上面紅框內是對 if 的匯編結果,可見它使用的是 jne 指令。它的作用是看它的上一句比較結果,如果不相等則跳轉到 4004bc 處,相等的話則繼續執行后面的指令。
在 jne 指令后面緊挨著的是 n = n + 2 對應的匯編代碼, 也就是說它把 n = n + 2 這段代碼邏輯放在了緊挨著自己身邊。而把 n = n - 2 的執行邏輯放在了離當前指令較遠的 4004bc 處。
我們再把 likey 換成 unlikey 看一下,發現結果是正好相反,這次把 n = n - 2 的執行邏輯放在前面了,如圖。
注意,編譯時需要加 -O2 選項,使用 objdump -S 來查看匯編指令。為了方便大家使用,我把它寫到 makefile 里了,和測試代碼一起放到了咱們的 Github 上了。大家想動手的可以玩玩,地址:https://github.com/yanfeizhang/coder-kung-fu/tree/main/tests/cpu/test01
3. 性能提升原理
那這么做為什么就能夠提升程序運行性能呢?原因有兩個。
首先第一個原因就是 CPU 高速緩存。現代的 CPU 一般都有三級的緩存,用來解決內存訪問過慢的問題。訪問數據時優先從 L1 讀取,讀取不到再讀 L2、還讀不到就讀 L3,最后用內存兜底。
圖中的數據來源于飛哥早期的一次測試,參見實際內訪問延時
這里要注意的是,每一次緩存數據的單位都是以一個 CacheLine 64 字節為單位進行存儲的。假如說要查詢的數據在 L1 中不存在,那么 CPU 的做法是一次性從 L2 中把要訪問的數據及其后面的 64 個字節全部緩存進來。
假如下一次再執行的時候要訪問的指令在上一次已經在 L1 中存在了,那么就直接訪問 L1,就不必再從 L2 來讀取了。回到上面的例子,假如說我們執行完了 cmp 對比指令以后,判斷確實是要進加法分支,那緊接著就會執行 jne 后面的 mov xor 等指令大概率就可以從 L1 中取到了。
假如說 cmp 對比執行后,發現是要跳到 4004bc 處的指令執行。那大概率該位置處的指令還沒有被加載到緩存中(實踐中一個分支下可能會包含很多代碼,而不是像我這個例子中簡單的兩三行),就避免不了從 L2 L3 甚至是速度更慢的 L3 去讀取指令。
除了高速緩存這個原因以外,還有一個更底層的原理。那就是 CPU 的流水線技術。CPU 在執行程序指令的時候,并不是先執行一個,執行完了再運行下一個這樣的。而是把每個指令都分成了多個階段,并讓不同指令的各步操作重疊,從而實現幾條指令并行處理。
還拿上面編譯出來的匯編碼來舉例,程序中 cmp、jne、mov 幾個指令是挨著的,那么 CPU 在執行的時候實際是大概這樣的。
當 jne 指令正在執行的時候,后面的兩個 mov 指令都已經分別進入到譯碼和取址階段了都。假如說分支預測失敗,那么這工作就白干了。4. 小結
總結一下,今天分享的?likely 和 unlikely 其實是屬于是輔助 CPU 分支預測的性能優化方法。這就是 likely 和 unlikely 背后的這點小秘密。它是為了讓 CPU 的高速緩存命中率更高,同時也為了讓 CPU 的流水線更好地工作。
Linux 作為一個基礎程序,在性能上真的是考慮到了極致。內核的作者們內功都是非常的深厚,都深諳計算機的底層工作原理。為了極致的性能追求精心打磨每一個細節,非常值得我們學習和借鑒。
不過這里要說的一點是,likely 和 unlikely 的概率判斷務必準確。如果寫反了,反而會起到反作用。
今天分享到此結束,求贊,求再看!
PS:?由于現在咱們公眾號攢了很多文章了,想在公眾號上定位某篇文章并不容易。所以推薦大家 star 咱們的 Github,上面有清晰完整的目錄結構,找起文章來更方便。點擊閱讀原文或者保存該地址:https://github.com/yanfeizhang/coder-kung-fu
總結
以上是生活随笔為你收集整理的你确定不反编译 likely 看看?的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 移动互联智慧杭州、技术精英引领中国
 - 下一篇: cpu与计算机其他的通信,PC与CPU2