Linux 核心快速運算 √x

整數 N 怎麼開平方根?

最簡單的做法,我們可以切一半取平方,然後比大小,夾取範圍。一直做二分逼近,直到我們可以接受的誤差。這樣做使用了大量乘除法,不僅了速度慢,也毫無「品味」。

讓我們來看看 Linux 核心如何實現?

unsigned long int_sqrt(unsigned long x)
{
	unsigned long b, m, y = 0;

	if (x <= 1)
		return x;

	m = 1UL << (__fls(x) & ~1UL);
    while (m != 0) {
        b = y + m;
        y >>= 1;
		if (x >= b) {
			x -= b;
			y += m;
		}
		m >>= 2;
	}

	return y;
}

這品味實在有點難以消化。

讓我們娓娓道來。

數值分析

假設我們知道答案 √N = y,也就是 y2 = N。若將 y 表示為 n 項常數的和,則可以寫成 yn = a1 + a2 + … + an。也就是說,我們可以透過從 a1 猜到 an 以求得 N 的平方根。這樣做在每次計算到前 m 項時,都必須得重新計算一次 ym2,並與 N 做比較,以求得下一項 am+1 的數值。

我們試著將前 m 項的平方展開,看看其平方和有沒有規律。

  • \(y_{1}^{2} = (0 + a_{1}^{2})\)
  • \(y_{2}^{2} = (0 + a_{1}^{2}) + (2( a_{1} ) + a_{2})a_{2}\)
  • \(y_{3}^{2} = (0 + a_{1}^{2}) + (2( a_{1} ) + a_{2})a_{2} + (2(a_{1} + a_{2}) + a_{3})a_{3}\)
  • \(y_{m}^{2} = \text{…}\)

可以觀察出:\(y_{m}^2 = y_{m-1}^2 + Y_{m}\),其中

\( \begin{align}
Y_{m} = (2 \sum_{i=1}^{m-1}a_{i} + a_{m}) \cdot a_{m}
\end{align}\)

若我們考慮計算到第 m 項的平方和與 N 的誤差 Xm,也就是 Xm = N – ym2。隨著 m 越接近 n,這項誤差就會越小。並且可依據前面的歸納,得出底下規律:

\(X_{m} = X_{m-1} – Y_{m}\)

而我們的目標,就是使得當 m = n 時,Xn 可以逼近 0。

上述式子可以用 n 位元 10 進位數字來替換。此時 yn = b110n-1 + b210n-1 + … + bn-1101 + bn ,其中 bi 為 1 ~ 9 自然數 。這樣 Ym 就會變成:

\( \begin{align}
Y_{m} &= (2 \sum_{i=1}^{m-1}b_{i}10^{n-i} + b_{m}10^{n-m}) \cdot b_{m} 10^{n-m} \\
&= (2\cdot 10^{n-m+1}\sum_{i=1}^{m-1}b_{i}10^{m-i-1} + b_{m}10^{n-m})\cdot b_{m}10^{n-m} \\
&= (20\sum_{i=1}^{m-1}b_{i}10^{m-i-1} + b_{m})\cdot b_{m}10^{2(n-m)}
\end{align}
\)

手算開平方

底下以求 √15200 為例我們一項一項試看看。當 N 等於 15200 時,由於 4 位數的平方就超過,n 取 3 即可。

  • a1 = b1·100。取 b1 使得 (b1·100)2 <= N < ((b1 + 1)·100)2。可得 b1 = 1。
    • 此時 y12 = a12 = 1002 = 10000。與 N 的誤差 X1 = 5200。
  • X2 = X1 – Y2 = 5200 – (20·1 + b2)·b2。取 b2 使得 X2 為最小正數。可得 b2 = 2。
  • X3 = X2 – Y3 = 800 – (20·12 + b3)·b3。取 b3 使得 X3 為最小正數。可得 b3 = 3。
  • 故所求 yn = 1·100 + 2·10 + 3 = 123。且誤差為 X3 = 71。

有點難懂?底下用類似長除法的視覺輔助,帶大家來手算開平方看看。

    求 √15200。

          1  2  3  <- yn
        √ 1 52 00
          1        <- 1002
    X1 ->   52
            44     <- (20·1 + b2)·b2
    X2 ->    8 00
             7 29  <- (20·12 + b3)·b3
    X2 ->      71
    

    二進位簡化

    由於電腦使用二進位的每一位元都是由 0 或者 1。從第 n 位元找到第 m 位元的表示式就可以寫成 y = bn + bn-1 + … + bm+1 + bm ,其中 bm 為 2m 或 0。則前述公式可改寫成:

    \(\begin{align}
    Y_{m} = (2 \sum_{i=n}^{m+1}b_{i} + 2^{m}) \cdot 2^{m}
    \end{align}\)

    欲確定 bm 為 2m 或 0,一樣可以透過逐項減少與 N 的誤差 \(X_{m} = X_{m+1} – Y_{m}\)。若Xm 仍大於等於 0,則為 2m 反之為 0 。若令 \(P_{m+1} = \sum_{i=n}^{m+1}b_{i}\) 則此時 \(Y_{m} = 2 P_{m+1} \cdot 2^{m}+ 2^{2m}\) 且我們將 Ym 的右邊兩項分別令為 cm、dm,可得:

    \(\begin{align}
    c_{m} &= 2 P_{m+1} \cdot 2^{m} \\
    d_{m} &= 2^{2m} \\
    Y_{m} &= c_{m} + d_{m}
    \end{align}\)

    分析 cm、dm 與下一項(第 m – 1 項)的關係,可得:

    \(\begin{align}
    c_{m-1} &= 2 P_{m} \cdot 2^{m-1} = 2 (P_{m+1} + b_{m}) \cdot 2^{m-1} \\
    &= P_{m+1}2^{m} + 2 b_{m} 2^{m-1} = P_{m+1}2^{m+1}/2+b_{m}2^{m} \\
    &= \begin{cases}
    c_{m}/2 + d_{m} & \text{if } b_{m} = 2^{m} \\
    c_{m}/2 & \text{if } b_{m} = 0 \end{cases} \\
    d_{m-1} &= 2^{2(m-1)} = 2^{2m-2} = 2^{2m}/4 \\
    &= d_{m}/4 \\
    \end{align}\)

    留意所求 y = P0 = c-1

    將以上數值分析寫成程式。

    unsigned long int_sqrt(unsigned long x)
    {
        unsigned long d;
        unsigned long c = 0; // c_n
        if (x <= 1)
            return x;
        
        // d_n is the highest bit we need to check
        // such that 2^2n < x
        d = 1UL << (__fls(x) & ~1UL);
    
        // for each d_m where d_m is d_n ... d_0
        while (d != 0) {
            if (x >= c + d) {      // if X_m = X_(m+1) - Ym ≥ 0, then b_m = 2^m
                x -= c + d;        //   store X_m
                c = (c >> 1) + d;  //   store c_(m-1)
            } else {               // else b_m = 0
                c = c >> 1;        //   store c_(m-1)
            }
            d >>= 2;               // store d_(m-1)
        }
        return c;                  // c_(-1)
    }

    終於搞定!

    回頭對照一開始 Linux 核心的版本,發現還有再進一步簡化,以減少些許重複計算。實在歎為觀止。

    後記

    也許自己不是資工背景,光是了解數值運算方程就花了我好長時間。好不容易理解了方程,一變成程式碼,腦袋又卡住,無法一一對應前面結論,故撰文重新整理。除對齊所有變數之外,也補充了一些當初自己卡住的細節及說明。

    References