owned this note
owned this note
Published
Linked with GitHub
# 數值系統 閱讀紀錄
> author: < `Shiritai` >
>
> [原文見此](https://hackmd.io/@sysprog/c-numerics)
>
> 以下對該文核心做摘要 + 補充。
## 平衡的三進制
> [Donald E. Knuth](https://en.wikipedia.org/wiki/Donald_Knuth) 在《[The Art of Computer Programming](https://en.wikipedia.org/wiki/The_Art_of_Computer_Programming)》第 2 卷提到:
> > 也許平衡的三進制表示法是最美麗的數值系統。
透過 `+`, `0`, `-` 表示一個平衡三進制的位元:
* `+` 表 $1\times 3^k$
* `-` 表 $-1\times 3^k$
* `0` 表 $0\times 3^k=0$
此時有一些優美的性質:
> 複習[群論相關的離散數學](https://hackmd.io/t2E724Q_R8W3V3EEW-nu8w?both#%E7%94%A8%E5%BE%97%E5%88%B0%E7%9A%84%E4%BB%A3%E6%95%B8%E7%B3%BB%E7%B5%B1%E8%88%87%E7%BE%A4%E8%AB%96%E6%A6%82%E8%BF%B0)...
* 每個位元與加法擁有以下性質
* Identity: `0`
* Inverse: `+` 與 `-` 互為反元素,即「平衡」三進制的平衡兩字
* 另 Closed, Associative, Communicative 三性質要等具體設計好有限編碼的細節才知道。
可以看出,平衡三進制與生俱來(每個位元都)擁有二進制所沒有的 Inverse 這項性質,後者透過編碼以盡可能獲得此性質,這使平衡三進制的運算變得很簡單,比如反元素 (取負數) 變為 `+`, `-` 倒轉。
## 浮點數不是[阿貝爾群](https://hackmd.io/t2E724Q_R8W3V3EEW-nu8w?both#%E7%94%A8%E5%BE%97%E5%88%B0%E7%9A%84%E4%BB%A3%E6%95%B8%E7%B3%BB%E7%B5%B1%E8%88%87%E7%BE%A4%E8%AB%96%E6%A6%82%E8%BF%B0)
考慮浮點數編碼與加法
| Feature | Yes or no |
|:-------------:|:----------------------------:|
| Closed | Y |
| Associative | N |
| Identity | N (`0`, `-0`) |
| Inverse | Y (考慮 `INF`, `NaN` 的話 N) |
| Communicative | Y |
這使編譯器最佳化的前後可能影響浮點數的運算結果。
## 整數使用不當例子
* 2002 年 FreeBSD [53]
```c=
#define KSIZE 1024
char kbuf[KSIZE];
int copy_from_kernel(void *user_dest, int maxlen) {
int len = KSIZE < maxlen ? KSIZE : maxlen;
memcpy(user_dest, kbuf, len);
return len;
}
```
為將 kernel 資料搬至 user space 的函式,在 `x86` 下若傳入值 `maxlen` 為負數,則 `len` 為負數,傳入 `memcpy` 第三參數 (被呼叫方解釋為 `size_t`) 後變一巨大的正數,如此可能將 kernel 不應該暴露的資料流出。
* 2002 年 External data representation (XDR) [62]
```c=
void *copy_elements(void *ele_src[], int ele_cnt, int ele_size) {
void *result = malloc(ele_cnt * ele_size);
if (result==NULL) return NULL;
void *next = result;
for (int i = 0; i < ele_cnt; i++) {
memcpy(next, ele_src[i], ele_size);
next += ele_size;
}
return result;
}
```
雖 `ele_cnt` 和 `ele_size` 都於整數範圍內,不過乘法可能導致溢位,進而影響 malloc 的大小不符合預期而複製 `ele_src` 到不該複製的位置。
## 位元運算的有趣應用
### 二補數中 `x & (x - 1) == 0` 的意義
> 透過去除「最低位 `1`」判斷是否為二的冪。
* 定義 `x`
$$
x := \{X...X\}_{part2}1\{0...0\}_{part1}
$$
* `x - 1` 後各個位元變成
$$
\rightarrow x - 1 = \{X...X\}_{part2}0\{1...1\}_{part1}
$$
其中 $\{\}$ 內兩部分的位元數前後相同。
* 則 `x & (x - 1)` 為 $\{X...X\}_{part2}0\{0...0\}_{part1}$,與 `x` 相比,表去除最低位的 $1$。
* 故 `x & (x - 1) == 0` 為真時,表示 $\{X...X\}_{part2}0\{0...0\}_{part1}$ 為零,也就是高位都為零。換言之,可用來判斷 $x$ 是否為二的冪。
### 位元反轉
```c
unsigned char reverse(unsigned char n) {
n = (n & 0xF0) >> 4 | (n & 0x0F) << 4;
n = (n & 0xCC) >> 2 | (n & 0x33) << 2;
n = (n & 0xAA) >> 1 | (n & 0x55) << 1;
return n;
}
```
實作原理十分精妙,讓我來分析其中原委。
要完成位元反轉,一種做法是對 $i \forall i \in [0, 3]$ 位位元,將其位移至 $7-i$ 位,對高 $4$ 位同理。
也就是我們需要建立一個映射,使
$$
i \rightarrow 7 - i, \forall i \in [0, 7]
$$
對於 2 進制,我們可以看得更仔細:
```c
(before: i) 000 001 010 011 100 101 110 111
(after: 7-i) 111 110 101 100 011 010 001 000
```
映射結果即原本位置二進制的 bitwise flip。實作上,對於 $i$ 位置位元,為零時向 MSB 位移相應的距離,為一時向 LSB。從組成 $7$ 之二進制數 $4$ 開始,位置 $4$ 為零者向 MSB,位置 $4$ 為一者向 LSB。如果用 bit mask 來區別向 MSB 或向 LSB 的位元們,區別向 MSB 的 mask 應該長得像:
$$
0b00001111 = \tt{0x0F}
$$
同時區別向 LSB 的 mask 應該長得像:
$$
0b11110000 = \tt{0xF0}
$$
其他位置 ($2$, $1$) 同理,如此便解釋了前述玄妙的實作原理。
### ASCII 的編碼細節: 快速大小寫轉換
:::spoiler 用一小程式
```c
void print_128bin(int a) {
for (int i = 0; i < 8; ++i)
printf("%d", (a >> (7 - i)) & 1);
}
void print_ascii() {
for (int j = 0; j < 32; ++j) {
for (int i = 0; i < 4; ++i) {
int res = i * 32 + j;
print_128bin(res);
printf(" [%c]\t", (char) (isprint(res) ? res : '~'));
}
printf("\n");
}
}
```
:::
:::spoiler 生成下表,`'~'` 表非列印值。
```txt
00000000 [~] 00100000 [ ] 01000000 [@] 01100000 [`]
00000001 [~] 00100001 [!] 01000001 [A] 01100001 [a]
00000010 [~] 00100010 ["] 01000010 [B] 01100010 [b]
00000011 [~] 00100011 [#] 01000011 [C] 01100011 [c]
00000100 [~] 00100100 [$] 01000100 [D] 01100100 [d]
00000101 [~] 00100101 [%] 01000101 [E] 01100101 [e]
00000110 [~] 00100110 [&] 01000110 [F] 01100110 [f]
00000111 [~] 00100111 ['] 01000111 [G] 01100111 [g]
00001000 [~] 00101000 [(] 01001000 [H] 01101000 [h]
00001001 [~] 00101001 [)] 01001001 [I] 01101001 [i]
00001010 [~] 00101010 [*] 01001010 [J] 01101010 [j]
00001011 [~] 00101011 [+] 01001011 [K] 01101011 [k]
00001100 [~] 00101100 [,] 01001100 [L] 01101100 [l]
00001101 [~] 00101101 [-] 01001101 [M] 01101101 [m]
00001110 [~] 00101110 [.] 01001110 [N] 01101110 [n]
00001111 [~] 00101111 [/] 01001111 [O] 01101111 [o]
00010000 [~] 00110000 [0] 01010000 [P] 01110000 [p]
00010001 [~] 00110001 [1] 01010001 [Q] 01110001 [q]
00010010 [~] 00110010 [2] 01010010 [R] 01110010 [r]
00010011 [~] 00110011 [3] 01010011 [S] 01110011 [s]
00010100 [~] 00110100 [4] 01010100 [T] 01110100 [t]
00010101 [~] 00110101 [5] 01010101 [U] 01110101 [u]
00010110 [~] 00110110 [6] 01010110 [V] 01110110 [v]
00010111 [~] 00110111 [7] 01010111 [W] 01110111 [w]
00011000 [~] 00111000 [8] 01011000 [X] 01111000 [x]
00011001 [~] 00111001 [9] 01011001 [Y] 01111001 [y]
00011010 [~] 00111010 [:] 01011010 [Z] 01111010 [z]
00011011 [~] 00111011 [;] 01011011 [[] 01111011 [{]
00011100 [~] 00111100 [<] 01011100 [\] 01111100 [|]
00011101 [~] 00111101 [=] 01011101 []] 01111101 [}]
00011110 [~] 00111110 [>] 01011110 [^] 01111110 [~]
00011111 [~] 00111111 [?] 01011111 [_] 01111111 [~]
```
:::
可觀察到大小寫之差在第五位的有無,故大小寫轉換可以透過
* 添加第五位: 將大寫轉小寫 (原本即小寫者不變)
> `' '`: 純第五位
```c
('a' | ' ') // 得到 'a'
('A' | ' ') // 得到 'a'
```
* 刪去第五位: 將小寫轉大寫 (原本即大寫者不變)
> `'_'`: 沒有第五位
```c
('a' & '_') // 得到 'A'
('A' & '_') // 得到 'A'
```
* 反轉第五位: 大小寫調換
```c
('a' ^ ' ') // 得到 'A'
('A' ^ ' ') // 得到 'a'
```
### 以 XOR 調換數字
> 我第一次接觸到此做法是在[劉教授的用片語學 C 語言程式設計](https://sites.google.com/view/c-programming-2ed/home)
[該書中](https://www.youtube.com/watch?v=3bCrd4soChg&list=PLOvZ8aEg7xDmRtTbeWHGKAkXI8ZkcyAhe&index=132)提到這種 Ninja Code 的存在:
```c
a ^= b ^= a ^= b;
// X= 從右邊結合
a ^= (b ^= (a ^= b));
```
以上成立是因為 XOR 的交換率,對位元 $a_n, b_n$ 而言,以上操作即
$$
a_n' = a_n \oplus b_n
$$
$$
b_n' = b_n \oplus a_n' = b_n \oplus a_n \oplus b_n \\
= a_n \oplus b_n \oplus b_n = a_n \oplus 0 = a_n
$$
$$
a_n'' = a_n' \oplus b_n' = (a_n \oplus b_n) \oplus (a_n) = b_n \oplus 0 = b_n
$$
故所有位元都得到交換,且全程 in-place,即不另外使用暫存器或記憶體。
> 另外,劉教授提到不應為了炫技而寫出不易閱讀的程式碼,不過我認為若其在實務上有價值,且透過正確地理解原理以及家事適當的註解,亦可活用該程式碼。
### 相加除二: 避免溢位
```c
(x & y) + ((x ^ y) >> 1)
```
`x & y` 為「進位」,`x ^ y` 為「位元和」,將後者左移一位,前者留在原地,正是將加法結果除以二的表現。
### `0` 的偵測
[原文](https://hackmd.io/@sysprog/c-numerics#%E9%81%8B%E7%94%A8-bit-wise-operator)以 top-down 的方式解釋 [`DETECT`](https://github.com/eblot/newlib/blob/master/newlib/libc/string/strlen.c) 巨集的行為。
```c
#if LONG_MAX == 2147483647L
#define DETECT(X) \
(((X) - 0x01010101) & ~(X) & 0x80808080)
#else
#if LONG_MAX == 9223372036854775807L
#define DETECT(X) \
(((X) - 0x0101010101010101) & ~(X) & 0x8080808080808080)
#else
#error long int is not a 32bit or 64bit type.
#endif
#endif
```
我想以 buttom-up 的方式作補充 。
假設對於 $1\ \rm{byte}$ 而言,我們希望確認其是否為零,可以怎麼做?
還記得前面提到我們有[確認數字是否為二的冪](https://hackmd.io/y_jVeGzDQhetNS6A-XxPmA?both#unsigned-%E4%B8%AD-x-amp-x---1--0-%E7%9A%84%E6%84%8F%E7%BE%A9)的邏輯嗎?當中利用 $x$ 和 $x-1$ 的關係進行位元運算來消除最低位的 `1`。
複習一下這兩數的外型 (1 byte):
* 定義 $x$
$$
x := \{X...X\}_{part2}1\{0...0\}_{part1}
$$
其中**第一部分的長度可為 0~8**。
* $x - 1$ 後各個位元變成
$$
\rightarrow x - 1 = \{X...X\}_{part2}0\{1...1\}_{part1}
$$
注意若 $x$ 為零,則實際上 $x$ 只有第一部分而已,此時 $x-1$ 為全一。若 $x$ 非零,則 $x-1$ 至少有一個位元為 $0$ ,緊跟在第一部分之上。不仿想辦法取出這個有標示性意義的 $0$,假設我們最後希望該零存在時為真,不存在時為假...
* 所有 $x$ 一定有第一部分(長度自 $0$ 至 $8$),設法忽略掉。
* 第二部分不重要,設法忽略掉。想忽略掉相同的東西,常見兩方法
* 取 xor (`x ^ (x - 1)`): 在此副作用是使第一部分和標示性位元全變成 $1$,混淆視聽。
* 取 not 後 and: 正好在消除前面的同時,把 $x$ 的標示性位元留下,有兩種取法
* `(x - 1) & ~x`: $\{0...0\}_{part2}0\{1...1\}_{part1}$
* $x = 0 \rightarrow 11111111$,僅此時最高位為 $1$
* $x \ne 0 \rightarrow 0...01...1$,最高位以降至少有一個 $0$
* `(x - 1) & ~x & 0x80` 為真,表示 $x=0$,符合需求 :)
* `~(x - 1) & x`: $\{0...0\}_{part2}1\{0...0\}_{part1}$
* $x = 0 \rightarrow 00000000$
* $x \ne 0 \rightarrow 0...010...0$,某一位為一,只是不知道是哪位
* `(~(x - 1) & x) == 0` 為真,表示 $x=0$,符合需求 :)
問題來了,這倆哪個比較好?就要看架構了。
根據上方的成果,如果應用在類似 SIMD 的場景,比如將八個字元以 64 bits 一次讀入運算,比如[原文給的例子](http://opensource.apple.com/source/Libc/Libc-583/arm/string/strlen.s),以及[舊版 glibc 實作 strlen](https://github.com/lattera/glibc/blob/master/string/strlen.c),希望高效的尋找 ``'\0'`` 的位置,就可以活用此技巧。
> 以上 `glibc` 的程式碼為舊版,[新版](https://github.com/bminor/glibc/blob/master/string/strlen.c)程式碼使用巨集技巧讓程式碼變得更加優雅。
## 位元細緻操作
> [`NachOS`](https://en.wikipedia.org/wiki/Not_Another_Completely_Heuristic_Operating_System) 中的 [`bitmap.c`](https://github.com/Shiritai/NTHU-Operating-System-Eroiko_Blue/blob/master/MP4/NachOS-4.0_MP4/code/lib/bitmap.cc) 應該是不錯的例子。順帶一提,過去實作作業系統作業時,我曾對其部分操作的複雜度進行優化。
### Set a bit
```c=
void Bitmap::Mark(int which)
{
map[which / BitsInWord] |= 1 << (which % BitsInWord);
}
```
### Clear a bit
```c=
void Bitmap::Clear(int which)
{
map[which / BitsInWord] &= ~(1 << (which % BitsInWord));
}
```
### Flip a bit
```c=
void Bitmap::Flip(int which)
{
map[which / BitsInWord] ^= 1 << (which % BitsInWord);
}
```
### Test a bit
```c=
bool Bitmap::Test(int which) const
{
return !!map[which / BitsInWord] & (1 << (which % BitsInWord));
}
```