contributed by < tinyynoob
>
為了對 8 bit 數字進行翻轉,這裡的作法以類似 top-down 的方式往下處理,每次交換左右半部,直到每個半部的 size 都為 1 即完成所有翻轉,圖解如下:
因為 0xcc
= 11001100
,所以第 5 行需要補上的就是另一半的操作,因此 EXP2
為 (x & 0x33) << 2
。
同理 EXP1
為 (x & 0x55) << 1
。
可以看出第 4 至 15 行在把 dvd
跟 dvs
都轉成非負的值,並把 sign 值存放到 signal
。另外也可以看出缺乏對 divisor 為 0 的檢查。
這裡由於是 int 轉成 unsigned int ,所以取二補數並不會因 INT_MIN 導致 undefined behaviour ,以 4 bit 舉例說明:
因此第 8 及 14 行會得出正確的結果。
接著後面就是要想辦法用 shift 和 subtract 來做出除法。因為除法是從最高位開始,所以這裡的第 17~19 行即是要推出最高位並以 shift
紀錄,大概會想到兩個可能的答案
dvs << shift
dvs << shift + 1
有例子會比較容易推測,所以我們先假定 dvd
= 16 和 dvs
= 3 ,則相除結果應為 0101 。除了第 26 行未知以外,唯一在更新答案的就是第 25 行,其功能為將特定位元設為 1 ,因此可以推斷第一次執行到第 25 行時 shift
應該要為 2 。而因為第 23 ~ 24 行結束後會保證 dvd >= (dvs << shift)
,所以第 17~19 行把 shift
起始值稍微設超過也沒關係,因此 EXP6
可以選成較為簡潔的 dvs << shift
。
在繼續除之前,我們需要更新 dvd
,所以 EXP7
就是 dvd -= (dvs << shift)
,得:
接著繼續想,會發現 23~24 行幫助我們找到哪一位可以繼續除,全部做完就會像這樣:
不過因為我們是用 unsigned int 在除,而商的回傳型態是 int ,所以前面所提及的 INT_MIN 還是可能造成問題。一樣以 4-bit 為例 , 1000 除以 1 得 1000 ,但因為 1000 原本代表的是負數所以 signal
會被標記為 -1 ,那麼到第 31 行就會發生 1000 * (-1) 的狀況
abs(INT_MIN)
是未定義行為,那 (INT_MIN) * (-1)
呢?
測試似乎會得到原值
不理解 28, 29 行的功能
我發現它沒有對 divisor
為 0 的檢查,結果會使程式陷入無窮迴圈。
使用一些 corner cases 測試可以發現:
在 INT_MIN / -1
的情境中,也因為結果出現 32 位有號數無法表示的 導致答案不如預期。
為了了解在慣例上這兩種情況該如何處置,我去測試了 standard library 中的 div()
。
呼叫 div(7, 0)
以及 div(INT_MIN, -1)
皆得到相同結果:
看來它們都是直接用 exception 的方式處理,查詢 man signal
找到這段敘述:
Integer division by zero has undefined result. On some architectures it will generate a SIGFPE signal. (Also dividing the most negative integer by -1 may generate SIGFPE.) Ignoring this signal might lead to an endless loop.
我想仿造使用相同的方式,因此在 divide()
函式的最前方加入:
測試後 signal 可被正確地觸發。
我認為這裡 while 迴圈判斷 shift
的方式,可以使用 clz()
來取而代之,程式碼更動如下:
嘗試測量 10000 組隨機數進行除法所耗費的時間,得:
結果顯示改進版程式碼的耗費時間竟為原版的 倍,與我的預期截然不同。
working on…
給定 32 位的 unsigned v
,這題想計算出需要幾個 bit 才能表示 v
,顯然 ret
,且 ret
是由 v
的 most significant set bit 所決定。
根據二進制的原理,我們知道任意 v
必定能表示為
值最大且為 1 的 就是我們要的 ret
,這裡想透過類似二分搜尋的方式來進行查找。
首先把 32 個 bit 分為兩半,數字代表 most significant set bit 在由下往上的第幾位。
v
至少要是
才能落在左邊,因此當 v
時 v
屬於左半,否則 v
屬於右半。
在屬於左半的情況若是我們先把數字 16 存到某個變數,那麼左右兩個 branch 又變成一樣的問題,那就是求「最大的 1 在 16 位中的何處?」,於是繼續:
完成後一樣把結果加到 m
並繼續 4 位的搜尋。
在表示上,我們將前面的 v
更改為等價的 v
以利程式看起來的對稱性和二分關係。
在每一輪中我們藉由比較和左移把結果暫存到 m
,並用 |
來立起 ret
中對應的 bit ,接著右移 v
來讓兩種結果回歸同一個新問題。
繼續二分的結果, EXP10
顯然為 (v > 0x3U) << 1
。
最後第 16 行的 EXP11
就比較特別了,在程式走到這裡的時候僅剩下 2 個 bit ,所以要判斷是否 ,我們可以列出 v
為 00
, 01
, 10
, 11
的情況來思考,結果應該要分別對應到 0, 1, 2, 2。
然而我們目前不能產生兩位數 (已於 13 至 15 行佔用第 2 位),此外仔細觀察可以發現第 1 位也早在第 3 行就已經被佔用,因此 0
和 1
的 case 其實已經判斷完畢。
那麼 EXP11
就應該以加法更新,並只在最後是 10
或 11
時更新,即為 ret += (v > 1)
。
雖然目前 architecture 上大都有提供 fls 或相似的 instruction,但為了 cross-platform 和 cross-compiler 的情境,我們有時仍需要 software 的 fls 處理。上方的 ilog32()
程式碼仍可能有 branch 產生,因此我們想找到更好的 branch-free 版本。
考慮 -bit 無號整數,此篇文章以 、求 ffs 為例。
首先,我們需要一組長度為 的 de Bruijn sequence,這類特殊 binary sequence 所有 長度的 cyclic subsequence 完全不重複,也就是每個 長度的整數都會剛好出現一次。
例如這裡給定為 debru
= 00011101
,它所有長 的 cyclic subsequence 為:000
, 001
, 011
, 111
, 110
, 101
, 010
, 100
(由左而右),我們可以建立 look-up table 以快速查詢位置:
subsequence | index |
---|---|
000 |
0 |
001 |
1 |
010 |
6 |
011 |
2 |
100 |
7 |
101 |
5 |
110 |
4 |
111 |
3 |
接著要如何實現 ffs 呢?
我們先針對輸入值 weight 為 1 的 case,做法如下:
x
乘以 debru
>> 5
& 7
以 x
= 00010000
為例,首先因為 x
的 weight 為 1,將其乘以 debru
等同於對 debru
左移 ffs(x)
步驟 2 和 3 固定取出 @@@
位置的 bits
可以發現這些操作就等同取出 debru
的第 ffs(x) 個 cyclic subsequence,於是反查就可以輕易找到 ffs(x)。
以上僅適用於 weight 為 1 的 x
,那要如何擴充到任意 x
呢?只要先對 x
使用 isolate first set 操作就行了,一個經典的方法是 x = x & (-x)
。
需要注意到我們這裡的 ffs 並未考量 x
= 0
解釋至此, fls() 也可以用相似的技巧配合 isolate last set 進行快速求解。
我們取 stackoverflow 討論串 裡的 debru
,並嘗試自己撰寫出 fls() 。
以 32-bit 為目標,首先撰寫輔助程式建出 look-up table
於是可以寫出:
ils()
function
依據之前 branch 版本的慣例,我將 MSB 視為 1、LSB 為 32,並補上 fls(0) = 0。
簡單用亂數進行測試,將 fls32()
的結果與 __builtin_clz() + 1
比對,完全沒出現錯誤回報,唯兩者對 0 會產生不同結果。
ilog2()
實作此題想在整數除法中選擇較接近的整數作為答案,而非原本的無條件捨去。
先考慮正數的例子,從數學上來思考,假設除法結果落在 @
會得到 1,而落在 #
會得 2,從數線看起來如:
假設除出來位在
如果我們能夠把它右移 0.5 ,那麼 x
就會落入傳統 @
的範圍並被電腦映射到 1。
然而在整數中根本沒有 0.5 的概念,而追究 0.5 的來源即是整數除法中無法整除的部份,因此我們應想辦法去調整被除數。
假設正整數 在電腦上做除法得 ,我們希望在算出 之後可以將它右移 ,那麼可以將原算式改造成 達成目的, 在電腦上可以使用 >> 1
。
接著我們需要將負整數除法也納入討論,在本機使用 gcc 編譯測試顯示 /
運算的結果會對稱於原點 0 :
注意負數部份,例如 -4/3
得 -1 而非 -2 。
嘗試搜尋 C99 規格書,看到除法運算寫在 6.5.5 章節,但沒有查到對於商或餘數選擇的敘述
由於這種對稱性,因此前述對於正數右移 0.5 的工作,到了負數情況便改成要左移 0.5 。
回頭來看題目:
因為 -1 = UINT_MAX > 0
,所以表達式 ((typeof(x)) -1) > 0
可用於判斷該變數是否為 unsigned ;而 (((__x) > 0) == ((__d) > 0)))
檢驗兩者是否同號。
因為 C 語言會將 unsigned 和 signed 混合運算的結果轉為 unsigned ,因此只要被除數或除數中有 unsigned number ,那結果必定非負。若兩者同號那相除結果也會非負。
於是乎我們可以推測 RRR
處理 的結果, SSS
處理 的結果。根據之前的討論,填入:
RRR
: __x + (__d >> 1)
SSS
: __x - (__d >> 1)
待補
這裡我跳過 fls()
的部份,直接針對開平方根的部份進行解釋,其功能等同 。
對於 滿足 ,我可以猜到 ,但是我不知道要怎麼算出更細節的結果。
我在 wiki 找到相關的說明,它的概念是利用 公式,想辦法在已知 的情況下去湊出 。
這題的變數和要填空的格子有點多,很難依可見的資訊判斷作答,我研究了好一陣子都很茫然,所以我直接去看答案。
以 為例來說明:
首先fls(x)
會算出 6 ,接著 & ~1
的作用是把最低的位元改成 0 ,也就是取出小於等於 fls(x)
的最大偶數,以我們的例子會輸出 6 。最後把 m
初始化為 ,稍候就會發現 m
變數是用來指示目前運算到第幾位。
因為 return y
所以 y
應是存開根號結果的地方。
每次它會算出 b
之後檢查 x >= b
,如果成立那 x
便可以減 b
並得該位的 y
是 1 ,否則該位 y
是 0 。
繼續 loop 之前執行第 16 行,目的是把 m
對齊下一個要運算的位。