contributed by < nelsonlai1
>
這題要實作在任何環境下可運作的 ars(arithmetic right shift),ars 會在右移後,依照原本首位的 bit 補回值,也就是正數補 0,負數補 1。
一開始的 logical 判斷環境是不是 logical right shift (一律補 0),所以對 -1 右移 1(OP1 = >> 1
),如果是 lrs,會變成 0b011…111 > 0 = true = 1,反之則是 0b111…111 < 0 = false = 0。
fixu 用來判斷被除數 m 是不是負數(OP2 = m < 0
),當 logical = 1 以及 m < 0 時,fixu = -(1 & 1) = 0b111…111,其他時候fixu = 0b000…000。fix 則是將 fixu(unsigned int) 的值存成 int。
最後 return 的結果,如果在 logical = 1 以及 m < 0 都成立時,fix ^ (fix >> n) 會是前 n 個 bits 為 1,其餘為 0。所以 | (fix ^ (fix >> n)) 就可以把 lrs 吃掉的 1 補回來。
example : m = 0b10110101, n = 4, system = logical
這題要輸入的數字是否為 4 的次方,4 的二進位表示是 100,也就是說 4 的次方一定是 1 後面接著偶數個 0 (每次乘四都是 << 2)。
一開始 num > 0
判斷是否為正整數,(num & (num - 1)) == 0
判斷是否為 2 的次方
( & ) ,所以最後這項 !(__builtin_ctz(num) OPQ
可以知道是在判斷後面的 0 的個數是否為偶數了。而任一偶數 & 1 = 0 (偶數最後一項 bit 為 0),所以 OPQ = & 0x1
。
& 0x55555555 可以一次判斷是否為正數以及 trailing 0-bits 是否為偶數(在 num 是 2 的次方條件下)
利用右移會無條件捨去的特性,先將輸入值左移到最左邊後做 not,再右移一樣的次數(__builtin_clz(N)
),就能得到結果。
First Missing Positive :
用 bucket sort 的概念,將 1, 2, 3… 換到 nums[0], nums[1], nums[2]…。最後再從 nums[0] 開始找起,第一個不符合的數就是答案,如果都符合的話答案為 numsSize + 1 (代表從 1 到 numsSize 都各出現一次)。
值得一提的是,如果直接寫 nums[nums[i] - 1],會出現 ERROR: AddressSanitizer: heap-buffer-overflow
錯誤,推測是因為數據中有小於 1 的數,即使不會執行到這行,系統也會判斷操作到錯誤的 array 位置 (< 0)。
這題計算一個數經過多少次計算可以變為零,而計算只有分兩種 : >> 1 (當前是偶數)以及 - 1 (當前是奇數)。
int 寬度為 32 bits,因此將最左邊的 bit 移到最右邊要 31 次 >> 1,每有一個 1 都需要做一次 - 1,而起始 1 的位置每往右一個 (leading 0-bits 多一)就能少做一次 >> 1。
所以 AAA = __builtin_popcount(num)
, BBB = __builtin_clz(num)
因為 __builtin_clz(0)
不被允許,所以加上 | 1
讓即使在 num == 0
時也能返回正確值,同時也不會影響其他情況的結果(因為是最後一位)。
popcount 的寫法參照測驗 3 題目敘述的 popcnt_branchless,而 clz 寫法前面五行可以保證 MSB 後的 bits 皆為 1。最後再用 32(total bits) 減去 popcount 就是 leading 0-bits 數量。
(做法參考 The Aggregate Magic Algorithms)
64-bit GCD (greatest common divisor, 最大公因數) 求值函式
改寫為以下等價實作
上面的 function 在實作輾轉相除法,一開始先判斷如果 u, v 有任一數不存在,就回傳存在的那個數。之後 while 迴圈裡在做的是將較大的一方 u 對較小的一方 v 取餘數(即使一開始 u 比較小,過完一輪後就會跟 v 互換值),將 v 改為餘數值(小),將 u 改為原本的 v (大),算是比較直觀的寫法。
下面的做法可以搭配 Binary GCD 來思考。第一個 for 迴圈的內容就是這句的應用
binary gcd 的精神就是當兩數為偶數時,必有一 2 因子。
而 shift 用來紀錄有幾個 2 因子,所以最後 return 的結果應該要 << shift () 補回來。
之後分別檢查 u, v 是否偶數則是對應這句
且一數為奇數另一數為偶數,則無 2 因子。
所以透過不斷除以二,直到兩者都是奇數為止。
最後就是做一般的輾轉相除法,只是這次不使用 % 運算子,所以就是比較兩數大小,然後大減小,變得比較像是 Wikipedia 的展示動畫,而我們也可以發現 v 都是被減數,所以XXX = v
。
而 return 結果則是依照 ,把 乘回來,所以 YYY = u << shift
。
第五行使用 Bit Twiddling Hacks 的方法得到兩數中較小的數。
利用 perf 檢查兩者效能差距 :
由於單一次呼叫 function 不足以有效看出差距,所以使用 for 迴圈來多次呼叫,且因為主要改善在
u, v 皆為偶數時,故只測 u, v 皆為偶數的情況。
原始 :
使用 __builtin_ctz 改寫 :
可以發現在速度上以及 branches 有大約 30% 的改善。若是 u, v 有很大的 2^N 公因數時,效能差距可以更為顯著。
在影像處理中,bit array (也稱 bitset) 廣泛使用,考慮以下程式碼:
可用 clz 改寫為效率更高且等價的程式碼:
一個 bitmap 長得像這樣,共有 64 * bitmapsize 個 bits。
for 迴圈裡做的是找出那些 bits 為 1 (if ((bitset >> i) & 0x1)
),pos 紀錄 1 的個數,out 紀錄 1 的位置(out[pos++] = p + i
)。
0 | 1 | 2 | … | bitmapsize - 1 | |
---|---|---|---|---|---|
0 | 0 | 1 | 0 | … | 1 |
1 | 0 | 0 | 1 | … | 0 |
2 | 1 | 1 | 1 | … | 1 |
… | … | … | … | … | … |
63 | 0 | 1 | 0 | … | 1 |
下方的 code 改寫了後面尋找 1 的部分,從原本逐個 bit 測試,改成直接計算有幾個 trailing 0-bits,再把最後的 1 改成 0,進入下一輪。
而可以做到的 t 是 bitset & -bitset
,因為 -bitset = ~bitset + 1 (2's complement),所以 t 會是 bitset 最後一個 1 的位置為 1,而其餘皆是 0 (獨立 LSB)。而 bitset ^= t
就能把最後一個 1 改成 0 了。
在這篇文章中有提到使用 SIMD 指令改寫的方法,在 bitmap 密集時,有明顯的效能增長。