# Greatest Common Divisor 特性和實作考量
> 資料整理: [jserv](https://wiki.csie.ncku.edu.tw/User/jserv)
## 簡介
探討最大公因數 (GCD) 特性,考慮微處理器架構實作帶來的效能衝擊,並探討 binary GCD 及其最佳化。
Greatest Common Divisor 中譯「最大公因數」,簡稱 GCD。
給定整數 $a, b$,二者各自有**自己的因數**[^factor],取**相同**的因數中**最大**的數,即最大公因數 $\gcd(a, b)$
## GCD 性質
- $\gcd(a, b) = \gcd(b, a)$
- $\gcd(a, 0) = |a|$
- $\gcd(a, b) = \gcd(a, b \% a)$
> $\%$ 是取餘數運算,表示存在 $k$ 滿足 $0 \leq b\% a = b - k\cdot a < a$
## Euclidean algorithm
> 歐幾里得法,也就是「輾轉相除法」
:::info
給定整數 $a, b$ 求出 $\gcd(a, b)$
:::
令 $a_0=a, b_0=b$,根據上述 GCD 性質:
$$
\begin{split}
\gcd(a_0, b_0) &= \gcd(b_0 \% a_0 = \underline{b_1}, a_0) \\
\gcd(\underline{b_1}, a_0) &= \gcd(a_0 \% b_1 = \underline{a_1}, b_1) \\
\gcd(\underline{a_1}, b_1) &= \gcd(b_1 \% a_1 = b_2, a_1) \\
&\vdots \\
\gcd(a_i, b_i) &= \gcd(b_i \% a_i = a_{i+1}, b_i) \\
&\vdots \\
\gcd(\mathbf{0}, b_n) &= |b_n|
\end{split}
$$
![](https://i.imgur.com/HtQU22S.gif) [^euclidean]
> 展示動畫
綜合上面過程,可得程式碼實作:
```c
int gcd(int a, int b)
{
return a ? gcd(b % a, a) : b;
}
```
## Bezout's Theorem
對於所有整數 $a, b$,存在**整數** $x, y$ 使得 $ax + by = \gcd(a, b)$
## Extended Euclidean algorithm
:::info
給定整數 $a, b$ 求出整數 $x, y$ 滿足 $ax + by = \gcd(a, b)$
:::
令 $g = \gcd(a, b)$,配合上述 Bezout's Theorem 可得:
$$
0\cdot x_n + g\cdot y_n = g
$$
此處 $x_n \in \mathbb{Z}, y_n = 1$
> 即 $x_n$ 為任意整數
而根據 GCD 性質 $\gcd(a_i, b_i) = \gcd(b_i\%a_i, a_i)$
以及 $b_i \% a_i = b_i - \lfloor{b_i\over a_i}\rfloor \cdot a_i$
得到
$$
\begin{split}
g &= x_j\cdot (b_i \% a_i) &+ y_j &\cdot a_i \\
&= x_j\cdot (b_i - \lfloor{b_i\over a_i}\rfloor \cdot a_i) &+ y_j &\cdot a_i \\
&= x_j \cdot b_i &+ (y_j - \lfloor{b_i\over a_i}\rfloor \cdot x_j) &\cdot a_i
\end{split}
$$
也就是說
$$
g = a_i \cdot x_{j-1} + b_i \cdot y_{j-1} \text{ for }\begin{cases}
x_{j-1} = y_j - \lfloor{b_i\over a_i}\rfloor \cdot x_j \\
y_{j-1} = x_j
\end{cases}
$$
綜合上述過程:
```c
/* It uses pointers to return multiple values */
int extended_gcd(int a, int b, int *x, int *y)
{
if (a == 0) {
*x = 0;
*y = 1;
return b;
}
int _x, _y;
int gcd = extended_gcd(b % a, a, &_x, &_y);
*x = _y - (b/a) * _x;
*y = _x;
return gcd;
}
```
延伸閱讀:
* [Extended Euclidean algorithm](https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm)
## GCD 實作和微處理器指令
回顧歐幾里得法:
1. 大數 := 大數 - 小數
2. 若「最大」和「最小」兩數相同,那麼它就是最大公因數,否則,回到第 1 步
示範 `GCD(48, 40)` 的計算過程
* 48 - 40, 40 $\to$ 8, 40
* 8 != 40, 於是重複上述動作: 40 - 8, 8 $\to$ 32, 8
* 8 != 32, 於是重複上述動作: 32 - 8, 8 $\to$ 24, 8
* 8 != 24, 於是重複上述動作: 24 - 8, 8 $\to$ 16, 8
* 8 != 16, 於是重複上述動作: 16 - 8, 8 $\to$ 8, 8
* 8 == 8 成立,於是 8 為 GCD
用 C 語言實現歐幾里得法: (v1)
```c
int findGCD(int a, int b)
{
while (1) {
if (a > b) a -= b;
else if (a < b) b -= a;
return a;
}
}
```
### 針對 [Pentium 4](https://en.wikipedia.org/wiki/Pentium_4) 硬體的實驗
以 Intel C compiler 產生的機械碼來說: ([Pentium 4](https://en.wikipedia.org/wiki/Pentium_4))
> 資料來源: [The Software Optimization Cookbook: High Performance Recipes for IA-32 Platforms, 2nd Edition](http://www.amazon.com/The-Software-Optimization-Cookbook-Performance/dp/0976483211)
> 針對 2004 年的個人電腦
|指令|數量|延遲|cycle count|
|:---:|:---:|:---:|:----:|
|減法|5 |1 |5 |
|比較|5 |1 |5 |
|分支|14 |1 |14 |
|其他|0 |1 |0 |
| | | |24 |
改成 mod 操作的 C 語言程式如下: (v2)
```c
int findGCD(int a, int b)
{
while (1) {
a %= b;
if (a == 0) return b;
if (a == 1) return 1;
b %= a;
if (b == 0) return a;
if (b == 1) return 1;
}
}
```
以 Intel C compiler 產生的機械碼來說:
|指令 |數量|延遲|cycle count|
|:-----------:|:--:|:--:|:--------:|
|mod (整數除法) |2 |68 |136 |
|比較 |3 |1 |3 |
|分支 |3 |1 |3 |
|其他 |6 |1 |6 |
| | | |148 |
若可排除整數除法和減法的負擔,得到以下實作: (v3)
```c
int findGCD(int a, int b)
{
while (1) {
if (a > (b * 4)) {
a %= b;
if (a == 0) return b;
if (a == 1) return 1;
} else if (a >= b) {
a -= b;
if (a == 0) return b;
if (a == 1) return 1;
}
if (b > (a * 4)) {
b %= a;
if (b == 0) return a;
if (b == 1) return 1;
} else if (b >= a) {
b -= a;
if (b == 0) return a;
if (b == 1) return 1;
}
}
}
```
可得到更快的實作:
```
v1: 14.56s
v2: 18.55s
v3: 12.14s
```
上述都是針對 2004 年的個人電腦所得的實驗結果。
### 針對 [Arm Cortex-A8](https://en.wikipedia.org/wiki/ARM_Cortex-A8) 進行實驗
* 平台:BeagleBone Black (ARMv7-A, 1 GHz)
* 實驗程式碼:[GCD_Efficiency_Compare](https://github.com/JaredCJR/GCD_Efficiency_Compare)
採用組合語言撰寫 (GCD 的部份),由於 Cortex-A8 不支援 idiv (要在 ARM Cortex-A15 或 A7 以上才有),因此改用以 Arm 組合語言實作
* 發現 gcc 的除法會針對 2 的倍數進行判斷,移位(除法)
* 實作的版本尚未考慮 exception
- [A Fast Hi Precision Fixed Point Divide](https://me.henri.net/fp-div.html) 列出 exception 重點
### 實驗方法
* 圖上一個點,代表 GCD (big, small) 中的 big,會尋找比它小的所有正整數(>1)的GCD所需的時間「總和」
* 若存在 big=9995,那麼會找 9993 組 GCD,small 的範圍從 2 到 9994
* 如: (9995,2), (9995,3), (9995,4), ..., (9995,9994)
* **所以 big 越大,找尋的組數越多,整體趨勢微微上升(從左到右)是正常的**
* 重點是比較各演算法的趨勢
### ARM vs. Thumb instruction
* ARM 指令執行較快
* 直線為 curve fitting 的結果 (時間越低,效能越高)
![image](https://hackmd.io/_uploads/rJNpsCGjC.png)
**Assembly**
```c
MOD:
@ Entry r0: low (remainder low) must be postive<input:Dividend>
@ r1: high (remainder high) any number
@ r2: Divisor (divisor) must be non-zero and
@ positive<input:Divisor>
low .req r0;
high .req r1;
divisor .req r2;
mov high,#0
.rept 33 @ repeat 33 times
subs high, high, divisor @ remainder = remainder - divisor
it lo
addlo high, high, divisor
@ low << 1; ((high-divisor) > 0) ? (new bit = 1) : (new bit = 0)
adcs low, low, low
@ divisor >> 1 == high(64bit for high & low) << 1
adc high, high, high
.endr
@ shift remainder to correct position
@ because last divide don't know this is the last divide,
@ it still prepare for the next divide<shift>)
lsr high, #1
@ Exit r0: Quotient (remainder low)
@ r1: remainder (remainder high)
mov pc, lr
```
**Euclidean(thumb) V.S. mod(thumb) V.S. mod_minus(thumb)**
不符合預期的原因推估
* v3 版本的 branch 過多(if,else),thumb 的 branch 效能不佳,導致拉低平均
* if 數量比較(一個 loop)
- v1 : 3 個
- v3 : 7 個
![image](https://hackmd.io/_uploads/HJJCiAGoC.png)
**Euclidean(arm) V.S. mod(arm) V.S. mod_minus(arm)**
比較
* 與 thumb 版本
* arm 的 branch 效能較佳,所以 v3 版本速度改善最多,這裡符合預期
* v1 輸給 v2
* arm 的 v1 與 thumb 的 v1 相比,只有些許提昇 (對照第一張圖),但是 mod 卻提昇較多(幾乎是半張圖片的範圍),導致這個結果
* curve fitting
![image](https://hackmd.io/_uploads/HJc0sRMjR.png)
## C 語言
* arm-angstrom-linux-gnueabi-gcc 內的除法經過最佳化後,效能比減法更佳
![image](https://hackmd.io/_uploads/r1-ghRfjC.png)
* arm-linux-gnueabihf-gcc(linaro) (without vfpv3)
* 上下圖比較,可看到整體在 arm-angstrom-linux-gnueabi-gcc 效能比較好
* 即使在這裡,單純除法比單純減法(的演算法)還快...
* 可是綜合起來看 v3 更快(符合預測)
![image](https://hackmd.io/_uploads/SJKWh0MiR.png)
* arm-linux-gnueabihf-gcc(linaro) (with vfpv3)
* mod 加快,減法變慢
* 開了導致反效果
![image](https://hackmd.io/_uploads/BkmM2Cfo0.png)
## [Binary GCD](https://en.wikipedia.org/wiki/Binary_GCD_algorithm)
> [Binary GCD Algorithm](https://iq.opengenus.org/binary-gcd-algorithm/)
binary gcd 的精神就是當兩數為偶數時,必有一 $2$ 因子。
$$\text{gcd}(x, y) = 2·\text{gcd}(\frac{x}{2}, \frac{y}{2})$$
且一數為奇數另一數為偶數,則無 $2$ 因子。
$$
\text{gcd}(x, y) = \text{gcd}(x, \frac{y}{2})$$
於是我們可以改良為:
$$
\text{even_factor} = \text{min}(\text{ctz}(x), \text{ctz}(y)) \\
\text{gcd}(x, y) = \text{even_factor}·\text{gcd}((x\gg \text{even_factor}), (y\gg \text{even_factor}))
$$
其中符號 $\gg$ 是 right shift。
剩下的過程就直接採用 Euclidean algorithm 的作法即可。
注意,僅在微處理器具備有效的 `ctz` 指令時,[Binary GCD](https://en.wikipedia.org/wiki/Binary_GCD_algorithm) 才會展現相較於 Euclidean 的優勢,這也是為何 Linux 核心提供二種 [gcd 實作](https://github.com/torvalds/linux/blob/master/lib/math/gcd.c)。
延伸閱讀:
* [The Speed of GCD](https://hbfs.wordpress.com/2013/12/10/the-speed-of-gcd/)
* [Fastest way to compute the greatest common divisor](https://lemire.me/blog/2013/12/26/fastest-way-to-compute-the-greatest-common-divisor/)
* [Greatest common divisor, the extended Euclidean algorithm, and speed!](https://lemire.me/blog/2024/04/13/greatest-common-divisor-the-extended-euclidean-algorithm-and-speed/)
## Optimized Binary GCD for Modular Inversion
> it requires GCC or Clang, a 64-bit x86 architecture, and a target CPU that supports the [BMI2 opcodes](https://en.wikipedia.org/wiki/Bit_manipulation_instruction_set) (i.e. Haswell or later, in the line of Intel CPU).
[bingcd](https://github.com/pornin/bingcd)
[^factor]: 整數 $n$ 的**因數**為可**整除** $n$ 的數
[^euclidean]: [Wikipedia/ 輾轉相除法的展示動畫](https://zh.wikipedia.org/zh-tw/%E8%BC%BE%E8%BD%89%E7%9B%B8%E9%99%A4%E6%B3%95#/media/File:Euclidean_algorithm_252_105_animation_flipped.gif)