--- tags: 你所不知道的 C 語言, 進階電腦系統理論與實作, NCKU Linux Kernel Internals, 作業系統 --- # 數值系統、Bitwise 操作、浮點數運算、定點數操作 contributed by <`RusselCK` > ###### tags: `RusselCK` ## [數值系統](https://hackmd.io/@sysprog/binary-representation) * [解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation) #### 1. Bitwise XOR (`^`) | bit a | bit b | bitwise `^` | | ------ | ------ | ----------- | |0 |0 |0 | |1 |0 |1 | |0 |1 |1 | |1 |1 |0 | 注意看最後兩列,若要讓每個位元都是 `0` -> `1`, `1` -> `0` 的話,選用的 bitmask (可翻譯為「位元遮罩」,聽起來很文言,但很好理解,即想對某個數值做位元操作,才會得到預期的結果) 就該是 111...1 (在有效的位元空間中全部填入 `1`),後者在二補數系統中,就是 `-1`。換言之,對 `-1` 做 XOR 操作可達到位元反轉。 `~x` (反轉全部位元) 可用「對 `0xFFFFFFFF` 做 XOR 操作,會變成和原來相反的位元」的特性來改寫,亦即可用 `(x ^ 0xFFFFFFFF)` 代替。 #### 2. 針對 32 位元整數實作絕對值: ```cpp #include <stdint.h> int32_t abs(int32_t x) { int32_t mask = (x >> 31); return (x + mask) ^ mask; } ``` 該作法的原理: 1. 若 `x >= 0`,產生的 `mask` 是 `0x00000000`,做 XOR 之後根本不會變,做完的結果再扣掉 `mask` 也不會變; 2. 反之,若 `x < 0`,產生的 `mask` 就會是 `0xFFFFFFFF`,也就是二補數的 `-1`。上面的作法剛好就是把 `x` 扣掉 1 之後再反轉所有位元; ```cpp int32_t abs(int32_t x) { int32_t mask = (x >> 31); return (x ^ mask) - mask; } ``` 當 $x \geq 0$ 時,`mask` 為 `0`,於是回傳值就是 `x`。那麼,當$x \lt 0$ 時,上面的結果會不會給出 $-x$ 呢?後者也就是 $x$ 的加法反元素、對應的二補數表示法。回想二補數系統「`x`, `y` 互為反元素的充要條件是 $x + y = 0x00000000$」,亦即某數和其反元素的關係: $$ \mathtt{\bar x + x = 0x00000000} $$ 裡頭的 $\bar x$。 回到上面程式,當 `x < 0` 時,存有: $$ \mathtt{abs(x) = (\sim x) + 1} $$ 驗證是否滿足反元素的定義: $$ \mathtt{abs(x) + x = (\sim x) + x + 1} $$ 因為 `~x + x` 是 `0xFFFFFFFF`,再加上 `1` 即成為 `0x00000000`,因此結果是 `0x00000000`,從而證明 $abs(x)$ 確實是 $x$ 的反元素。由此可知在 $x < 0$ 時,程式碼的輸出即是 `-x`。 #### 3.給定兩個有號整數,找出其中較小的數值: ```cpp int32_t min(int32_t a, int32_t b) { int32_t diff = (a - b); return b + (diff & (diff >> 31)); } ``` 注意:這實作僅在 `INT32_MIN <= (a - b) <= INT32_MAX` 成立時,才會發揮作用。 再者考慮以下程式碼: ```cpp int32_t a, b, c, x; ... if (a > b) x += c; ``` 當 `INT32_MIN <= (b - a) <= INT32_MAX` 條件成立時,我們可改寫 `if (a > b) x += c;` 為以下程式碼: ```cpp x += ((b - a) >> 31) & c; ``` 儘管現代的中央處理器具備分支預測器 ([branch predictor](https://en.wikipedia.org/wiki/Branch_predictor)),在 `a` 和 `b` 之間變化難以預測時,上述的 branchless 手法可消弭因分支預測錯誤引發的效能損失。 #### 參考資料 * [Optimized abs function](https://www.strchr.com/optimized_abs_function) * [Optimized ABS function in Assembly](https://helloacm.com/optimized-abs-function-in-assembly/) * [How to get absolute value of a 32-bit signed integer as fast as possible?](https://community.arm.com/developer/ip-products/processors/f/cortex-m-forum/6636/how-to-get-absolute-value-of-a-32-bit-signed-integer-as-fast-as-possible) * [Bit Twiddling Hacks](https://graphics.stanford.edu/~seander/bithacks.html) 解析: [(一)](https://hackmd.io/@0xff07/ORAORAORAORA), [(二)](https://hackmd.io/@0xff07/MUDAMUDAMUDA), [(三)](https://hackmd.io/@0xff07/WRYYYYYYYYYY) * [理解矩陣](https://blog.csdn.net/myan/article/details/647511) --- #### ASCII with Bitwise Operator * 將字元轉成小寫: 免除使用分支 ```cpp ('a' | ' ') // 得到 'a' ('A' | ' ') // 得到 'a' ``` * 將字元轉為大寫: 免除使用分支 ```cpp ('a' & '_') // 得到 'A' ('A' & '_') // 得到 'A' ``` * 大小寫互轉: 避免使用分支 ```cpp ('a' ^ ' ') // 得到 'A' ('A' ^ ' ') // 得到 'a' ``` #### 4. [XOR swap](https://en.wikipedia.org/wiki/XOR_swap_algorithm) 1. naive swap ```clike void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; } ``` :::warning * 額外用到 `t`,不為 in-place,如何避免? ::: 2. 搬移差距 swap ```clike void swap(int *a, int *b) { *a = *a - *b; /* 兩數相差 */ *b = *a + *b; /* 相加得出 *b */ *a = *b - *a; /* 相減得出 *a */ } ``` :::danger * 可能會造成 Overflow ::: 3. XOR swap * 交換兩個記憶體空間內的數值,可完全不用額外的記憶體來實作 * 不只 `int` ,任何型態的數字都可用 ```cpp void xorSwap(int *x, int *y) { *x ^= *y; *y ^= *x; *x ^= *y; } ``` #### 5. 避免 overflow * 比方說 `(x + y) / 2` 這樣的運算,有個致命問題在於 (x + y) 可能會導致 overflow (考慮到 x 和 y 都接近 [UINT32_MAX](https://msdn.microsoft.com/en-us/library/mt764276.aspx),亦即 32-bit 表示範圍的上限之際) * [Binary search 的演算法提出之後十年才被驗證](https://www.comp.nus.edu.sg/~sma5503/recitations/01-crct.pdf) * 於是我們可以改寫為以下: ```cpp (x & y) + ((x ^ y) >> 1) ``` * 用加法器來思考: `x & y` 是進位, `x ^ y` 是位元和, `/ 2` 是向右移一位 * 位元相加不進位的總和: `x ^ y`; 位元相加產生的進位值: `(x & y) << 1` * `x + y = x ^ y + ( x & y ) << 1` * 所以 (x + y) / 2 = `(x + y) >> 1` = `(x ^ y + (x & y) << 1) >> 1` = `(x & y) + ((x ^ y) >> 1)` #### 6. [strlen](http://opensource.apple.com/source/Libc/Libc-583/arm/string/strlen.s) 偵測是否為 0 或者說是否為 NULL char ’\0’ * 我們先思考 strlen() 該怎麼實做,以下實作一個簡單的版本 ```C unsigned int strlen(const char *s) { char *p = s; while (*p != ’\0’) p++; return (p - s); } ``` 這樣的版本有什麼問題?雖然看起來精簡,但是因為他一次只檢查 1byte,所以一旦字串很長,他就會處理很久。另外一個問題是,假設是在 32-bit 的 CPU 上,一次是處理 4-byte (32-bit) 大小的資訊,不覺得這樣很浪費嗎? * C 語言程式的 DETECT NULL 巨集 ```cpp #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 ``` 為了可以思考這樣的程式,我們由已知的計算方式來逆推原作者可能的思考流程,首先先將計算再簡化一點點,將他從 **(((X) - 0x01010101) & ~(X) & 0x80808080)** 變成 ``` ((X) - 0x01) & ~(X) & 0x80 ``` 還是看不懂,將以前學過的笛摩根定理套用上去,於是這個式子就變成了 ``` ~( ~(X - 0x01) | X ) & 0x80  ``` 再稍微調整一下順序 ``` ~( X | ~(X - 0x01) ) & 0x80  ``` 所以我們就可進行分析 * `X | ~(X - 0x01)` => 取得最低位元是否為 0 ,並將其他位元設為 1 * X = 0000 0011 => 1111 1111 * X = 0000 0010 => 1111 1110 * 想想 0x80 是什麼? 0x80 是 1000 0000 ,也就是 1-byte 的最高位元 上面這兩組組合起來,我們可以得到以下結果 * X = 0    => 1000 0000  => 0x80 * X = 1     => 0000 0000 => 0 * X = 2    => 0000 0000 => 0 * ....... * X = 255 => 0000 0000 => 0 於是我們知道,原來這樣的運算,如果一個 byte 是 0,那經由這個運算得到的結果會是 0x80,反之為 0。 再將這個想法擴展到 32-bit,是不是可以想到說在 32bit 的情況下,0 會得到 0x80808080 這樣的答案?我們只要判斷這個數值是不是存在,就可以找到 ’\0’ 在哪了! #### 7. Count Leading Zero 當我們計算 log~2~ (以2為底的對數) 時, 其實只要算高位有幾個 0's bits. 再用 31 減掉即可。 > 舉例 in 8 bits: $x$ =`00011111` 高位有3個bits -> 最大的1出現在第(8-3=5)位 -> log~2~x 約等於 5 * 一般寫法 ```cpp int BITS = 31; for (; i < 32; --BITS) { if (N & 0x80000000) break; N <<= 1; } ``` * 類似 De Bruijn 演算法 (32-bit version) ```cpp const int tab32[32] = { 0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30, 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31 }; int log2_32 (uint32_t value) { value |= value >> 1; value |= value >> 2; value |= value >> 4; value |= value >> 8; value |= value >> 16; return tab32[(uint32_t)(value*0x07C4ACDD) >> 27]; } ``` * gcc 提供 built-in Function: * [int __builtin_clz (unsigned int x)](http://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) * Returns the number of leading 0-bits in x, starting at the most significant bit position. * If x is 0, the result is undefined. * 可用來實做 log2: ```cpp #define LOG2(X) ((unsigned) \ (8 * sizeof (unsigned long long) - __builtin_clzll((X)) - 1)) ``` * 實做 clz * binary search technique ```cpp 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; } ``` --- ## ==[ Bitwise 操作](https://hackmd.io/@sysprog/c-bitwise)== #### 1. Set a bit :::info where n is the bit number, and 0 is the least significant bit ::: ```cpp unsigned char a |= (1 << n); ``` Example: ``` a 1 0 0 0 0 0 0 0 a |= (1 << 1) = 1 0 0 0 0 0 1 0 a |= (1 << 3) = 1 0 0 0 1 0 0 0 a |= (1 << 5) = 1 0 1 0 0 0 0 0 ``` #### 2. Clear a bit: ```cpp unsigned char b &= ~(1 << n); ``` - [ ] Example 1: ``` b 1 1 1 1 1 1 1 1 b &= ~(1 << 1) = 1 1 1 1 1 1 0 1 b &= ~(1 << 3) = 1 1 1 1 0 1 1 1 b &= ~(1 << 5) = 1 1 0 1 1 1 1 1 ``` * `~` :反向 * Test a bit: ```cpp unsigned char e = d & (1 << n); //d has the byte value. ``` #### 3. Toggle a bit: ```cpp unsigned char c ^= (1 << n); ``` Example: ``` c 1 0 0 1 1 0 1 1 c ^= (1 << 1) = 1 0 0 1 1 0 0 1 c ^= (1 << 3) = 1 0 0 1 0 0 1 1 c ^= (1 << 5) = 1 0 1 1 1 0 1 1 ``` * The Magic of XOR ``` x ^ y == (~x & y) | (x & ~y) ``` #### 4. The right/left most byte assuming 16 bit, 2-byte short integer: ```cpp unsigned char right = val & 0xff; /* right most (least significant) byte */ unsigned char left = (val >> 8) & 0xff; /* left most (most significant) byte */ ``` * 要不要考慮 Little endian or Big endian ? #### 5. sign bit assuming 16 bit, 2-byte short integer, two's complement: ```cpp bool sign = val & 0x8000; // sign bit ``` --- #### bitwise 練習題 * [Bitwise Operators in C](http://www2.mta.ac.il/~hbinsky/c%20content/Bits.pdf) * Uses of Bitwise Operations or Why to Study Bits * [C Bitwise Operators Quiz](http://doc.callmematthi.eu/C_Bitwise_Operators.html) * [線上測驗](https://pconrad.github.io/old_pconrad_cs16/topics/bitOps/) (附上解答) * [Bitwise Practice](https://web.stanford.edu/class/archive/cs/cs107/cs107.1202/lab1/practice.html) * [2018q1 考古題](https://hackmd.io/s/rJh9U4Guf) / [參考解答](https://hackmd.io/s/S1f0tSyTG) * [2018q3 考古題](https://hackmd.io/s/S1a9-YO_Q) / [參考解答](https://hackmd.io/s/By5KCaZam) * [2019q1 考古題1](https://hackmd.io/@sysprog/SyrZMGYr4) / [參考解答](https://hackmd.io/@chenishi/S1DHQ3KrE) / [延伸閱讀](https://medium.com/fcamels-notes/%E4%BD%BF%E7%94%A8%E4%BD%8D%E5%85%83%E8%A8%88%E7%AE%97%E5%8A%A0%E9%80%9F%E6%B1%82%E8%A7%A3-n-queen-d20442f34110) * [2019q1 考古題2](https://hackmd.io/@sysprog/H1Pik8M8E) / [參考解答](https://hackmd.io/@1IzBzEXXRsmj6-nLXZ9opw/B1vrcKcu4) * [2020q1 考古題](https://hackmd.io/@sysprog/linux2020-quiz3) / [參考解答](https://hackmd.io/@Hsuhaoxiang/2020q3) --- ## [浮點數運算與定點數操作](https://hackmd.io/@NwaynhhKTK6joWHmoUxc7Q/H1SVhbETQ) #### 浮點數表示 ![](https://i.imgur.com/F5VKeeI.png) ![](https://i.imgur.com/z5fEKkv.png) #### 案例: 浮點數乘以二 * 推導:欲使給定的浮點數乘以 `2`,必須考慮到 3 種情況 1. Normalize 模式的話,必須將指數加一後放回。 2. Denormalize 模式的話,比較有趣,可以直接將原數左移一單位, * Mantissa 的 overflow 會自動變成 exp 的 1,之後再將 sign bit 放回即可。 4. 遇到 NaN, INF,就直接 return ```clike /* Return bit-level equivalent of expression 2 * uf * for floating point uf */ unsigned float_twice (unsigned uf) { unsigned uf_sgn = (uf >> 31) & 0x01; //擷取首位正負號 unsigned uf_exp = (uf >> 23) & 0xff; //擷取八位指數位 if (uf_exp == 0) { //檢查為 denormalized 模式 uf <<= 1; // 將數值左移一位也就是乘以二 uf += (uf_sgn << 31); //將 sign bit 放回原數 } else if (uf_exp != 0xff) { //檢查不是 INF or NaN (也就是剩下來的 Normalized 模式) uf_exp += 1; //讓 exp + 1 來乘以二 /* 更新 uf */ uf &= 0x807fffff; //把 exp 的部分遮蔽掉 uf = uf + (uf_exp << 23); //把處理完的 exp 放回去 } return uf; } ``` * 數值範圍檢查 1. case NORMALIZE: NORMALIZE 下能表示的數字為 ±(1.F) \* 2 ^ (E - 127) 其中 E 範圍在 1 ~ 254 之間 以最大值 E = 254 為例,經過 `float_twice` 之後 EXP 部份變成 254 + 1 = 255 也就是 infinity 或者是 NaN 的表示法 :::warning 理論上這邊結果不應該是 NaN 而應該是 infinity 不過要怎麼確認 mantissa 部份在這個時候會變成零 是不是要補個條件判斷或 mask > [name=chenishi] ```clike /* Return bit-level equivalent of expression 2 * uf * for floating point uf */ unsigned float_twice(unsigned uf) { unsigned uf_sgn = (uf >> 31) & 0x01; unsigned uf_exp = (uf >> 23) & 0xff; if (uf_exp == 0) { uf <<= 1; uf += (uf_sgn << 31); } else if (uf_exp != 0xff) { uf_exp += 1; /* 如果從 normalize 加到 denormalized */ if (uf_exp == 0xff) { uf &= 0x80000000;// 歸零 mantissa /* 正常從 normalize 到 normalize */ } else { uf &= 0x807fffff;// 留下 mantissa } uf = uf + (uf_exp << 23); } return uf; } ``` ::: 2. case DENORMALIZE: DENORMALIZE 下條件為 EXP 部份全部為零,如果 `float_twice` 前,mantissa 為 0b1......(也就是首位為 1) > 舉 0-00000000-1111111111111111111111 為例(denormalize 情況下的最大數) > E = 1 - Bias = -126 > M = 2^23 - 1 / 2^23 `float_twice` 之後 > 0-00000001-1111111111111111111110 > E = e - Bias = -126 > M = (2^23 - 1 / 2^23) * 2 (可以看成原本的 Mantissa 左移一位) 這正好符合 CS:APP 中關於 NORMALIZE/ DENORMALIZE 中 EXP 的解釋 (FLOAT 等級距的性質) 3. case NaN/ infinity: 直接回傳不影響 #### 案例: 浮點數的絕對值 * 我們知道,浮點數的正負號是由最前面的 MSB 控制的 * 所以想要取得絕對值,只要**把 $x$ 的 MSB 設為 0**。 ```clike #include<stdio.h> #include<stdlib.h> void absf(double *x) { int a = 0x1; char little_end = *((char *) &a); *(((int *) x) + little_end) &= 0x7fffffff; } int main() { double x = -1; printf("%f \n", x); absf(&x); printf("%f \n", x); } ``` > double : 8 bytes > int : 4 bytes > char : 1 bytes * `a` : `0x00 00 00 01` * 當此機器為 Little Endian 時,`char little_end`會變為`0x1`, * 而 Big Endian 時,`char little_end` 會得到`0x0`。 * 用意: * 在 Big Endian 之下,將 `*x & 0x7fffffff` 即為所求 * 在 Little Endian 之下,將 `*(x+1) & 0x7fffffff` 才是所求 ```graphviz digraph{ address[shape = record label = "{*x}|{*(x+1)}"] } ``` #### Big / Small Endian ![](https://i.imgur.com/MW8UOkh.png) ![](https://i.imgur.com/l0mLNDl.png) * 現今 little endian 架構在筆電,桌機上盛行 * big endian 也被稱作 Network Byte Order * [Big Endian 與他們的現狀](https://hackmd.io/s/BJRaOrGCX) #### [C 語言強制轉型 (Casting)](https://) #### 要怎麼知道電腦是 Little/Big - endian ? 雖然現在主流的系統絕大多數都是 little-endian,不過還是有辦法偵測 endian 如果是在 Linux 系統,可以使用 shell script 判斷 ```bash echo -n I | od -to2 | head -n1 | cut -f2 -d" " | cut -c6 ``` 如果是 little endian 則應該會回傳 1,如果是 big endian 則應該會回傳 0 #### 什麼是定點數? 定點數與浮點數的差別最大在於「十進制中小數點位置是否固定」 像我們一般用的 int 整數就是「定點數」的一種,其小數點固定在最右方 這樣的表示方法用在小數上,代表我們只能夠表示固定大小的小數位 舉例來說,假設我們定點數小數點固定放在右三到右四之間,那麼 > 10 / 3 = 3.333 如果是左五到左四之間 > 10 / 3 = 3.3333 也就是說,隨著我們設計小數點位置不同,定點數能表示的範圍以及數值精度都會受到影響