contributed by <56han
>
Question: 為甚麼 x 和 y 都要加 0L,再進行位移?
Answer: 因為 x 和 y 的位元寬是 32 位元 ( uint32_t ) ,左移 32 位會超出它的位元寬,會出現 warning: left shift count >= width of type。long 是 64 位元,左移 32 位在這種情況下是合法的,不會引發警告。
(-1L >> 32)
計算出的值是0000 0000 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 1111 1111 1111 1111
。
延伸閱讀: SIMD and SWAR Techniques
SIMD 單指令流多資料流(Single Instruction Multiple Data,SIMD):是一種採用一個控制器來控制多個處理器,同時對一組資料中的每一個分別執行相同的操作,從而實現空間上的並列性的技術。
SWAR (SIMD within a register):在一個暫存器上進行單指令多資料流操作。它是一種跨 CPU 暫存器各部分應用 SIMD 平行處理的處理模型,在 SIMD(單指令多資料)並行處理模型中,這些 byte-entities(即小於一個字節的資料單位)經常被用來在一個 CPU 寄存器的不同部分上進行並行計算。
這樣的加法操作會直接對整個 64 位的數字進行運算,包括進位。
SWAR 技術則是通過在寄存器內部同時處理多個較小的資料單元來提高計算效率。例如,對於 64 位寄存器,我們可以將其視為包含了 8 個 8 位的 byte,並對每個 byte 進行獨立的加法運算。使用 SWAR 技術,我們可以在一個指令中並行地對所有 byte 進行加法運算。具體的 SWAR 技術加法公式如下:
其中,x &~H
和 y &~H
去掉了每個 byte 的最高位,以避免在進行無進位加法時受到影響,再透過 (x ^ y) & H
找出每個 byte 中的進位情況。
如此一來,在一次操作中就能對所有的 byte 進行加法計算,而不需要逐個處理進位,這大大提高了運算效率。
For bytewise (rankwise) math inside a 64-bit register, H
is 0x8080808080808080
and L
is 0x0101010101010101
.
詳細步驟:
x &~H
和 y &~H
去除了每個 byte 的最高位(sign bit),留下其餘 7 位來進行無進位加法。x ^ y
會找出 x
和 y
在每個 byte 中不相同的位元(即可能產生進位的位置)。然後與 H
進行 AND 操作來確保只保留最高位,透過這個操作,我們只保留了那些可能進位的位置。:H
= 1000 0000
x
= 0000 0001
y
= 1111 1110
計算非進位和:
~H
= 0111 1111
x &~H
= 0000 0001
y &~H
= 0111 1110
(x &~H) + (y &~H)
= 0111 1111
處理進位:
x^y
= 1111 1111
(x^y) & H
= 1000 0000
最終結果合併:
((x &~H) + (y &~H)) ^ ((x ^ y) & H)
= 1111 1111
本程式預期可以在一段 string 中找到一個字元,並印出該字元後的 string。
預期執行結果:
X
是否與 long
的邊界對齊。如果 X
沒有對齊,則返回非零值。為甚麼要檢查記憶體是否對齊?
memcpy
、memset
等)時,處理對齊問題可以使這些操作更加高效。long
型別的大小(通常為 4 或 8 Byte)。long
型別的大小。參考這篇文章。strlen
每次處理 1 個字元,因此需要 n 次比較 O(n)
,DETECT(X)
在 32-bit 下一次處理 4 個字元,在 64-bit 下一次處理 8 個字元,因此迴圈次數減少為 n/4
(32-bit)或 n/8
(64-bit)。理論上 DETECT(X)
版本的時間複雜度仍是 O(n)
,但它每次能處理多個字元,因此速度大約是 strlen
的 4 到 8 倍。
X
是否包含指定的字節,利用 XOR 操作,目的是將 X
中所有與 mask
相同的位元組變為 0。long
型別大小逐塊處理:建立 mask
,將 d
字節複製到 long
的每個位元組中。第三行的 mask
= 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010 1110 0010 1110
第四行的 mask
= 0000 0000 0000 0000 0000 0000 0000 0000 0010 1110 0010 1110 0010 1110 0010 1110
第六行的 mask
= 0010 1110 0010 1110 0010 1110 0010 1110 0010 1110 0010 1110 0010 1110 0010 1110
memchr_opt
,設計實驗來觀察隨著字串長度和特定 pattern 變化的效能影響clock_gettime
)記錄每次函數運行的時間。measure_memchr
和 measure_memchr_opt
:分別測量 memchr
和 memchr_opt
的執行時間。
run_experiment
:測試不同長度的字串,並在不同位置測試字母的存在。使用多次運行來獲取平均執行時間。其中 void * memset ( void * str , int c , size_t n )
函數將指定的值 c
複製到 str
所指向的記憶體區域的前 n
個位元組中,這可以用於將記憶體區塊清除或設定為特定值。
main
:呼叫 run_experiment
,測試不同的字串長度。
輸出:
從結果中可以看到,大部分情況下,memchr
的運行時間比 memchr_opt
短,這可能是由於以下幾個原因:
memchr_opt
在開始時進行了額外的對齊檢查和預處理步驟,這增加了開銷。memchr_opt
進行了對齊檢查,這會導致在字串長度較小或未對齊時,額外的開銷可能超過了任何潛在的優化收益。當字串較短時,對齊檢查和其他操作可能反而降低了效能。memchr_opt
中的條件分支較多,例如對齊檢查和字串長度檢查,可能會導致分支預測失敗,這會影響效能。memchr_opt
利用了 SIMD 技術進行批量處理,這可能在更大資料塊下顯示出優勢,但在較小資料塊下,這些優化的開銷可能反而導致性能下降。Linux 核心原始程式碼找出 x86 對應的最佳化實作:arch/x86/lib/string_32.c
repne scasb
指令進行快速字節掃描。memchr_opt
實作策略unsigned long
)進行處理,以加快速度。DETECT_CHAR
巨集進行高效比對。x86 最佳化實作:利用了 x86 指令集的特性,通過 inline assembly 直接操作 CPU 指令來提高效能,適合 x86 平台,簡單高效。
memchr_opt
實作:使用通用的 C 語言進行優化,具有跨平台的可移植性,但在特定平台上的性能可能不如專門針對該平台進行優化的實作。
若干處理器架構無法有效地處理除法,且我們想避免除法造成的例外狀況 (如 Division by zero),於是我們考慮以下實作。
likely
巨集__builtin_expect
,告訴編譯器 x
的值很可能為真,以便進行更好的分支預測和優化。為什麼要使用 __builtin_expect
函數?它究竟有什麼作用呢?
__builtin_expect
函數來增加條件分支預測的準確性,CPU 會提前裝載後面的指令,遇到條件轉移指令時會提前預測並裝載某個分支的指令。編譯器會產生對應的代碼來優化 CPU 執行效率。n
是 2 的冪,則它的二進位表示中只有一個位為 1,n & (n - 1) 應該等於 0。x
的二的對數__builtin_clz
計算前導零的數量,再根據位元數進行轉換。__builtin_clz
這個函數作用是傳回輸入數二進位表示:從最高位元開始 ( 左起 ) 連續的 0 的個數;如果傳入 0 則行為未定義。
三個不同的函數分別用於 unsigned int 、 unsigned long 以及 unsigned long long。
__builtin_constant_p
是編譯器 GCC 內建函數,判斷 n 在 compiler time 是否為常數,是的話就 return 1
並使用 (n) < 2 ? 0 : 63 - __builtin_clzll(n)
,否則 return 0
並使用 (sizeof(n) <= 4) ? __ilog2_u32(n) : __ilog2_u64(n)
,但根據 GCC 提供的文件,這不代表 n
不是常數,只是 GCC 無法證明它是在活躍的優化選項集限制內的常數。The function returns the integer 1 if the argument is known to be a compile-time constant and 0 if it is not known to be a compile-time constant. A return of 0 does not indicate that the value is not a constant, but merely that GCC cannot prove it is a constant within the constraints of the active set of optimization options.
在活躍的優化選項集限制內的常數 ( a constant within the constraints of the active set of optimization options ) 是甚麼?
This approach multiplies by the reciprocal of b: to divide n by b, we multiply n by ( p / b ) and then divide by p.
= n
因為乘法運算比除法運算在 CPU 上執行更快,它需要更少的 CPU 週期和更少的指令數量。透過事先計算除數的倒數,使得除法轉換為乘法,從而提高運算效率。
The efficiency comes from the compiler's ability to optimize much of this code during compile time through constant propagation, leaving behind only a few multiplication operations. This is why we use this large macro, as a static inline function does not consistently achieve the same optimization.
編譯器在編譯期間能夠透過 constant propagation 來優化這段代碼,最終只留下少量的乘法操作,因此提高運算效率。constant propagation:一種編譯器優化技術,編譯器會分析程式碼中的常數,並將這些常數值直接替換到程式碼中的變數,從而減少運行時的計算量。
為甚麼巨集可以達到更好的 constant propagation 效果?而 inline function 不行?
主要原因是巨集在預處理階段進行文本替換,使得編譯器能夠更早、更靈活地對展開的程式碼進行優化。而 inline function 仍然保留了函數的特性和約束,優化效果可能受到限制。因此,在需要充分利用 constant propagation 進行優化的情況下,巨集通常比 inline function 更有效。
___p
___p
: 接近 ___b
的 2 的冪。
___m
,使其接近 ((p << 64) + b - 1) / b
的值第一步
第二步
這是一個修正項,確保結果精確:
兩步合起來,正好等價於 ((p << 64) + b - 1) / b,這個表達式計算了 ( × p) ÷ b 。
___x
是能被 ___b
整除的最大數減 1目的:在進行大數除法時,可以用來檢查和處理極限情況。
res = (___m * ___x) / (2^64 * ___p)
的方式。___res
之後做為判斷 bias 之標準,如果 ___res = ___x / ___b
,則 m 足夠精確;否則需要 bias 修正。這是檢查除數 ___b
是否符合特殊模式:檢查 -1 是否能被 b 的奇數部分整除。
(___b & -___b)
提取 ___b
的最低位 1 (即 2 的冪次部分)___b / (___b & -___b)
獲得 ___b
的奇數部分~0ULL % 奇數部分 == 0
,判斷是否可以簡化若符合,代碼執行特殊處理:
發現了一個數學特性:除數 b 可以分解為 b = 2^k × q,且 q 是一個能夠整除 2^64-1 的奇數。
___n /= (___b & -___b)
對於 2^k 部分,直接使用位移操作:n /= (b & -b) 等同於 n >>= k
___m = ~0ULL / (奇數部分)
m = ~0ULL / q
___p = 1
和 ___bias = true
else if (___res != ___x / ___b)
,說明表示 m 不夠精確,需要 bias。也就是說,透過乘法和位移運算來近似除法的過程中,出現了誤差(由於有限精度和位截斷引起)。
It is necessary to introduce a bias to counteract errors caused by bit truncation.
在數字運算中,特別是當我們處理浮點數或大數進行除法、乘法等運算時,計算結果可能超出變數類型所能表示的範圍。為了解決這個問題,我們通常會截斷多餘的位元,這個過程稱為位元截斷(bit truncation)。在這段程式碼中,位元截斷可能會在進行乘法和除法運算時發生。當結果超過了變數所能表示的位數時,多餘的位數會被丟棄,這可能導致數值誤差。為了減少或抵消這些誤差,需要引入一個偏差(Bias)。這個偏差是一個額外的數值,用來調整計算結果,使其更接近真實值。
Without this bias, an extra bit would be required to accurately represent the variable 'm', which would exceed the capacity of a 64-bit variable. To manage this, the solution involves setting 'm' equal to 'p' divided by 'b', and 'n' divided by 'b' as equal to (n * m + m) / p. This approach is taken because avoiding the bias is not feasible due to the limitations imposed by bit truncation errors and the 64-bit variable size constraint.
位元容量限制:如果沒有引入偏差,我們需要額外的位元來表示計算過程中的變數 m
。這是因為在計算過程中,可能會出現結果超過 64 位元變數所能表示的範圍。
解決方案:重新計算 m,使用標準公式:
Question: 目前不理解為甚麼在變數 m
、 p
、 b
沒有改變的情況下,這裡需要再計算一次變數 m
? 使m = (p << 64) / b
。
原本計算過一次 (m = ((p << 64) + b - 1) / b)
,差異是 (b - 1)/b
。
原本:
現在:
Answer: 帶入數值分析 (b = 10, p = 16)
初始 m 值計算:
~0ULL / 10
= ( -1) / 10 = 1844674407370955161~0ULL % 10
= ( -1) % 10 = 5(5 + 1) * 16
= 96(96 + 10 - 1) / 10
= 105 / 10 = 10.5 → 10m = 1844674407370955161 * 16 + 10
= 29514790517935282586重新計算 m:
(~0ULL % 10 + 1) * 16 / 10
= 96 / 10 = 9.6 → 9m = 1844674407370955161 * 16 + 9
= 29514790517935282585差值確實是 1,相當於 (b-1)/b
的整數部分。
為什麼需要這種差異?
原因在於精度與偏差(bias)的平衡:
m
需要更精確,包含 (b-1)/b
這個微小調整。(n*m + m)/2^64/p
,公式本身已包含補償,所以 m
不需要額外的 (b-1)/b
調整。數學原理:
如果 m 足夠精確,代碼進入優化階段,試圖簡化 m 和 p 的值(移除 m 和 p 的共有因子)。
在數學上,如果 m/p = k 是一個常數,那麼 = k 仍然等於相同的常數。這裡代碼試圖找到 m 和 p 的最大公約數(或近似值),然後同時除以它,保持比值不變但降低數值大小。
___bits = -(___m & -___m)
:
(___m & -___m)
獲取 m 的最低位 1___bits |= ___m >> 32
:
___bits = (~___bits) << 1
:
___bits
的值決定如何縮小 ___m
和 ___p
:
___bits == 0
:表示必須設置 bit 31,此時除以 m 的最低位 1___bits
的最高位 1 來決定移位量,同時對 m 和 p 右移Now we have a combination of 2 conditions:
Condition 1 : whether or not we need to apply a bias.
Condition 2 : whether or not there might be an overflow in the cross product determined by(___m & ((1 << 63) | (1 << 31)))
.
Select the best way to do(m_bias + m * n) / (1 << 64)
.
From now on there will be actual runtime code generated.
__xprod_64
函式中判斷是否需要偏差來抵消因位元截斷造成的誤差。__xprod_64
函式檢查 (___m & ((1 << 63) | (1 << 31)))
來確定是否會發生溢位。1 << 63
和 1 << 31
分別是第 63 位和第 31 位。___m
的第 63 位和第 31 位被設置,可能會導致乘法運算中的溢位問題。根據這兩個條件,選擇最佳的計算方法來進行 (m_bias + m * n) / (1 << 64)
的運算。這裡 m_bias
是偏差,m
是乘數,n
是被除數的一部分。
__xprod_64
會根據是否應用偏差來決定如何進行運算,以避免溢位並保證結果的準確性。
___p
用於將結果進一步調整到正確的範圍內。
__xprod_64
計算乘積retval = ((bias ? m : 0) + m * n) >> 64
根據是否應用偏差進行條件判斷:
Case 1:無偏差。
Case 2:有偏差但無溢出風險。
Case 3:有偏差且有溢出風險。
計算 m_lo * n_hi
和 m_hi * n_lo
,並將結果右移 32 位。
Case 1:無溢出風險。
Case 2:有溢出風險。
加上 m_hi * n_hi
的乘積,最終結果存儲在 res
中並返回。
快速處理高位元
帶入數值分析:計算 12345678901234 ÷ 100:
這個初步優化可能大幅減少計算時間。
標準二進制長除法算法
帶入數值分析:計算 1234 ÷ 10:
初始值:rem = 1234, b = 10, d = 1, res = 0
第一階段(左移):
第二階段(右移與減法):
結果:商 = 123,餘數 = 4
這正是 1234 ÷ 10 = 123 餘 4。
__rem
。__base
是常數且是 2 的冪。__base
是常數且不為 0。n
小於 232。__base
不是編譯時常數,並且 n
是一個大於 232 的大數。補充:__builtin_constant_p(__base)
會是 false 的情況
res
進行除法運算,並輸出商數。uint64_t r = div64(res, 3);
,則 r
為餘數。argc
的值
argc
是命令行參數的數量。當執行程式時,命令行的每一個參數都會被計算在內,包括程式的名稱。
例如,如果在命令行中輸入 ./program arg1 arg2
,則 argc
的值為 3(包括程式名和兩個參數)。因此,argc
的值取決於如何執行程式,它不是每次都固定的。
如果以 ./program
執行程式(即沒有額外的參數),argc
是 1。
Linux 核心原始程式碼:include/asm-generic/div64.h
探討 do_div()
巨集在不同除數情境下的表現。
步驟1:撰寫測試模組
撰寫一個 Linux 核心模組,實現以下功能:
BITS_PER_LONG == 32
,所以可以分成以下四個情境討論。do_div()
的執行時間和結果。Jiffies 是什麼?
Jiffies 為 Linux 核心變數( 32 位元變數,unsigned long),它被用來紀錄系統自開機以來,已經過多少的 tick。每發生一次 timer interrupt,Jiffies 變數會被加一。值得注意的是,Jiffies 於系統開機時,並非初始化成零,而是被設為 -300 HZ (arch/i386/kernel/time.c),即代表系統於開機五分鐘後,jiffies 便會溢位。那溢位怎麼辦?事實上,Linux 核心定義幾個巨集(timer_after, time_after_eq, time_before 與 time_before_eq),即便是溢位,也能藉由這幾個巨集正確地取得 jiffies 的內容。
步驟2:編譯並載入模組
編寫 Makefile 以編譯模組:
編譯模組:
載入模組:
查看核心訊息以獲取測試結果:
do_div()
的執行時間計算常數除數的平均時間與非常數除數的平均時間,然後計算效率提升(Performance Gain):
計算結果顯示,使用常數除數時的效率提升約為 33.3%。
當除數是常數:對於 2 的次方的除數,編譯器可以使用右移操作來代替除法操作(例如 n >> 3
來代替 n / 8
),這比實際的除法運算要快得多。case 2 中,雖然除數不是 2 的次方,但由於除數是常數,編譯器可以進行一些數學優化,使得除法運算更高效。
當除數不是常數:
無法使用位元運算或乘法反元素等編譯時期優化技術,必須在運行時進行實際的除法運算,導致運行時間較長。當 n
的值較大且 base
不是常數時,除法運算需要進行完整的 64 位整數除法,這是一個相對複雜且耗時的運算過程,因此運行時間最長。
在不同 case 下的效益和限制
case 1:常數除數且為 2 的次方
效能:右移運算比標準除法運算快很多,因為位運算在硬體上直接支持且執行速度快。
限制:僅適用於除數為 2 的次方的情況,其他除數不適用。
case 2:常數除數但不是 2 的次方
效能:編譯期可以進行一些優化,例如使用乘法和位移來模擬除法,從而提升運行速度。
限制:僅在除數為常數的情況下有效,變量除數無法應用這種優化。
case 3:除數是動態變數,且 n
的高 32 位為 0
效能:直接使用 32 位的除法運算,效率相對較高。
限制:僅適用於 n
可以用 32 位表示的情況,對於更大的數值無法應用。
case 4:除數和被除數都是變量且超過 32 位
效能:需要進行 64 位的除法運算,並且在運行時動態計算,速度較慢。
限制:無法進行編譯時期優化,完全依賴於運行時的計算,效能較低。