contributed by < JimmyLiu530
>
進階電腦系統理論與實作
在 C 語言中,對有號型態的物件進行算術右移(簡稱 ASR)歸屬於 unspecified behavior。考慮到程式碼的相容性,我們想實作一套可跨越平台的 ASR,假設在二補數系統,無論標的環境的 shift 究竟輸出為何,我們的 ASR 總可做到:
上述 即為 向 進位的輸出
算術右移一個正數其最左邊的 bit 必定是補 0 ,而算術右移一個負數有可能是補 0 或是補 1,而根據題目的意思,顯然是後者,因此需要知道 compiler 是補 0 還是 1。若是補 1,則符合我們的要求不需修補;否則需要修補。而這個修補會在 line 6 的| (fix ^ (fix >> n))
。
line 3 的用意就是在看此 compiler 對一個負數算術右移後會補 0 還是 1,若補 1,則右移後的結果為負 (<0),因此 logical 為 0;否則 logical 為 1。所以 OP1 = >> 1
。
接著,考慮算術右移一個負數補 0 的情況,因為 line 6 的修補項是要用來修補的,所以 (fix ^ (fix >> n))
的最高的 n 個位數一定要是 1,也就代表 fix
會是 0xffffffff
,最後可推得 OP2 = m < 0
。
此結果也符合一開始說的條件: 除了原本 compiler 右移負數補 1 不用修補之外,正數也不需要 (即只有右移補 0
且 為負數
才需要修補)。
已解問題:
int fix = *(int *) &fixu;
這樣寫的用意?
int fix = (int) fixu;
以及可以改成這樣嗎?
雖然此程式看似可互換,但這兩行程式碼的意義不太相同,前者為
interpret
,後者為cast
。看下面的例子:會得到:
也就是說前者以 整數 的角度去詮釋(interpret) 浮點數,好讓我們去對其中的某幾個位元做操作。
而後者是直接把 浮點數 強制轉型(cast)成 整數。
GCC extension 其中包含兩個函式: ctz 和 clz:
Built-in Function:
int __builtin_ctz (unsigned int x)
- Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined.
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.
我們可用 __builtin_ctz
來實作 LeetCode 342. Power of Four,假設 int 為 32 位元寬度。實作程式碼如下:
line 3 的第二個條件 (num & (num - 1))==0
其實就區分出 2 的冪次方的數了,像是 減1後,,因此接下來只要排除掉 2 的冪次方()中,n 為奇數的數即可。一樣可以透過觀察:
decimal | binary | # of trailing zero |
---|---|---|
2 | 10 | 1 |
4 = | 100 | 2 |
8 | 1000 | 3 |
16 = | 10000 | 4 |
32 | 100000 | 5 |
64 = | 1000000 | 6 |
發現 n 為奇數的數,其 trailing zero 也會是奇數個,因此 OPQ = & 0x1 。 |
將原本的 (num & (num - 1))==0
改成 __builtin_popcount(num) == 1
,兩者都可以過濾掉非 2 的冪次方的數。
又或是可以改寫成:
__builtin_popcount(num) == 1
等價於 !(__builtin_popcount(num) ^ 0x1)
,且
4 的冪次方的數其 first set bit 的位置會是奇數。
Built-in Function: int __builtin_ffs (int x)
- Returns one plus the index of the least significant 1-bit of x, or if x is zero, returns zero.
Complement of Base 10 Integer:
^ 1111...1111
32 - (N 的 leading zero)
個位元First Missing Positive:
給定一個非負整數 num
,回傳將此數依照下面的規則變成 0 需要幾步。而規則就是:
當目前的數為偶數,則將它除以 2;否則將它減 1。
我們可運用 gcc 內建函式實作,假設 int 寬度為 32 位元,程式碼列表如下:
因為在正整數中,除以 2 即為右移 1 位元,所以總共要除幾次就等於 MSB 移到最低位需要移幾次,MSB 移至最低位需 31 - __builtin_clz(num)
步,因此也需要除那麼多次。
再者,當 num
的二進位表示法中有 k 個 1 時,代表總共需要做 k 次的減 1,因為這些 1 總會被移到最低位元的位置,而最低位元為 1 代表是奇數需要減 1 ,所以共需 __builtin_popcount(num)
次減 1
因此最後答案就會是 AAA = __builtin_popcount(num)
及 BBB = __builtin_clz(num)
考慮以下 64-bit GCD (greatest common divisor, 最大公因數) 求值函式:
改寫為以下等價實作:
此改寫後的程式與最原本單純利用輾轉相除法不同的點在於,做輾轉相除法之前,先做了以下的步驟:
line 5-6 其實就是在做上述的第一點,總共提出shift
個 2。line 8-9 及 11-12就是做上述的第二點。而程式中的 do-while 其實就是輾轉相除法,只是是用減法去實現,因此 XXX = v
。最後,再將原本的結果 u 左移 shift 個 bit(即 x),所以 BBB = u << shift
。
__builtin_ctz
改寫 GCD,分析對效能的提升// TODO分析
在影像處理中,bit array (也稱 bitset) 廣泛使用,考慮以下程式碼:
可用 clz 改寫為效率更高且等價的程式碼:
第一個程式的作用在於看 bitmap
中哪些 bit 為 1,並將其位置記錄在 out
中。第一與第二個程式只有 line 8 的那個 while 的不同而已,因此聚焦在此。
透過觀察第一個程式碼,可知其脈絡為一個位元一個位元地去檢查 bitset
是否為 1,即 若要檢查第 k 個位元,需將 bitset
右移 k 位元,再與 1 比較。
而第二個程式也是 follow 這個脈絡,只是在 line 10 計算了 bitset
的 trailing zero,可以減少傻傻地去檢查為 0 的位元的時間,來提升效率。最後,line 12 需將把檢查過的位元變成 0,因此 KKK = bitset & -bitset
,這是因為 x & -x = x & (~x + 1) = 保留其 LSB,其餘設為 0
,所以 x ^ (x & -x) 會將原本 x 的 LSB 變成 0。