author: <
Shiritai
>以下對該文核心做摘要 + 補充。
Donald E. Knuth 在《The Art of Computer Programming》第 2 卷提到:
也許平衡的三進制表示法是最美麗的數值系統。
透過 +
, 0
, -
表示一個平衡三進制的位元:
+
表 -
表 0
表 此時有一些優美的性質:
複習群論相關的離散數學…
0
+
與 -
互為反元素,即「平衡」三進制的平衡兩字可以看出,平衡三進制與生俱來(每個位元都)擁有二進制所沒有的 Inverse 這項性質,後者透過編碼以盡可能獲得此性質,這使平衡三進制的運算變得很簡單,比如反元素 (取負數) 變為 +
, -
倒轉。
考慮浮點數編碼與加法
Feature | Yes or no |
---|---|
Closed | Y |
Associative | N |
Identity | N (0 , -0 ) |
Inverse | Y (考慮 INF , NaN 的話 N) |
Communicative | Y |
這使編譯器最佳化的前後可能影響浮點數的運算結果。
x86
下若傳入值 maxlen
為負數,則 len
為負數,傳入 memcpy
第三參數 (被呼叫方解釋為 size_t
) 後變一巨大的正數,如此可能將 kernel 不應該暴露的資料流出。ele_cnt
和 ele_size
都於整數範圍內,不過乘法可能導致溢位,進而影響 malloc 的大小不符合預期而複製 ele_src
到不該複製的位置。x & (x - 1) == 0
的意義透過去除「最低位
1
」判斷是否為二的冪。
定義 x
x - 1
後各個位元變成
其中 內兩部分的位元數前後相同。
則 x & (x - 1)
為 ,與 x
相比,表去除最低位的 。
故 x & (x - 1) == 0
為真時,表示 為零,也就是高位都為零。換言之,可用來判斷 是否為二的冪。
實作原理十分精妙,讓我來分析其中原委。
要完成位元反轉,一種做法是對 位位元,將其位移至 位,對高 位同理。
也就是我們需要建立一個映射,使
對於 2 進制,我們可以看得更仔細:
映射結果即原本位置二進制的 bitwise flip。實作上,對於 位置位元,為零時向 MSB 位移相應的距離,為一時向 LSB。從組成 之二進制數 開始,位置 為零者向 MSB,位置 為一者向 LSB。如果用 bit mask 來區別向 MSB 或向 LSB 的位元們,區別向 MSB 的 mask 應該長得像:
同時區別向 LSB 的 mask 應該長得像:
其他位置 (, ) 同理,如此便解釋了前述玄妙的實作原理。
'~'
表非列印值。可觀察到大小寫之差在第五位的有無,故大小寫轉換可以透過
' '
: 純第五位
'_'
: 沒有第五位
我第一次接觸到此做法是在劉教授的用片語學 C 語言程式設計
該書中提到這種 Ninja Code 的存在:
以上成立是因為 XOR 的交換率,對位元 而言,以上操作即
故所有位元都得到交換,且全程 in-place,即不另外使用暫存器或記憶體。
另外,劉教授提到不應為了炫技而寫出不易閱讀的程式碼,不過我認為若其在實務上有價值,且透過正確地理解原理以及家事適當的註解,亦可活用該程式碼。
x & y
為「進位」,x ^ y
為「位元和」,將後者左移一位,前者留在原地,正是將加法結果除以二的表現。
0
的偵測原文以 top-down 的方式解釋 DETECT
巨集的行為。
我想以 buttom-up 的方式作補充 。
假設對於 而言,我們希望確認其是否為零,可以怎麼做?
還記得前面提到我們有確認數字是否為二的冪的邏輯嗎?當中利用 和 的關係進行位元運算來消除最低位的 1
。
複習一下這兩數的外型 (1 byte):
定義
其中第一部分的長度可為 0~8。
後各個位元變成
注意若 為零,則實際上 只有第一部分而已,此時 為全一。若 非零,則 至少有一個位元為 ,緊跟在第一部分之上。不仿想辦法取出這個有標示性意義的 ,假設我們最後希望該零存在時為真,不存在時為假…
取 xor (x ^ (x - 1)
): 在此副作用是使第一部分和標示性位元全變成 ,混淆視聽。
取 not 後 and: 正好在消除前面的同時,把 的標示性位元留下,有兩種取法
(x - 1) & ~x
:
(x - 1) & ~x & 0x80
為真,表示 ,符合需求 :)~(x - 1) & x
:
(~(x - 1) & x) == 0
為真,表示 ,符合需求 :)問題來了,這倆哪個比較好?就要看架構了。
根據上方的成果,如果應用在類似 SIMD 的場景,比如將八個字元以 64 bits 一次讀入運算,比如原文給的例子,以及舊版 glibc 實作 strlen,希望高效的尋找 '\0'
的位置,就可以活用此技巧。
以上
glibc
的程式碼為舊版,新版程式碼使用巨集技巧讓程式碼變得更加優雅。
NachOS
中的bitmap.c
應該是不錯的例子。順帶一提,過去實作作業系統作業時,我曾對其部分操作的複雜度進行優化。