Try   HackMD

2024q1 Homework4 (quiz3+4)

contributed by < yeh-sudo >

第三周測驗題

測驗 1

解釋程式碼運作原理

假設要求的平方根為

x ,並且將
x2
表示為以下形式:

x2=(000b0b1b2...bn1bn)2

其中

000b0b1b2...bn1bn 為 bit pattern ,
b0
為最高位的 1 。所以
x2
可以寫成:

x2=(b02n+b12n1+b22n2+...+bn121+bn20)2

a0=b02n,a1=b12n1,a2=b22n2...an1=bn121,an=bn20
x2
可以表示成:

x2=(a0+a1+a2+...+an1+an)2

根據 Digit-by-digit calculation

x2=(an+an1+an2+...+a1+a0)2=an2+2anan1+an12+2(an+an1)an2+an22+...+a12+2(i=1nai)a0+a02=an2+[2an+an1]an1+[2(an+an1)+an2]an2+...+[2(i=1nai)+a0]a0

Pm=i=mnai ,則
x2
可以表示為:

x2=an2+[2Pn+an1]an1+[2Pn1+an2]an2+...+[2P1+a0]a0

P0=an+an1+an2+...+a1+a0 即為要求出的平方根。所以,從
m=n
一路試到
m=0
,測試
Pm2x2
是否成立,分為兩種情況:

{Pm=Pm+1+amif Pm2x2Pm=Pm+1otherwise

在檢驗時,如果每次都要計算

Pm ,會需要很高的成本,所以可以利用重複利用每一次計算的差值
X
,可以得出:

Xn=x2Pn2=x2an2

Xn1=x2Pn12=x2(an+an1)2=x2an2an122anan1=(x2an2)an122anan1=Xnan122anan1=Xn[2an+an1]an1=Xn[2Pn+an1]an1

Xn2=x2Pn22=x2(an+an1+an2)2=x2an2an12an222anan12anan22an1an2=Xn[2Pn+an1]an12anan22an1an2an22=Xn1[2(an+an1)+an2]an2=Xn1[2Pn1+an2]an2

...

Xm=Xm+1[2Pm+1+am]am

Ym=[2Pm+1+am]am ,則可以寫成以下遞迴關係:

Xm=Xm+1Ym

Ym 拆成
cm
dm
兩部份,

Ym=[2Pm+1+am]am=2Pm+1am+am2=2m+1Pm+1+(2m)2=cm+dm

{Ym=cm+dmif am=2mYm=0otherwise

{cm=2m+1Pm+1dm=(2m)2

可以從

cm
dm
推出下一輪的
cm1
dm1
,再用
cm1
dm1
推出
cm2
dm2
,直到推出
c1=P0
即為所求的平方根
x
,而首項
cn=0,dn=(2n)2

cm1=2mPmdm1=(2m1)2=(2m)222=dm/4

此處的

Pm=Pm+1+am 成立的條件為
Pm2x2
,若不成立,則
Pm=Pm+1
,所以當
Pm2x2
時,
am=2m
,反之則
am=0
。所以最後可以將
cm
寫成以下關係:

cm1={cm/2+dmif am=2mcm/2if am=0

在程式碼中,

cm 對應到變數 z
dm
對應到變數 m , 差值
X
對應到變數 x 。由於是紀錄差值,所以每當差值大於等於
cm+dm
,也就是 b = z + m 時,就將差值扣掉 b ,並且,因為差值為正,代表
Pm2x2
,也就是
am=2m
,所以
cm1=cm/2+dm
,對應到的程式碼操作為 z += m ,持續進行迴圈直到 m 為 0 時,結束迴圈。

int z = 0;
for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= 2) {
    int b = z + m;
    z >>= 1;
    if (x >= b)
        x -= b, z += m;
}

以 ffs / fls 取代 __builtin_clz

程式碼 (31 - __builtin_clz(x)) 的作用是找到最大的 bit 所在的位置,而使用第二週測驗題提供的 __ffs ,可以實作出找出對大 bit 的 fls 函式,實作方法是當 x 不為 0 時,持續尋找 ffs ,找到時就使用測驗題提供的 __clear_bit 將找到的 bit 清為 0 ,當 x 為 0 時,代表最後找到的 ffs 為最大的 bit 。

static inline unsigned long fls(unsigned long x)
{
    int result = 0;
    while (x) {
        unsigned int bit = __ffs(x);
        if (bit > result)
            result = bit;
        __clear_bit(bit, &x);
    }
    return result;
}

另外還有一個方法可以找出 fls ,將 x 的逐位元反轉,再用 31 減掉 ffs 即可得到 fls

static inline unsigned long fls(unsigned long x)
{
    unsigned long result = x;
    result = ((result & 0xffff0000) >> 16) | ((result & 0x0000ffff) << 16);
    result = ((result & 0xff00ff00) >> 8) | ((result & 0x00ff00ff) << 8);
    result = ((result & 0xf0f0f0f0) >> 4) | ((result & 0x0f0f0f0f) << 4);
    result = ((result & 0xcccccccc) >> 2) | ((result & 0x33333333) << 2);
    result = ((result & 0xaaaaaaaa) >> 1) | ((result & 0x55555555) << 1);
    return 31 - __ffs(result);
}

提供分支和無分支 (branchless) 的實作

在教材〈解讀計算機編碼〉中,有描述不需要分支的設計,當 INT32_MIN <= (b - a) <= INT32_MAX 條件成立時,可以使用 bitwise 操作改寫分支。

  • 改寫 i_sqrt
-        if (x >= b)
-            x -= b, z += m;
+        int tmp = x;
+        x -= (~(tmp - b) >> 31) & b;
+        z += (~(tmp - b) >> 31) & m;
  • 改寫 __ffs
 #if BITS_PER_LONG == 64
-    if ((word & 0xffffffff) == 0) {
-        num += 32;
-        word >>= 32;
-    }
+    num += !(word & 0xffffffff) & 32;
+    word >>= 32;
 #endif
-    if ((word & 0xffff) == 0) {
-        num += 16;
-        word >>= 16;
-    }
-    if ((word & 0xff) == 0) {
-        num += 8;
-        word >>= 8;
-    }
-    if ((word & 0xf) == 0) {
-        num += 4;
-        word >>= 4;
-    }
-    if ((word & 0x3) == 0) {
-        num += 2;
-        word >>= 2;
-    }
-    if ((word & 0x1) == 0)
-        num += 1;
+    num += !(word & 0xffff) & 16;
+    word >>= 16;
+    num += !(word & 0xff) & 8;
+    word >>= 8;
+    num += !(word & 0xf) & 4;
+    word >>= 4;
+    num += !(word & 0x3) & 2;
+    word >>= 2;
+    num += !(word & 0x1) & 1;
     return num;

Linux 核心原始程式碼

~/linux/block master ❯ grep sqrt *.c
blk-wbt.c: *   window is then shrunk to 100 / sqrt(scaling step + 1).
blk-wbt.c:                                      int_sqrt((rqd->scale_step + 1) << 8));

在 Linux 核心中, block 這個目錄下的 blk-wbt.c 有使用到整數平方根的操作,使用的函式為 rwb_arm_timer

  • blk-wbt.c
static void rwb_arm_timer(struct rq_wb *rwb) { struct rq_depth *rqd = &rwb->rq_depth; if (rqd->scale_step > 0) { /* * We should speed this up, using some variant of a fast * integer inverse square root calculation. Since we only do * this for every window expiration, it's not a huge deal, * though. */ rwb->cur_win_nsec = div_u64(rwb->win_nsec << 4, int_sqrt((rqd->scale_step + 1) << 8)); } else {

閱讀實作 writeback throttling 這個機制的作者 Jens Axboe 的 commit messages , throttling 這個機制最主要是讓背景執行的 buffered writeback 不會影響到其他正在執行的程式,作者發現在執行大量的 buffered writeback 時,他想要開啟 Chrome 會打不開。

But for as long as I can remember, heavy buffered writers have not behaved like that. For instance, if I do something like this:

$ dd if=/dev/zero of=foo bs=1M count=10k

on my laptop, and then try and start chrome, it basically won't start
before the buffered writeback is done. Or, for server oriented
workloads, where installation of a big RPM (or similar) adversely
impacts database reads or sync writes. When that happens, I get people
yelling at me.

這個機制有點像是 CoDel networking scheduling algorithm , blk-wb 監控一段時間中的最小延遲,如果這段時間中,有最小延遲超過設定的目標,就增加 scaling step 與壓縮佇列的深度,接著將監控的時間縮小 100 / sqrt(scaling step + 1) 。其中的整數平方根運算對應的程式碼在 /lib/math/int_sqrt.c ,其實作的演算法也是使用 digit-by-digit calculation 。

  • int_sqrt
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;
}

閱讀〈Analysis of the lookup table size for square-rooting〉

閱讀〈Timing Square Root〉