contributed by <Shiritai
>
即 trace linux kernel 中 bitops
後留下的問題提問、實作與實驗集合。
Shiritai
以下討論基於 Linux kernel 6.3.0 source code。
builtin-__ffs
和 builtin-ffs
前後兩者的實作…結果竟然不同,i.e. 計算結果不同!
照理來說 ctz(x) + 1 == ffs(x)
,為何 builtin-__ffs
竟然直接以 ctz
實作?
表示你發現實作缺失,很好!
參照 Patch subject prefixes:Another common Subject prefix is "RFC". This stands for "Request for Comment", which tells maintainers should review your patch thoroughly, and provide feedback. RFC is typically used when sending feature patches for the first time, or anytime the patch is more than just a simple bug fix. Developers will often use [RFC v2] on the second revision of their feature patchset.
你可提交 RFC patch,讓開發者參與討論,確認你的發現是否合理並值得改正。
jservImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
報告教授,參考 bitops
系列對外的介面: arch/arc/include/asm/bitops.h
,經過更仔細的 trace code 後,我理解其中的原理了: 註解寫得很清楚但我眼瞎了 QQ
__XXX
系列: 以 index 0 索引,範圍為 0 ~ {length of input type - 1}
XXX
系列: 以 index 1 索引,範圍為 1 ~ {length of input type}
我以此為基礎調整生成 ffs
、fls
系列的巨集,目前可適應以同一份巨集於編譯時期生成上述兩種情況的程式碼,並確立所有常數,不因此影響執行時期的效能。
凡是掛名 __builtin_XXX
的函式本身以 CPU 硬體指令實作,過濾例外情況 作為輸入以避免 undefined behavior 是必備手續;同時使用 ffs
, fls
時輸入 本身就是極其不合理的事,此時檢查例外情況依然重要。
不過有趣的是,bitops
目錄下大家眾口一詞:有的說例外情況的判斷交由 caller 檢查,有的沒特別說 (只說其與 builtin ffs 有同樣的 routine),有的直接為 為輸入定義對應輸出…
builtin ffs
有同樣的 routine)
想請問這是否為 Jserv 您於一對一討論時提到不同大公司背景之貢獻者 coding rule 的異同?
還有我看得頭好痛,其他開發者在使用這些函式時難道不會很困惑嗎?
工程背後就是一系列的取捨和溝通
jservImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
好難…不過現在算理解整個 ffs
、fls
與 bitops
系列的關係,實現並驗證了對應的重構程式碼,目標是杜絕所有魔法與重複的程式碼 (i.e. bad smell)。
我將撰寫對應的 trace code 與實作報告,屆時請務必提供您的意見。
BITS_PER_LONG
的來由我撰寫 clz
巨集生成系列、inf_bits
時,對於位元數我總是使用 sizeof
運算子的協助。
若今想定義 BITS_PER_LONG
增加程式碼的可讀性,利用 sizeof
運算子正是時候。但 include/asm-generic/bitsperlong.h
的寫法讓我感到疑惑。
看起來他們有意避免使用 sizeof
運算子,想請問為什麼?
感覺可能與編譯器、C 語言規格、data model 有關,但不知道要從何找起…
這與 LP64 有關,這定義可能會被組合語言程式引用,因此用常數形式存在
jservImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
了解,感謝教授的回答,我想進一步提問:使用 sizeof
與依靠 BITS_PER_LONG
的結果是否一定相同?
如教授所知,我曾實作過巨集展開版 clz
、ctz
的軟體實作,目前也已經成功實作且驗證合乎我所認識的 ffs
、fls
巨集生成函式,在理解前述兩問題後,接著就是如何解決相容性問題。
相容性問題與其餘延伸問題整理如下:
ffs
系列,調整使用到 ffs
系列函式的核心原始碼?這聽起來是個浩大工程,一想到光是 trace code…bitops
作出顯著的調整,又要如何讓以後的開發者善用我們的調整?若能成功解決,也許能完成對 ffs
系列函式的重構,並有機會進一步使用這些巨集生成函式於其他型別,讓 ffs
系列更好用也更統一。
constant_fls
的存在意義本問題與編譯器有關。
arch/arc/include/asm/bitops.h
中存在一函式 constant_fls
,簽名與部分實作如下:
其目的是利用 gcc __builtin_constant_p
協助於編譯時期進行 Constant folding (propagation),於你所不知道的 C 語言:編譯器和最佳化原理篇介紹過其觀念。以下節錄 gcc 官方文件,說明 __builtin_constant_p
的使用:
You can use the built-in function
__builtin_constant_p
to determine if a value is known to be constant at compile time and hence that GCC can perform constant-folding on expressions involving that value. …… A return of
0
does not indicate that the value is not a constant, but merely that GCC cannot prove it is a constant with the specified value of the-O
option.You may use this built-in function in either a macro or an inline function. However, if you use it in an inlined function and pass an argument of the function as the argument to the built-in, GCC never returns
1
when you call the inline function with a string constant or compound literal (see Compound Literals) and does not return 1 when you pass a constant numeric value to the inline function unless you specify the-O
option.
表示當 __builtin_constant_p
用於 macro 或 inline function 時,傳入的參數為 常數數值,當開啟編譯器最佳化 -O
選項後即會進行 constant folding。
綜觀 constant_fls
函式,我們看不出其與 bitops/fls
有任何差別。這表示就算不實現 constant_fls
,也能利用上述 bitops/fls
函式協助編譯器進行 constant folding。
那麼問題就出現了:
constant_fls
實現的意義為何?bitops/flz
或 constant_fls
的實作,對於 bitops.h
中 fls
傳入參數為 __builtin_constant_p(x) == 1
時的效能是否有影響?即是否有可能影響 constant folding?(我覺得不會)另附上 bitops.h
中 fls
函式以供參考。
有一個小小細節是: 使用 if-else
實作系列函式的一個優點是可以略過部分不必要的操作,比如 __fls
函式: (這點 __ffs
亦同)
但隔壁鄰居 fls
(ffs
亦同) 可沒打算那樣做:
更有趣的是他的遠親 constant_fls
倒是考慮到了這點。
這是一個提出一個究極不起眼 patch 的機會嗎?
Shiritai
不過我更有興趣的是怎麼更徹底的解決 bitops
中實作混亂的問題,這引出下一個問題…
本問題已經解決,留下實驗與討論紀錄。
讓編譯器為我上一堂程式設計課的同時,驗證了我的實作應用於 Linux Kernel 的可行性。
x64
組語小實驗開始我嘗試將自己的 loop 版 ffs
置於 util.h
轉為組語,其在 main.c
中被呼叫並印出結果,以下節錄實作:
main.c
中故意使用 srand
、time(NULL)
和 rand
是為了不希望 compiler 將輸入設為常數;使用 printf
是希望 compiler 沒有機會把生成的 inline function 整個優化掉,強迫一定要取得結果並印出。
使用 cc main.c -O -std=c99 -S
編譯,產生的組語節錄如下:
這裡可以觀察到一些有趣的現象。
__gen_ffs
中 mask
變數確實透過編譯器的協助轉為常數 65535
(0xffff
),這符合預期。
util.h
第 13 行改為 mask = (in_type) ~(_ONES << shift);
後編譯為組語,發現得到一樣的結果。mask
常數,事實證明編譯器實在太聰明了!my_ffs
(使用巨集生成的 ffs
函式) 組語迴圈也很有趣。
.L4
以降第 16、17 行對應 while (shift)
,但在我發現原 C code 還有優化空間 (將 while
轉為 do-while
),調整並重新編譯為組語後,驚愕地發現「完全沒有變化」!x86
的 test
指令發現,原來其非條件判斷,而是 bitwise AND 運算,對應第 13 行 x & shift
的部分。x86
後發現,原來編譯器自動幫我將 while
轉為 do-while
,不得不說真的很聰明,直接體會到編譯器的強大。另外,對 x86_64
舉燭實在太累人了,不如把它學起來吧!
承襲於修讀計算機結構時學 RISC-V 的方法,把 cheat sheet 拿出來讀!
為啥我不早點拿出來呢… Shiritai
編譯器神通廣大,即使沒修過編譯器的我都聽過其中一個知名的功能:loop unrolling。透過將迴圈展開為只向下執行的程式碼,降低指令往前轉跳的 control hazard 和 instruction cache 成本。
參考本討論串,其討論到適合使用 loop unrolling 的時機:
對次數少的小迴圈有效,反之則要付出大代價。
平常依靠編譯器自動最佳化即可,正如 Donald Knuth 所言:
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Donald Knuth
明顯前述 gen_ffs
或者說 my_ffs
屬於適合 loop unrolling 的情況,於是我嘗試使用 -funroll-loops
這個 gcc 編譯參數,其嘗試在編譯時期展開可預測迴圈執行次數的迴圈:
(左: 原版,右:unroll loops 版)
成功得到展開後的迴圈,而且由於將邏輯迴圈化之前的重複次數本來就很少,展開後才只多七行,除了預見效能的提升外想不到其他可能。
等等,為啥現在竟然成功 unroll 了?之前不行欸,我也嘗試把舊版 gen_ffs
拿來測試,怎麼這次又可以了?
極有可能的原因是之前有殘餘一個 util.h.gch
檔,即 precompiled header file,八成是它在搞鬼,導致不會重新編譯 my_ffs
,此時自然在不用編譯 my_ffs
的情況下沒有任何迴圈可以展開。
Shiritai
更令我驚訝的是,除了能正確的 unroll loop,針對整數輸入型別展開為 5 次 以外,編譯器甚至聰明到把最後一個 的情況優化成不去對 x
做無謂的運算,也就是 .L6
中不存在對 %eax (x)
的右移指令。
換言之,若將我的巨集生成函式應用於 Linux Kernel ffs/fls
系列函式的實作,不僅不會影響其效能,更能簡化原始碼和統一實作方法,同時提供一個能實作目前尚未實作之輸入型別的 ffs/fls
,一舉三得!
那麼下一步便是搞清楚 Linux Kernel 是如何編譯這些檔案,以確認迴圈展開與常數傳遞等效果是否真正的被編譯器採用。
希望調整 kernel 於編譯時,至少與 ffs
相關的部分採用 GCC 編譯時傳入 unroll-loops
參數的效果。
直接調整編譯參數有點不切實際,原因如下:
另一方面,我想到 gcc 可能提供了額外的標記來解決問題,於是找到本討論串,其介紹了兩種方法:
pragma + GCC global optimize + push pop
: 在函式前為 GCC 增加全域優化設定,函式後取消該新增。
pragma
表示編譯提示。
attribute
: 在函式前加上
於是我嘗試實驗這兩者於以下編譯環境:
發現後者沒有效果,前者雖然有,但不知為何在巨集生成函式下沒有效果。我嘗試過以下做法:
pragma
define
中不能用 #...
,故 #pragma
不能使用。(語法錯誤)。
_Pragma
運算子替代 #pragma
巨集_Pragma("GCC pop_options")
,編譯後發現竟然有效,但這樣調整全域優化設定非常不好,畢竟會影響到其他程式碼,我所追求的是只針對本函式的 loop unroll。#pragma GCC unroll n
。
以下節錄 GCC 規格書,說明語法中 n
的意義:
n is an integer constant expression specifying the unrolling factor. The values of 0 and 1 block any unrolling of the loop.
_Pragma
運算子將程式改為:
指定 是因為對於 位元而言,迴圈將走訪 這幾項可能。
不過此時讀者可能會好奇,這樣不就限制我抽象為巨集生成函式的作法只能用於 bits?
答案是其實不會!
雖然規格書沒有詳細提及,不過實驗表明,當可 unroll 的次數 小於指派的次數,結果只會展開 次。畢竟 GCC 原本的 funroll-loops
本來就有能力分析迴圈合理的展開次數。
不過這裡的「舉燭」是否有疏漏?!我如何確認此效果於編譯 Linux kernel 時是否有效?
Shiritai