contributed by <Shiritai
>
將以下程式:
改為位元運算風格如下,應填入何?
「填補」的説明提示為
然而上述程式碼存在分支,於是我們可考慮建構以下「填補」位元表示中的 1:
可見往左每次補左移 1, 2, 4, 8, … 皆為二的冪,以此我們重新整理題目的程式碼,並加上適當的空行註解:
答案呼之欲出:
AAAA
為 8
,BBBB
為 32
,CCCC
為 x + 1
。
本程式碼透過以 1
填滿最高位 1
位元以降的所有位元,來獲得大於原值之二的冪減一,最後加一即獲得所求。
不過實際上本算法有個問題:當原本 x 即為二的冪時反而求得比其大的二的冪…
__builtin_clzl
改寫有 count leading zero,尋找目標二的冪變的更加簡單:取得 leading zero 中最高位的 0
的位移量,產生一個以零為基礎,將該為設為 1
後的結果:
極其簡單,不過如我於作業三的開發紀錄中 Count leading zero 實作一章所述,__builtin_clzl
有未定義行為,可能需要使用 branching 排除例外:
前面提到過 x 本身即二的冪的可能性,這可以透過 __builtin_ctzl
來判斷:當 lead + tail + 1
為 64
時即表示 x
為二的冪:
此時不仿利用邏輯與位元運算離規避上述 branching:
注意到利用邏輯運算子之 short circuit (短路求值) 的特性,第 12 行故意使用 ||
使當前者若非零就短路求值,這導致 res
停留在捕獲若 x
為二的冪時的結果,否則捕獲後者的結果。||
中後者的說明如下:
如此便可實現看似無 branching 版的 next_pow2
。
前述我們討論了兩種以 __builtin_clzl
為基礎的實現,差別在一個使用純 branching,,另一個極端的使用邏輯與位元運算規避 branching。一個重要的問題是:後面這樣折騰,是否真的比較高效?看看產生的指令也許可以略知一二:
以下所有組語使用 cc -O2 -std=c99 -S next_pow2.c
編譯。
前幾行求 lead
和 tail
皆相同,出現 __builtin_clzl
對應之組語 bsrq
和 __builtin_ctzl
對應之 bsfq
:
為了比較極端使用非分支與極端使用分支,我額外寫一個分支判斷二的冪後即不使用分支的中間版本作為比較…
可以發現全無分支版的竟然出現 jne
(jump if not equals) 指令,不過對於它的出現也不應該感到意外,這就是 short circuit 的實作。考慮到條件轉跳於預測錯誤時 CPU flush 的性能損耗,分支版有兩條件轉跳,其餘兩者皆為單條件轉跳,單轉跳的可能表現比較好。不過極端版組合語言明顯長非常多。綜合來看,道取中庸可能是比較好的選擇,其對應的 C 語言如下:
串接 至 間所有二進制數的值。
考慮判斷去除最低位的 1
後是否為零的邏輯,便是一個使用 __builtin_ctz
的好時機:將 tailing zero 位加一之位元去除,故 DDDD
為 x & ~(1 << __builtin_ctz(x))
。由於暫時不確定是針對哪個變數操作,故先以 x
代替。
之後我們看到第 18 行需要一個與 ans
於之前迴圈舊的值位移後有關的運算結果,推斷 DDDD
的邏輯應該是為了複製貼上 i
的位元所做的準備,故 DDDD
的變數應該與 i
有關,也就是 i
。這樣思考的話 EEEE
的答案也呼之欲出:即將 ans
位移 len
。
故答案為:
DDDD
: i & ~(1 << __builtin_ctz(i))
EEEE
: ans << len
由於直覺想到使用 __builtin_ctz
,與延伸不謀而合,於是我去觀賞其他學員們的答案,發現許多人 DDDD
都使用 i & (i - 1)
作答,其所代表的意義為直接判斷是否為二的冪,也十分精妙。
當初確診時看這題題目敘述就昏頭,現在看一次就懂了,可惡 qq
以 SWAR 實作計算 UTF8 字數的函式。
當中出現的 count_utf8
為以單字元為基礎掃描的版本:
首先觀察 swar_count_utf8
中初始化的兩指標,qword
表四重 words (2 bytes),也就是 位元,而 end
指向 qword
遍歷結束的位址,除以 (右移 ) 處理了 char *
至uint64_t *
位移量的型別轉換。
遍歷完迴圈後,可以預期 count
已經紀錄 continuation bytes 數,以總位元組數減去之後便是真正的 UTF8 字數。考量如此,(1 << AAAA) * (len / 8)
應該為總位元組數,也就是假設 len
整除於 ( bits) 情況下的總位元組數:整數除 後乘 == 1 << 3
,故 AAAA = 3
。
但 buf
未必能對齊於 uint64_t
,需額外計算未對其的部分。由此推斷 BBBB
為判斷對齊與否的邏輯:len & 0b111
,故 BBBB = 0b111 = 7
,同時判斷 DDDD
即 0
,表已經對齊不需補加。
至於 CCCC
,由於希望 count_utf8
幫我們處理未對齊的部分,故應該略去已經對齊的量,透過 & 0b111
即可。
故答案為:
AAAA = 3
BBBB = 7
CCCC = 7
DDDD = 0
由於單次尋找量為非 SWAR 的八倍,但單次迴圈內的計算量增加,故推測效能會有顯著而小於八倍的成長。
TODO: 更具體的效能比較…
Shiritai
以上述函式確認 bits 無號數是否符合以下樣式:
改由以下函式更加有效:
眼尖便能發現特定樣式的為最高位必是一個以上(含)連續的一。如果不使用題目的框架,我可能會這樣寫:
不過題目框架已經給好了,這還真腦筋急轉不過來…只好參考其他學員們的答案 (EEEE = ~x + 1
, FFFF = x
),但還是覺得沒有學到什麼,就不額外說明了。