---
tags: NCKU Linux Kernel Internals, C語言
---
# C 語言:數值系統
[你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics#%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84-C-%E8%AA%9E%E8%A8%80%EF%BC%9A%E6%95%B8%E5%80%BC%E7%B3%BB%E7%B5%B1%E7%AF%87)
## bit-wise operator
觀察與學習不同bit-wise operator的神奇操作,以後在閱讀kernel code時就可以保持對程式的敏感度(~~希望啦~~)
### 取絕對值的常數實作
如果要實作一個計算絕對值的函式
```c=
#include <stdint.h>
int32_t abs(int32_t x){
if(x>0)
return x;
else
return -x;
}
```
顯而易見的設計,但程式中產生了branch。
思考一下
$$
abs(x) =
\begin{cases}
x & \text{if } x \geq 0 \newline
-x & \text{if } x < 0
\end{cases}
$$
等價於
$$
abs(x) =
\begin{cases}
x & \text{if } x \geq 0 \newline
(\sim x) + 1 & \text{if } x < 0
\end{cases}
$$
我們可以通過~x (反轉全部位元)即是對 0xFFFFFFFF 做 XOR 操作的特性來改寫。
簡單考慮到32位的signed int時,向右位移採用[算數位移](https://en.wikipedia.org/wiki/Arithmetic_shift)。則:
* 若輸入值是正整數,那麼:
* A = 輸入值 >> 31 得到 0x00000000 => 0
* B = -A 得到 -0x00000000 => 0
* 若輸入值是負整數,那麼:
* A = 輸入值 >> 31 得到 0xFFFFFFFF => -1
* B = -A 得到 0x00000001 => 1
因此就可以將abs改寫成
``` c=
#include <stdint.h>
int32_t abs(int32_t x) {
int32_t mask = (x >> 31);
return (x + mask) ^ mask;
}
```
### 取最小值的常數操作
如何設計一個得到可以最小值的,沒有分支的函式?
```c=
int32_t min(int32_t a, int32_t b) {
int32_t diff = (a - b);
return b + (diff & (diff >> 31));
}
```
思考上面的式子,我們可以把a-b分成三種情況:
* a - b = 0
* (0 & (0 >> 31))
* return b
* a - b > 0:
* diff最高位為0,因此右移31bit = 0
* (diff & 0) = 0
* return b
* a - b < 0:
* diff最高位為1,因此右移31bit = 0xFFFFFFFF(注意是算術位移)
* (diff & 0xFFFFFFFF) = diff = (a - b)
* return b + (a - b) = a
更多細節請閱讀: [解讀計算機編碼](https://hackmd.io/vOjbtew3Tn2aA0uDrmUL4Q?view#%E8%A7%A3%E8%AE%80%E8%A8%88%E7%AE%97%E6%A9%9F%E7%B7%A8%E7%A2%BC)
### `x & (x - 1) == 0`
[X = X & X-1 Trick](http://sevensavants.blogspot.com/2010/01/x-x-x-1-trick.html)
* 判斷x是不是2的N次方(無號數)
* 如果一個數字是2的N次方,那麼其二進位表達的最高位為1,其餘位為0。
* 換個角度思考: 讓某個值少 1,就是讓那個值對最右邊的1產生借位。`x & (x - 1)`實際上是在**把二進位表示中最靠右邊位的1變成0**,而2的N次方**最右邊的1 = 最高位的1**
``` c=
int foo(unsigned int x)
{
int count = 0;
while(x)
{
count++;
x = x&(x-1);
}
return count;
}
```
如果你理解的話,應該可以知道這個函式的功用就是**計算x的二進位表示式中包含1的數量**(把最x右邊的1移除直到x=0)。
### ASCII table

ASCII table的安排大有學問,你可以發現 'A' 和 'a' 相差剛好32。~~巧合嗎?我可不這麼認為~~
* 大A到大Z: 0b1<font color=red>0</font>0 0001 到 0b1<font color=red>0</font>1 1010
* 小a到小z: 0b1<font color=red>1</font>0 0001 到 0b1<font color=red>1</font>1 1010
因此將字元轉成小寫
``` c=
('a' | ' ') // 得到 'a'
('A' | ' ') // 得到 'a'
```
將字元轉為大寫:
``` c=
('a' & '_') // 得到 'A'
('A' & '_') // 得到 'A'
```
### [XOR Swap](https://en.wikipedia.org/wiki/XOR_swap_algorithm)
用3個XOR,不需要額外的空間交換數值。

* XOR的同等運算: `x ^ y == (~x & y) | (x & ~y)`
### 避免 overflow
* 計算`(x + y) / 2`,如果x + y的時候產生overflow,就會出錯了
```
(x & y) + ((x ^ y) >> 1)
```
* 用加法器來思考: x & y 是進位, x ^ y 是位元和。
* 則 `x + y = x ^ y + ( x & y ) << 1`
* `(x + y) / 2 `
= `(x + y) >> 1 `
= `(x ^ y + ((x & y) << 1))>>1`
= `((x ^ y) >> 1) + (x & y)`
### Detect NULL
``` 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
```
這個macro的功用是偵測是否為0(是否為 NULL char ’\0’)。(詳細請看老師的教材!相當有趣XD)
## Count Leading Zero
Couting leading zero會被使用在甚麼地方?
如果我們想要對一個用 n 位元表示的數字計算 log2 (取整),只要找到 "最左邊有幾個 0 " 就可以了,用 n-1 減掉即可。為了方便了解,我們以 8 bits 為例,擴展到 32 / 64 bits 則同理:
* 4 = 0b00000100 = 7 - 5 = 2
* 7 = 0b00000111 = 7 - 5 = 2
把32位元的無號數直觀的寫成程式就是:
``` c=
for (bits = 32; bits > 0; --bits) {
if (n & 0x80000000) break;
n <<= 1;
}
return bits;
```
然而,我們可以透過一些技巧減少運算的時間。
* iteration: 每個iteration都把 x "折成一半",如果上半部有 1,接下來就找上半部即可;反之,只要找下半部就好。
```c=
int n = 32, c = 16;
do {
uint32_t y = x >> c;
if (y) { n -= c; x = y; }
c >>= 1;
} while (c);
return (n - x);
```
* binary search technique:
```c=
int clz(uint32_t x) {
if (x == 0) return 32;
int n = 0;
if (x <= 0x0000FFFF) { n += 16; x <<= 16; }
if (x <= 0x00FFFFFF) { n += 8; x <<= 8; }
if (x <= 0x0FFFFFFF) { n += 4; x <<= 4; }
if (x <= 0x3FFFFFFF) { n += 2; x <<= 2; }
if (x <= 0x7FFFFFFF) { n += 1; x <<= 1; }
return n;
}
```
* byte shift:
``` c=
int clz(uint32_t x) {
if (x == 0) return 32;
int n = 1;
if ((x >> 16) == 0) { n += 16; x <<= 16; }
if ((x >> 24) == 0) { n += 8; x <<= 8; }
if ((x >> 28) == 0) { n += 4; x <<= 4; }
if ((x >> 30) == 0) { n += 2; x <<= 2; }
n = n - (x >> 31);
return n;
}
```
還有更複雜的如[De Bruijn32](https://en.wikipedia.org/wiki/De_Bruijn_sequence)的作法。請直接參閱課程。
## Round Up / Down Power-of-2
> [2021q1 第 2 週測驗題: 測驗二](https://hackmd.io/@sysprog/linux2021-quiz2#%E6%B8%AC%E9%A9%97-2)
下面所展示的 `rounddown_pow_of_two` 可以接受 16 位元的無號整數 `N`,並回傳小於(且最接近)或等於 `N` 的 power-of-2,為甚麼呢?
```cpp
uint16_t rounddown_pow_of_two(uint16_t N) {
/* change all right side bits to 1 */
N |= N >> 1;
N |= N >> 2;
N |= N >> 4;
N |= N >> 8;
return (N + 1) >> 1;
}
```
試想一下我們可以怎麼做到 `rounddown_pow_of_two` 要求的行為,首先,這個問題其實相當於在找最高位的 1 在哪,所以我們其實可以用 [gcc builtin](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 的 `__builtin_clz` 來實作:
```cpp
uint16_t rounddown_pow_of_two2(uint16_t N){
// the input of clz is unsigned int
if (N == 0)
return 0;
return 1 << (31 - __builtin_clz(N));
}
```
:::info
1. N == 0 預期的行為沒定義,可以先忽略
2. 因為 `__builtin_clz` 接受的型態是 unsigned int,所以這裡用 31 去減
:::
換個想法的話,假設最高位的 1 在第 k 位,那麼我們可以先把最高位的 1 往後的位元都設成 1,然後再將結果加 1 後右移 1 位,也可以得到想求得的答案。例如 $21_{10}$ = $10101_{2}$,先把往後的位元都設成 1,也就是 $11111_2$ ,然後再計算 $(11111_{2} + 1) >> 1 = 16_{10}$。
此時你應該發現了,後面的計算其實就對應 `(N + 1) >> 1`,因此前面的一系列 `|` 和 `>>` 所做就是把後面的位元填成 1。為甚麼這些操作可以把最高位的 1 往後的位元都填成 1 呢? 首先我們可以先想得簡單一點,因為 `|` 右移後的數值必定不會改動到最高位元的 1 往前的位元,所以最直接的實作是:
```cpp
uint16_t rounddown_pow_of_two0(uint16_t N) {
/* change all right side bits to 1 */
N |= N >> 1;
N |= N >> 2;
N |= N >> 3;
...
N |= N >> 14;
N |= N >> 15;
return (N + 1) >> 1;
}
```
但是事實上不需要執行那麼多次,思考最極端的狀況 $1000000000000000_2$
* `N |= N >> 1` 變成 $1100000000000000_2$
* `N |= N >> 2` 變成 $1111000000000000_2$
* `N |= N >> 4` 變成 $1111111100000000_2$
* `N |= N >> 8` 變成 $1111111111111111_2$
可以看到每一次的 `|` 和 `>>` 操作都可以增加最多 2 倍的 1,因此最極端有 15 個零需要填成 1 的情況下也只要 4 次操作,增加 $2^4=16$ 倍的 1 就可以將最高位元以後的所有位元填成 1。
在此之外,思考一下上面的例子中 `N` 的適用(可以得到正確結果)範圍為何呢? 以 `uint16_t` 的版本為例,假如輸入 N = 0x80,最後 return 時的 $1111111111111111_2$ + 1 已經超過 `uint16_t` 可表示範圍,但最終卻可得到正確得結果 0x80。與之相對的,在 `uint32_t` 的版本:
```cpp
uint32_t rounddown_pow_of_two(uint32_t N) {
/* change all right side bits to 1 */
N |= N >> 1;
N |= N >> 2;
N |= N >> 4;
N |= N >> 8;
N |= N >> 16;
return (N + 1) >> 1;
}
```
如果輸入 N = 0x80000000,最後 return 時卻無法得到正確的結果 0x80000000,造成這個情形的原因是甚麼呢?
答案其實就藏在 [C 語言規格書](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf?fbclid=IwAR2k5K3ZPS4CzQ9AbVS96QD-5N2UQyE23Ui4ic270JX5Df3pEXJkg1cBDHA) 中。根據 6.3.1.1 Boolean, characters, and integers 的第 2 點:
> if an int can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions.
因此,`uin16_t` 型態的 N 在 expression 中會先被 promote 成 `int` 型態(假設 [data model](https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models) 中 `int` 為 32 bits),計算後再轉回 16 bits 型態回傳。與此相對的,`uint32_t` 沒有被隱性的轉換到更高的 bits 表示,因此 `(N + 1)` 產生 overflow,得到非預期的結果。
## Linux 中的 bitwise 操作
下面,嘗試研讀幾個 Linux 系統中重要的 bitwise 操作技巧。
### log2
在 [include/linux/log2.h](https://github.com/torvalds/linux/blob/master/include/linux/log2.h) 可見到與 log2 相關的 bitwise 操作。
#### `is_power_of_two`
```cpp
/**
* is_power_of_2() - check if a value is a power of two
* @n: the value to check
*
* Determine whether some value is a power of two, where zero is
* *not* considered a power of two.
* Return: true if @n is a power of 2, otherwise false.
*/
static inline __attribute__((const))
bool is_power_of_2(unsigned long n)
{
return (n != 0 && ((n & (n - 1)) == 0));
}
```
恰如其名,這個函式可以用來判別 `n` 是否為 $2^k$(k 為整數),而其實作技巧事實上就是前面曾經提到的 `x & (x - 1)`。
* [`__attribute__((const))`](https://www.keil.com/support/man/docs/armcc/armcc_chr1359124974244.htm) 提示此函數對於相同數值的輸入可以得到同樣的結果,且也不影響 global 的任何記憶體,使得編譯器可以進行必要的優化。
#### `__rounddown_pow_of_two` / `__roundup_pow_of_two()`
```cpp
/**
* __rounddown_pow_of_two() - round down to nearest power of two
* @n: value to round down
*/
static inline __attribute__((const))
unsigned long __rounddown_pow_of_two(unsigned long n)
{
return 1UL << (fls_long(n) - 1);
}
```
`__rounddown_pow_of_two()` 的作用如前所述,但是實作的思維上不同。`fls_long` 定義在 [linux/include/linux/bitops.h](https://github.com/torvalds/linux/blob/master/include/linux/bitops.h) 中
```cpp
static inline unsigned fls_long(unsigned long l)
{
if (sizeof(l) == 4)
return fls(l);
return fls64(l);
}
```
`fls` 意指 find last set bit,根據硬體的不同可能有不同的實作方式。 不過想法近似於此前我們曾用 `__builtin_clz` 實作的版本,找到最高位的 1 之位置 `i`(注意位置是位置的範圍是 1 ~ 64),則所求就是 $2^{i-1}$
```cpp
/**
* __roundup_pow_of_two() - round up to nearest power of two
* @n: value to round up
*/
static inline __attribute__((const))
unsigned long __roundup_pow_of_two(unsigned long n)
{
return 1UL << fls_long(n - 1);
}
```
與 `__rounddown_pow_of_two()` 相對,找到大於等於(且最接近)的 power of two
## TODO
- [ ] 閱讀 [CS:APP 第 2 章重點提示和練習](https://hackmd.io/@sysprog/CSAPP-ch2#%E6%95%B8%E5%80%BC%E7%B3%BB%E7%B5%B1)並透過練習題深入學習