contributed by < WangHanChi
>
1
題目為給定無號 64 位元數值 x
,找出最接近且大於等於 2 的冪的值
以下為一種實作的方法
但是為了減少 branch penalty ,所以我們更傾向使用 branchless 的版本
也就是以下
可以發現程式碼變得更精簡且沒有分支了,那 AAAA
, BBBB
以及 CCCC
又是什麼呢?
可以從程式的功能來看,輸入 x ,輸出最靠近且大於等於 2 的冪的值,也就是說輸出必定為 0b0100000
這類的答案,也就是二進制的表示式中只會有一個 1。接著看到上方的程式碼, x |= x >> 1;
它將小於他的位元都變成 1 。
以 64 為例子 (預期輸出 128 )
而 0111 1111 與 128 (1111 1111) 只差了 1 ,因此可以知道 CCCC
為 x + 1
為了滿足 x + 1
為2的冪,於是必須將所的位元都變成 1 ,因此就要重複 x |= x >> 1
這個操作多次。而將 x |= x >> 1
重複 8 次後,可以保證最高位數的 8 個位元都會是1,再來需要將接下來的 8 位元都設為 1 ,所以需要執行一次 x |= x >> 8;
,這邊比較奇怪的是好像題目少了這個步驟就直接執行了 x |= x >> 16;
這樣子會在輸入 x 小於 512 時輸出正確的答案,但是只要超過 512,就會輸出錯誤的答案了。
根據上述,我認為題目少了一行 x |= x >> 8;
完整程式碼應該如下
可以從 __builtin_clzl 這個網站中看到關於這個函式的內容
Built-in Function: int __builtin_clz (unsigned int x)
Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
Built-in Function: int __builtin_clzl (unsigned long)
Similar to __builtin_clz, except the argument type is unsigned long.
以 __builtin_clzl
為例,輸入一個 unsigned long 型別的數字 x ,它會回傳的型別為 int ,而內容就是從 x 的最高位 (MSB) 往最低位 (LSB) 的方向數共有幾的 0 ,如果在數的過程中遇到 1 ,就直接回傳數字。
可以用以下程式碼測試
它會回傳從 MSB 開始第一個不為 0 的位數,例如以 1025 為例,可以看到輸出為 10
接著將其應用在 next_pow2
上,於是可以寫出以下程式碼
可以看到輸出與 next_pow2
一致
稍微簡單寫了一個測試程式來檢測兩者的輸出是否為相同的,發現執行結果也是一樣的!
我目前發現有使用到 pow2(x)
這個類的函式
在以下程式碼可以看到第 331 行先定義了 pow2
這個巨集,然後他在第 351 行使用到它,主要的用途就是設定 virtual memory 的區域。
使用
來產生 objdump 的反組譯,並且將結果儲存在 builtin_pow2.dumptxt
可以看到在 X86-64 的環境下所產生的組合語言如下
可以注意到 bsr
這個指令,根據 BSR — Bit Scan Reverse 這裡所提供的資訊可知,它將在第二個引數中搜索 MSB 設置為 1 的數字,並且把他的索引 (第幾個位元為 1 ) 的資訊紀錄在第一個引數中,不難發現這個操作就像是跟 __builtin_clz(x)
一樣。
因此,可以判斷在 X86-64 的組合語言中 bsr
與 GCC 的內建函數 __builtin_clz
的功能是一樣的。
2
可以從 if it is power of 2, we increase the bit length
這段敘述得知 DDDD
的條件判斷應該是判斷是否為 2 的冪。要檢驗是否為 2 的冪的方式很簡單,只需要使用 i & (i - 1)
這樣的 位元操作就可以確定。
再來是要將 ans 進行偏移,而方法就是看當前的數字的二進制有幾個位元就向左偏移幾個,所以 EEEE
會是 ans << len
。
__builtin_{clz,ctz,ffs}
改寫TODO
改進 mod 的運算
3
由例子來講解程式碼,假如輸入 char *buf = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH", len = 32
, 可以知道 end = qword + 8。接下來進行 for-loop ,來計算到底有幾個 1。接著由老師在題目中的敘述
若輸入的字串是一個有效的 UTF-8 序列,則計算其中的後續位元組數量,並將此數字從總字串長度中減去,即可確定字元數量。
可以知道 (1 << 3) 後在乘 (len / 8) 就會是字串原本的長度,其實也就是 len ,因此,這裡的 AAAA 就是要填 3
,但是也有可能 len 的長度不是 8 的倍數的情況(例如 35 ),這時候若是套用的公式來看的話,就可以看到會有剩下的,所以就要考慮那些剩下的,因此 BBBB
就是要填入 7
,利用 &
來進行比對,若是有剩下的話,就會再進行一次 count_utf8((const char *) end, len & CCCC)
,因此 CCCC
這邊也是要填入 7
,最後如果沒有剩餘的數字的話,就 +=0
,也就是 DDDD
所要填入的。
這邊測試效能的方式會是使用 perf 來進行,而測試的資料為隨機產生的字串,在藉由兩種函式進行比較
接著把它繪製成 gnuplot
可以看到使用 SWAR 的執行時間確實有比沒有使用的快,但不確定為何進步沒有很大
由於上面的測試似乎沒有表現出 SWAR 的強大,所以用另外一種方式來檢測效能,使用了 lab0 中的 cpucycle()來進行檢測
以 100000 筆資料來進行比較,可以發現使用 SWAR 確實效能提升了不少
接著我測試從 10000 筆資料到 100000 筆資料,並使用 gnuplot 繪製成折線圖
可以看到確實 SWAR 確實使用了更少的 cpucycles 數
TODO
改進效能
4
可以看到上面程式碼中的 0x8000
轉換成二進制可以表示成 0b 1000 0000 0000 0000
,接著檢查 x & 0x8000
是否為 1 。這段可以把它視為要找的數字為從 MSB 開始連續的 1 ,例如 0b 1111 1100 0000 0000
這類的數字。
也可以從符合樣式的資料及來觀察
接著就把它用不同的方式實作出來
上面的程式碼也是用二進制會比較好觀察
可以看到只要 x 從 MSB 往 LSB 方向中間的連續 1 有中斷的話,就會讓 -x 也就是 n 在斷掉的位子產生 1,如此一來只要 進行 ^
運算,就會使得 n ^ x
變得比 x 還要大,所以就會回傳 false 。