貢獻者: jserv, TibaChang, Tcc0403
本文探討最大公因數 (Greatest Common Divisor, GCD) 特性,考慮微處理器架構實作帶來的效能衝擊,並探討如何運用 Linux 核心提供的 perf 工具進行效能分析並改進。
Greatest Common Divisor 中譯「最大公因數」,簡稱 GCD。
給定整數 ,二者各自有自己的因數[1],取相同的因數中最大的數,即最大公因數
是取餘數運算,表示存在 滿足
歐幾里得法,也就是「輾轉相除法」
給定整數 求出
令 ,根據上述 GCD 性質:
展示動畫
綜合上面過程,可得程式碼實作:
對於所有整數 ,存在整數 使得
給定整數 求出整數 滿足
令 ,配合上述 Bezout's Theorem 可得:
此處
即 為任意整數
而根據 GCD 性質
以及
得到
也就是說
綜合上述過程:
延伸閱讀:
回顧歐幾里得法:
示範 GCD(48, 40)
的計算過程
用 C 語言實現歐幾里得法: (v1)
以 Intel C compiler 產生的機械碼來說: (Pentium 4)
資料來源: The Software Optimization Cookbook: High Performance Recipes for IA-32 Platforms, 2nd Edition
針對 2004 年的個人電腦
指令 | 數量 | 延遲 | cycle count |
---|---|---|---|
減法 | 5 | 1 | 5 |
比較 | 5 | 1 | 5 |
分支 | 14 | 1 | 14 |
其他 | 0 | 1 | 0 |
24 |
改成 mod 操作的 C 語言程式如下: (v2)
以 Intel C compiler 產生的機械碼來說:
指令 | 數量 | 延遲 | cycle count |
---|---|---|---|
mod (整數除法) | 2 | 68 | 136 |
比較 | 3 | 1 | 3 |
分支 | 3 | 1 | 3 |
其他 | 6 | 1 | 6 |
148 |
若可排除整數除法和減法的負擔,得到以下實作: (v3)
可得到更快的實作:
上述都是針對 2004 年的個人電腦所得的實驗結果。
採用組合語言撰寫 (GCD 的部份),由於 Cortex-A8 不支援 idiv (要在 ARM Cortex-A15 或 A7 以上才有),因此改用以 Arm 組合語言實作
Assembly
Euclidean(thumb) V.S. mod(thumb) V.S. mod_minus(thumb)
不符合預期的原因推估
Euclidean(arm) V.S. mod(arm) V.S. mod_minus(arm)
比較
binary GCD 的精神就是當兩數為偶數時,必有一 因子。
且一數為奇數另一數為偶數,則無 因子。
於是我們可以改良為:
其中符號 是 right shift。
剩下的過程就直接採用 Euclidean algorithm 的作法即可。
注意,僅在微處理器具備有效的 ctz
指令時,Binary GCD 才會展現相較於 Euclidean 的優勢,這也是為何 Linux 核心提供二種 gcd 實作。
延伸閱讀:
it requires GCC or Clang, a 64-bit x86 architecture, and a target CPU that supports the BMI2 opcodes (i.e. Haswell or later, in the line of Intel CPU).
考慮以下實作:
GCD 演算法
以下分段說明:
對應至 GCD 演算法第 2 點
若 u 和 v 中其中一數為 0 ,則直接回傳另一數
x | 0
會得到 x
對應至 GCD 演算法第 3 點
先取出u
和 v
之間屬於 的公因數,shift
即為最大的
對應至 GCD 演算法第4 點
當 u
為偶數時,可以除以
其中 while(!(v &1)) v /= 2;
對應至 GCD 演算法第 5 點
可以透過迴圈不變性 (loop invariant) 來幫助解讀程式
u
為被除數v
為除數當除數 v
為 0 時結束迴圈,被除數 u
為進入迴圈前 u
和 v
的最大公因數
事實上 u
在迴圈前不一定是被除數,因為先前透過 GCD 演算法第 4 點去除不必要的計算
最後回傳最大公因數時,乘上原先取出屬於 的公因數,可以透過左移達成
__builtin_ctz
改寫程式已知除法運算會耗費較多時間,我們可以利用 __builtin_ctz
(編譯器會產生對應到現代微處理器的專門指令) 搭配上述針對二進位特性調整的演算法,減少除法的使用
將一百萬組由 rand
函式生成的亂數傳入 gcd64
函式執行,並藉由 Linux 核心提供的 perf 工具來分析:
由上圖可以發現,花費 CPU 週期數最多的段落為迴圈當中的 v /= 2
運算,佔據約 28%
以下為改寫過後的程式
透過 perf 分析前後差異
將同樣一百萬筆由 rand
函式所產生的亂數傳入 gcd64
和 gcd64_ctz
函式比較,發現改寫過後的版本花費時間幾乎是原來的一半。
透過 perf 分析改寫程式
佔據花費時間最多的為分支指令 (如 jne),其次是迴圈內部的減法運算
整數 的因數為可整除 的數 ↩︎