---
tags: linux2022
---
# 2022q1 Homework2 (quiz2)
contributed by < `YiChainLin` >
> [測驗題目](https://hackmd.io/@sysprog/linux2022-quiz2)
實驗的 gcc 編譯器版本
```shell
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
```
## 測驗1
* 兩數平均數,直觀的作法為直接對兩數相加並除以二
* 考慮相加的兩數在 32 位元無號整數,範圍: 0 ~ 4294967295 ,設定兩數分別為 3000000000
```c
#include <stdio.h>
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a + b) / 2;
}
int main(){
uint32_t a = 3000000000;
uint32_t b = 3000000000;
uint32_t sum = average(a, b);
printf("%d\n", sum);
return 0;
}
```
得出的結果為:
```
852516352
```
* 這是可預期的結果,在 `a + b` 的時候造成了 `overflow` 的結果,因此 `a + b` 的值為 `6000000000 - 4294967295 = 1705032705` ,除二後得到 `852516352` 的結果
* 避免 `overflow` 的方式
1. 若我們已知 a, b 數值的大小,可用下方程式避免 `overflow` :
透過較高的數值減去較低的數值可避免
```c
#include <stdint.h>
uint32_t average(uint32_t low, uint32_t high)
{
return low + (high - low) / 2;
}
```
2. 透過 bitwise 方式避免 `overflow` 方式
參考 [你所不知道的 C 語言: bitwise 操作](https://hackmd.io/@sysprog/c-bitwise)
* 將兩數個別透過位移運算子( Shift operator ),將二進制數值進行右移或左移
:::info
以 7~10~ = 0111~2~ 為例:
左移 : 0111~2~ << 1 = 1110~2~ = 14~10~
將位元向左移,並在右邊補0,在10進制中等價為乘2
右移 : 0111~2~ >> 1 = 0011~2~ = 3~10~
將位元向右移,並在左邊補0,在10進制中等價為除2
:::
* 因此計算兩數平均為將個別數值進行右移,但是右移的過程會將最右一個位元忽略刪除,所以要先確認兩數在最後一個位元在相加時是否有進位的可能
* 檢查方式將兩數使用 `&` 運算子確認 `a & b` 是否進位,若有進位則為 1 若無則 0
* 最後在與 1 再進行 `&` 運算,若有進位表示在移位時,進位的值不可忽略,要補 1 回去
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (EXP1);
}
```
:::success
EXP1 = a & b & 1
:::
以下使用 4 位元的數值進行平均數的舉例
a = 10~10~ = 1010~2~ , b = 6~10~ = 0110~2~
```
a >> 1 = 0101
b >> 1 = 0011
(a >> 1) + (b >> 1) + (a & b & 1) = 1000 + 0000 = 1000
二進位 二進位
0101 1010
+ 0011 0110
........ & 0001
1000 ........
0000
```
得到的結果為 1000~2~ = 8~10~
3. 透過 bitwise 方式避免 `overflow` 另一種方式
```c
uint32_t average(uint32_t a, uint32_t b)
{
return (EXP2) + ((EXP3) >> 1);
}
```
:::success
EXP2 = a & b
EXP3 = a ^ b
:::
使用 `XOR` 邏輯運算子可計算出兩數相加的結果,但不進位,所以進位的部分要透過 `AND` 運算子計算在配合右移,因此先思考兩數的相加為:
```
(a ^ b) /*相加但不進位*/
((a & b) << 1) /*計算進位*/
a + b = ((a & b) << 1) + (a ^ b)
因此兩數平均為:
(a + b) >> 1 = (((a & b) << 1) + (a ^ b)) >> 1 (分配律)
= (a & b) + ((a ^ b) >> 1)
```
以下使用 4 位元的數值進行平均數的舉例
a = 10~10~ = 1010~2~ , b = 6~10~ = 0110~2~
```
a & b = 0011
a ^ b = 1100
(a ^ b) >> 1 = 0110
(a & b) + ((a ^ b) >> 1) = 0010 + 0110 = 1000
二進位 二進位
1010 1010
& 0110 ^ 0110
........ ........
0010 1100
>>1
........
0110
```
得到的結果也為 1000~2~ = 8~10~
## 測驗2
改寫〈[解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation)〉一文的「不需要分支的設計」一節提供的程式碼 min,我們得到以下實作 (max):
相關對於 `XOR` 運算子讀物: [That XOR Trick](https://florian.github.io/xor-trick/)
```c
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((EXP4) & -(EXP5));
}
```
* 思考方式:
* 利用 `XOR` 運算子 Toggle (切換)特性
* 重複使用 `XOR` 會抵消,舉例來說, `a ^ a ^ b` (交換律)-> `b ^ a ^ a` 為 b 對 a 做了兩次的切換結果會得到 b 自己
* 觀察題目使用 a 做 `XOR` 運算得出 a 或 b
* 要得出 a : a ^ 0 = a
* 要得出 b : a ^ a ^ b = b
* 所以在 `EXP4` 中為 `a ^ b`
* `EXP5` 要能在 `a ^ b` 中得到 0 或 `a ^ b` 的結果
* 利用 1 或 0 的遮罩方式選擇( 1 的遮罩為 1111...,在十進位數值為 -1 )
* `a ^ b` & -1 = `a ^ b`
* `a ^ b` & 0 = `a ^ b`
* 而 -1 或 0 可透過判別式取得 1 或 0 的結果,因此要加上 `-` 將 1 轉 -1
:::success
如何得到 111... 的二進制數值遮罩,在看到題目的引數值為 uint_32 也就是32 位元的無號整數,因此思考到關係運算子( [relational-expression](https://en.wikipedia.org/wiki/Relational_operator),>、<...) 出來的值為是否為有號或無號的整數
參考[ISO/IEC 9899:1999](http://www.dii.uchile.cl/~daespino/files/Iso_C_1999_definition.pdf) 規格書的 6.5.8 中的第 6 點
>6. Each of the operators < (less than), > (greater than), <= (less than or equal to), and >= (greater than or equal to) shall yield 1 if the specified relation is true and 0 if it is false.) The result has type int.
指出關係運算子得出來 1 或 0 型態為 int ,為有號的整數
所以在題目中的 `a < b` 判別式中得出的 1 或 0 為有號的整數,而在 -1 的二進位表示方式為 111111............ 為 1 的遮罩方式
:::
* 程式運作
以下使用 4 位元的數值進行平均數的舉例
a = 10~10~ = 1010~2~ , b = 6~10~ = 0110~2~
```
a ^ b = 1010 ^ 0110 = 1100
若 a < b ( ~(a < b) = 1): 若 a > b( ~(a < b) = 0):
二進位 二進位
1100 1100
& 1111 & 0000
........ ........
1100 0000
分別對 a 進行 XOR 運算:
二進位 二進位
1010 1010
^ 1100 ^ 0000
........ ........
0110 <--得到 b 1010 <--得到 a
```
:::success
EXP4 = a ^ b
EXP5 = a < b
:::
* 反過來說,將也可以將題目改寫成使用 b 進行 `XOR` 運算得出較大的值
```c
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return b ^ ((a ^ b) & -(a > b));
}
```
* 延伸問題:針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
不管是在無號數或是有號數,在 `a > b` 中的敘述中就能判斷出兩數的大小,所以在此函式即能判斷出兩數的最大值,因此修改引數的宣告
```c
#include <stdint.h>
int32_t max(int32_t a, int32_t b)
{
return b ^ ((a ^ b) & -(a > b));
}
```
## 測驗3
考慮以下 64 位元 GCD ([greatest common divisor](https://en.wikipedia.org/wiki/Greatest_common_divisor), 最大公因數) 求值函式:
```c
#include <stdint.h>
uint64_t gcd64(uint64_t u, uint64_t v)
{
if (!u || !v) return u | v;
while (v) {
uint64_t t = v;
v = u % v;
u = t;
}
return u;
}
```
改寫為以下等價實作:
```c
#include <stdint.h>
uint64_t gcd64(uint64_t u, uint64_t v)
{
if (!u || !v) return u | v;
int shift;
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
while (!(u & 1))
u /= 2;
do {
while (!(v & 1))
v /= 2;
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (COND);
return RET;
}
```
:::success
COND = v
RET = u << shift
:::
根據 GCD 演算法:
1. If both x and y are 0, $gcd(0,0) = 0$
2. $gcd(x,0) = x$ and $gcd(y,0) = y$ because everything divides $0$.
以下程式碼針對上述兩個條件進行判別:
若其中數值為 0 的情況下,會透過 `OR` 運算子得到另一個非 0 數值
若兩數皆為 0 則也會返回 0 的數值
```c
if (!u || !v) return u | v;
```
3. If x and y are both even, $gcd(x,y) = 2 * gcd({x\over 2},{y\over 2})$ because $2$ is a common divisor. Multiplication with $2$ can be done with bitwise shift operator.
計算兩數擁有共同公因數 2 的數量,在最後輸出的時候,可以透過 shift 的方式簡化計算,直到兩數其中一數不為 2 的倍數(奇數)
```c
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
```
再來為非偶數的輾轉相除法( GCD )過程
1. 首先先將兩數確保為奇數(因為已經判斷過2的倍數的數量了),透過 `while` 迴圈消除
```c
while (!(u & 1))
u /= 2;
while (!(v & 1))
v /= 2;
```
* 在傳統的輾轉中,透過兩數不斷的相減取餘數相減的方式,直到其中一邊為 0 ,則另一邊的餘數為最大公因數,以圖中的示例來說最大公因數為 $10 - 8 = 2$
![](https://i.imgur.com/98sJzGJ.png)
* 程式的執行方式相似:
2. 固定 u 數為較大值(被除數),v 數為取餘數後的結果,若 v 數較大則進行調整,由 t 進算兩數取餘數的結果 $u$ mod $v$ = $t$ ,u 透過 v 回歸最大值,v 成為新的除數 t,直到餘數為 0 的時候,
* 因此當 v 為餘數 = 0的時候停止 while 迴圈,所以 `COND = v`
```c
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
```
3. 此時的 u (被除數) 即為最大公因數,但是不包含 $2$ 的倍數,因此返回值不為 u,應為 u * (因數 2 的數量),而數量由 `shift` 變數紀錄
* 因此 `RET = u << shift`
* 延伸問題:在 `x86_64` 上透過 `__builtin_ctz` 改寫 `GCD`,分析對效能的提升
* 可以得知 `__builtin_ctz` 函式用於找出最低位的 1 後面有多少 0 的個數
>Built-in Function: int __builtin_ctz (unsigned int x)
Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined. [gcc 文件說明](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)
* 舉例說明:
```c
int a = 8; /* 0100 */
__builtin_ctz(a) /* 2 ,最低位的 1 後面有兩個 0 */
```
* 運用函式可改寫在尋找 2 的因數
* 改善的部分在於:
* 不以 for 迴圈找尋兩數的 2 的因數的數量,改用 if-else 敘述找出
* 自訂兩變數在用於找出 u 、 v 兩變數 2 的因數數量,並 shift 該個數次數
* 在 do-while 迴圈中,每次皆會更新 v 可能在每一輪餘數中 2 倍數的可能性,將其維持在奇數
* 改寫為下列
```c
#include <stdint.h>
uint64_t gcd64(uint64_t u, uint64_t v)
{
if (!u || !v) return u | v;
int u_ctz = __builtin_ctz(u);
int v_ctz = __builtin_ctz(v);
int shift = u_ctz < v_ctz ? u_ctz : v_ctz;
u >>= u_ctz;
v >>= v_ctz;
do {
v >>= __builtin_ctz(v);
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (v);
return u << shift;
}
```
* 改寫後使用 [perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) 工具進行分析,分別執行 5 次
* 尚未改寫程式的運行表現
```shell
Performance counter stats for './quiz_w2_3.out' (5 runs):
5,082 cache-misses # 10.399 % of all cache refs ( +- 63.07% )
48,873 cache-references ( +- 3.38% )
819,973 instructions # 0.93 insn per cycle ( +- 0.77% )
879,293 cycles ( +- 3.48% )
0.0005822 +- 0.0000363 seconds time elapsed ( +- 6.24% )
```
* 使用 `__builtin_ctz` 改寫後的程式運行表現
```shell
Performance counter stats for './quiz_w2_3_v2.out' (5 runs):
4,217 cache-misses # 8.505 % of all cache refs ( +- 69.49% )
49,581 cache-references ( +- 1.48% )
814,529 instructions # 0.92 insn per cycle ( +- 0.42% )
885,792 cycles ( +- 3.32% )
0.0004346 +- 0.0000208 seconds time elapsed ( +- 4.78% )
```
* 測試的結果可以看到,在減少了 for 迴圈的使用下,運行的時間提升了約有 30% 的速度(可見 for 迴圈增加了不少運行時間),並且也減少了在 `cache-misses` 的發生,整體而言使用 `__builtin_ctz` 改寫後程式的運行有較好的改善
## 測驗4
在影像處理中,[bit array](https://en.wikipedia.org/wiki/Bit_array) (也稱 bitset) 廣泛使用,考慮以下程式碼:
```c
#include <stddef.h>
size_t naive(uint64_t *bitmap, size_t bitmapsize, uint32_t *out)
{
size_t pos = 0;
for (size_t k = 0; k < bitmapsize; ++k) {
uint64_t bitset = bitmap[k];
size_t p = k * 64;
for (int i = 0; i < 64; i++) {
if ((bitset >> i) & 0x1)
out[pos++] = p + i;
}
}
return pos;
}
```
用以改寫的程式碼如下:
```c=
#include <stddef.h>
size_t improved(uint64_t *bitmap, size_t bitmapsize, uint32_t *out)
{
size_t pos = 0;
uint64_t bitset;
for (size_t k = 0; k < bitmapsize; ++k) {
bitset = bitmap[k];
while (bitset != 0) {
uint64_t t = EXP6;
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
}
return pos;
}
```
>其中第 9 行的作用是找出目前最低位元的 1,並紀錄到 t 變數。舉例來說,若目前 bitset 為 000101b,那 t 就會變成 000001b,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。
:::success
EXP6 = bitset & -bitset
:::
參考這篇文章 : [Find the lowest set bit](https://stackoverflow.com/questions/12247186/find-the-lowest-set-bit)
* 本題要找出數值 bitset 中的最低位元,可透過 2 補數( [two's complement](https://en.wikipedia.org/wiki/Two%27s_complement) )的方式查找
* 以下以用 8 位元的數值做示例
bitset = 01011100~2~
```
bitset = 01011100
-bitset = 10100011 + 00000001 /* 2's complement */
=10100100
01011100 <- bitset
& 10100100 <- -bitset
...........
00000100 <- 最低位元
```
* 透過二補數與原數做 `AND` 運算,找出最低位元的數值
## 測驗5
以下是 [LeetCode 166. Fraction to Recurring Decimal](https://leetcode.com/problems/fraction-to-recurring-decimal/) 的可能實作:
```c
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "list.h"
struct rem_node {
int key;
int index;
struct list_head link;
};
static int find(struct list_head *heads, int size, int key)
{
struct rem_node *node;
int hash = key % size;
list_for_each_entry (node, &heads[hash], link) {
if (key == node->key)
return node->index;
}
return -1;
}
char *fractionToDecimal(int numerator, int denominator)
{
int size = 1024;
char *result = malloc(size);
char *p = result;
if (denominator == 0) {
result[0] = '\0';
return result;
}
if (numerator == 0) {
result[0] = '0';
result[1] = '\0';
return result;
}
/* using long long type make sure there has no integer overflow */
long long n = numerator;
long long d = denominator;
/* deal with negtive cases */
if (n < 0)
n = -n;
if (d < 0)
d = -d;
bool sign = (float) numerator / denominator >= 0;
if (!sign)
*p++ = '-';
long long remainder = n % d;
long long division = n / d;
sprintf(p, "%ld", division > 0 ? (long) division : (long) -division);
if (remainder == 0)
return result;
p = result + strlen(result);
*p++ = '.';
/* Using a map to record all of reminders and their position.
* if the reminder appeared before, which means the repeated loop begin,
*/
char *decimal = malloc(size);
memset(decimal, 0, size);
char *q = decimal;
size = 1333;
struct list_head *heads = malloc(size * sizeof(*heads));
for (int i = 0; i < size; i++)
INIT_LIST_HEAD(&heads[i]);
for (int i = 0; remainder; i++) {
int pos = find(heads, size, remainder);
if (pos >= 0) {
while (PPP > 0)
*p++ = *decimal++;
*p++ = '(';
while (*decimal != '\0')
*p++ = *decimal++;
*p++ = ')';
*p = '\0';
return result;
}
struct rem_node *node = malloc(sizeof(*node));
node->key = remainder;
node->index = i;
MMM(&node->link, EEE);
*q++ = (remainder * 10) / d + '0';
remainder = (remainder * 10) % d;
}
strcpy(p, decimal);
return result;
}
```
* 題目主要功能為浮點數的運算方法,也考慮到無限循環小數的運算
* 透過指標、 `hash table` 、 `linked list` 的實作進行
使用 Leecode 中的例子進行 trace: (預期結果 "0.(012)")
```c
int numerator = 4, int denominator = 333;
```
1. 整數的運算,主要存取 `n/d` 所產生的整數值, `4 / 333 = 0` ,透過`sprintf` 函數寫入整數值在 `p` 中,最後透過移位加入 `.` 字元(小數點)
>#include <stdio.h>
int sprintf(char * restrict s,const char * restrict format, ...);
>Description:
>The sprintf function is equivalent to fprintf, except that the output is written into
an array (specified by the argument s) rather than to a stream.
```c
char *p = result;
long long remainder = n % d;
long long division = n / d;
sprintf(p, "%ld", division > 0 ? (long) division : (long) -division);
p = result + strlen(result);
*p++ = '.';
```
2. 建立空的 `hash table` ,主要用於儲存計算的結果,與循環小數的查找
```c
size = 1333;
struct list_head *heads = malloc(size * sizeof(*heads));
for (int i = 0; i < size; i++)
INIT_LIST_HEAD(&heads[i]);
```
```graphviz
digraph graphname {
rankdir = LR;
splines = false;
node[shape = "record"]
ht[label = "Hash table|<m1>head[0]|<m2>head[1]|<m3>head[2]|...|<mn_1>head[1331-1]|<mn>head[1332]"]
}
```
3. 浮點數的運算,建立每一次小數運算值的資料 `struct rem_node *node`
* `key` 為每一輪小數除法的餘數值([模除](https://zh.wikipedia.org/wiki/%E6%A8%A1%E9%99%A4))
* `index` 為紀錄整個 `for` 迴圈的輪次
* `link` 為 `struct list_head` 結構用於資料的指標連接
```graphviz
digraph graphname {
rankdir = LR;
splines = false;
node[shape = "record"]
new_node[label = "node|key|index|link"]
}
```
* 所以範例 `4 / 333` 中的第一位小數為 `0` , `remainder` 為 `40` 在 `for` 迴圈的 `i = 0` 的輪次,也就是 `key = 40 index = 0` ,將結果加入到 `hash table` 中
```graphviz
digraph graphname {
rankdir = LR;
splines = false;
node[shape = "record"]
new_node[label = "node|key = 40|index = 0|<l>link"]
ht[label = "Hash table|<m1>head[0]|<m2>head[1]|...|<m41>head[40]|...|<mn_1>head[1331-1]|<mn>head[1332]"]
ht:m41 -> new_node:l
}
```
:::success
MMM = list_add 為加入 `hash table` 的巨集
EEE = &head[remainder % size] 加入的位置在對應模除的位置
(EEE 參考 [laneser(quiz2)同學的開發紀錄](https://hackmd.io/@laneser/linux2022-quiz2))
:::
* 最後將該輪次的商數,也就是 `(remainder * 10) / d` 加入在 `q` 中,但是 `q` 的型態為 `char *` 需要將算出來的商數轉換成字元型態才能加入
* ***注意到後面有*** `+ '0'` 的一個敘述,這是一個將整數轉字元的技巧,觀察其 [ASCII 碼](https://zh.wikipedia.org/wiki/ASCII)就會發現在 `48~57` 區間的顯示字元為 `'0'~'9'` , 而 `'0'` 所代表的值為 `48`
* `remainder = (remainder * 10) % d;` 更新下一輪的餘數值進行運算,如果 = 0 則表示已除盡
* `q` 每一輪會存取該輪的除法的商值, 以 `4 / 333` 來說經過 3 輪存下了 `0` 、 `1` 、 `2`
```c
struct rem_node *node = malloc(sizeof(*node));
node->key = remainder;
node->index = i;
MMM(&node->link, EEE);
*q++ = (remainder * 10) / d + '0';
remainder = (remainder * 10) % d;
```
* 因此經過了 `4 / 333` 經過四輪後得到像這樣的 `hash table` :
```graphviz
digraph graphname {
rankdir = LR;
splines = false;
node[shape = "record"]
new_node0[label = "node|key = 4|index = 3|<l>link"]
new_node1[label = "node|key = 67|index = 2|<l>link"]
new_node2[label = "node|key = 40|index = 1|<l>link"]
new_node3[label = "node|key = 4|index = 0|<l>link"]
ht[label = "Hash table|<m1>head[0]|<m2>head[1]|...|<m5>head[4]|...|<m41>head[40]|...|<m68>head[67]|...|<mn>head[1332]"]
ht:m41 -> new_node2:l
ht:m68 -> new_node1:l
ht:m5 -> new_node0:l -> new_node3:l
}
```
* 由於此範例會有循環小數的產生,觀察 `find` 函數,利用 `hash table` 找尋是否會有餘數相同的情況,也就是在 `hash table` 在任意 `head[i]` 中找到有相同 `key` (相同餘數)
* 所以範例中在 `head[4]` 中透過 `list_for_each_entry` 走訪可以得到會有相同的 `key` 發生,因此返回當前的 `index` 值,"返回第一次 `key` 的 `index` 值",範例中返回 `index = 0`
* ***返回 `index` 值意義在於不重複的循環小數值的數量***,例如:可能會有結果為 `"0.23(45)"` 其中 `2` 、 `3` 不為循環小數,此時在 `find` 的函數值會返回 2
```c
static int find(struct list_head *heads, int size, int key)
{
struct rem_node *node;
int hash = key % size;
list_for_each_entry (node, &heads[hash], link) {
if (key == node->key)
return node->index;
}
return -1;
}
```
* `list_for_each_entry` 對一個 `linked list` 的中,對每一個成員的起始位址進行訪查,所以能得到結構內部的資料
```c
#define list_for_each_entry(entry, head, member) \
for (entry = list_entry((head)->next, __typeof__(*entry), member); \
&entry->member != (head); \
entry = list_entry(entry->member.next, __typeof__(*entry), member))
```
* 解析循環小數的填入方式:
* 透過 `find` 函數找出不重複的循環小數值的數量,在 3~4 行中進入 `while` 迴圈要填入未循環的小數(商)值
* 加入 '(' 表示循環小數的開始
* 依序填入循環小數的值 (從 `q = decimal` )
* 加入 ')' 表示循環小數的結束
* 最後加入結尾符 `'\0'`
* 以 `4 / 333 ` 的範例中得出的結果為 `"0.(012)"`
:::success
PPP = pos-- 為每一次填入未循環小數就會遞減已填入的數量
:::
```c=
int pos = find(heads, size, remainder);
if (pos >= 0) {
while (PPP > 0)
*p++ = *decimal++;
*p++ = '(';
while (*decimal != '\0')
*p++ = *decimal++;
*p++ = ')';
*p = '\0';
return result;
}
```
* 延伸問題:指出程式碼不足,並予以改進
* 改寫程式碼
```c
bool sign = (float) numerator / denominator >= 0; /* origin */
bool sign = numerator < 0 ^ denominator < 0 /* improved */
```
* 看到程式碼中使用 `malloc` 配置記憶體,但是卻沒有釋放,因此需要釋放掉整個 `hash table` 所佔的記憶體空間(包含 `head` 與 `node`)
* 所以在計算 while 迴圈中的循環小數的 return 敘述前與程式最後的 return 前加入釋放記憶體的自定義函數
* 利用 `list_for_each_safe` 走訪每一個 heads 中所連結的 node 資料,並將其釋放
* 由於要進行記憶體釋放,所以透過 `cur` 可以記住走訪點的下一個位置,避免發生錯誤,在走訪的過程中較安全(也因此有 safe 敘述)
```c
void free_ht(struct list_head *heads, int size)
{
struct rem_node *node, *cur;
list_for_each_safe (node, cur, heads) {
free(node);
}
free(heads);
}
```
* `list_for_each_safe` 函式,[參考 list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h)
```c
#define list_for_each_safe(pos, n, head) \
for (pos = (head)->next, n = pos->next; \
!list_is_head(pos, (head)); \
pos = n, n = pos->next)
```
* 在兩個 `return result;` 前加入釋放記憶體的函式
```c
free_ht(heads, size);
return result;
```
## 測驗6
[`__alignof__`](https://gcc.gnu.org/onlinedocs/gcc/Alignment.html) 是 GNU extension,以下是其可能的實作方式:
```c
/*
* ALIGNOF - get the alignment of a type
* @t: the type to test
*
* This returns a safe alignment for the given type.
*/
#define ALIGNOF(t) \
((char *)(&((struct { char c; t _h; } *)0)->M) - (char *)X)
```
請補完上述程式碼,使得功能與 [`__alignof__`](https://gcc.gnu.org/onlinedocs/gcc/Alignment.html) 等價。
* 程式的目的,傳回變數型態的佔用 `byte` 長度,透過記憶體對齊的方式
* 舉例:
```c
printf("%ld", ALIGNOF(short)); /* 2 */
printf("%ld", ALIGNOF(int)); /* 4 */
```
* 查找的方式(以 int 示例):
* 為了記憶體對齊,所以 char 的宣告長度會與 int 一樣,如下圖
* 在 struct 定義中順序為先宣告 char 再宣告 int
* 由於 int 占了 4 bytes, char 占 1 byte,分配的方式如下圖
* `&((struct { char c; t _h; } *)0)->M)` 在這個敘述中目的要取得 `t` 的記憶體位置,因此 `M` 為 `struct` 結構中 `t` 型態的便數成員,所以 `M = _h`
* 得到了 int 的宣告的起始位置再扣掉 char 的起始位置,也就是單一變數型態的宣告長度,所以 `X = 0` 表示該結構的起始位置(也就是 char 的位置),計算後就可以得到引數的占用記憶體長度
:::success
M = _h
X = 0
:::
```graphviz
digraph graphname {
rankdir = LR;
node[shape = "record"]
alignment[label = "{char||||int(起始)|int|int|int}", width = 7]
}
```
* 延伸問題:在 Linux 核心原始程式碼中找出 `__alignof__` 的使用案例 2 則,並針對其場景進行解說
* 在 [kernal.h](https://github.com/torvalds/linux/blob/master/include/linux/kernel.h) 中找到兩個函式使用到 `__alignof__`
* 觀察兩個函式會發現主要兩個引數的型態,分別為 `unsigned int` 與 `unsigned long` ,第二個函式分別為 `unsigned int` 與 `long` 主要是將 `unsigned int` 轉換成 `long` 的型態變換
* `if-else` 判斷系統的 `long` 與 `long long` 分配的記憶體長度,進而選擇要對哪一種的型態進行記憶體對齊
* 參考至[Data Model](https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models),舉例來說如果在 `LP64 model` 下執行,因為 `long` 與 `long long` 都是 `64-bits` 所以會執行 if 的敘述使 `32-bits` 的 `int` 與 `long long` 轉換,並記憶體對齊
* 若是在 `LLP64 model` 則 `long` 與 `long long` 分別為 `32-bits` 與 `64-bits` ,此時會執行 else 敘述,使 `32-bits` 的 `int` 與 `long` 轉換,並記憶體對齊
* 引數中的 `const char *s` 用途與題目的 `char c` 一樣,編譯器會挑選占用較大的記憶體型態進行分配
```c
static inline int __must_check kstrtoul(const char *s, unsigned int base, unsigned long *res)
{
/*
* We want to shortcut function call, but
* __builtin_types_compatible_p(unsigned long, unsigned long long) = 0.
*/
if (sizeof(unsigned long) == sizeof(unsigned long long) &&
__alignof__(unsigned long) == __alignof__(unsigned long long))
return kstrtoull(s, base, (unsigned long long *)res);
else
return _kstrtoul(s, base, res);
}
static inline int __must_check kstrtol(const char *s, unsigned int base, long *res)
{
/*
* We want to shortcut function call, but
* __builtin_types_compatible_p(long, long long) = 0.
*/
if (sizeof(long) == sizeof(long long) &&
__alignof__(long) == __alignof__(long long))
return kstrtoll(s, base, (long long *)res);
else
return _kstrtol(s, base, res);
}
```
## 測驗7
考慮貌似簡單卻蘊含實作深度的 [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz) 題目:
* 從 1 數到 n,如果是 3 的倍數,印出 “Fizz”
* 如果是 5 的倍數,印出 “Buzz”
* 如果是 15 的倍數,印出 “FizzBuzz”
* 如果都不是,就印出數字本身
以下是利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼: (bitwise.c):
* 首先先分析 `is_divisible` 函數,功能為判斷是否為被整數 n 整除
* 以本題的 M3 為例創造一個 64 位元的 111..11 遮罩
示例一個整數 6 與 5 判斷,可發現值會有很大的差異
```
6:
6 * ((0xFFFFFFFFFFFFFFFF) / 3 + 1) = 4
5:
5 * ((0xFFFFFFFFFFFFFFFF) / 3 + 1) = -6148914691236517202
```
* 原因是如果是 3 的倍數的話乘回去,會剛好因為 overflow 的特性,所以 6 = 3 * 2 也就是整個補數系統會轉兩圈,所得出來的值為 6 - 2(補了2次的111..11遮罩到 0) = 4
* 因此可以判斷出數值使否能被該數的遮罩技巧整除,能剛好達到意外的效果檢測
```c
static inline bool is_divisible(uint32_t n, uint64_t M)
{
return n * M <= M - 1;
}
static uint64_t M3 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1;
static uint64_t M5 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 5 + 1;
```
* 在 `length` 中要決定字串的長度,最多為 8 ,所以要讓 `length` 達到 8 的可能為 2 shift 2 次,也就是若 `div3` 與 `div5` 都返回 `bool` 的 1 就可以決定要顯示多少長度的字串,分別為 2 (印出數字本身) 、 4 (Fizz 或 buzz) 、 8 (Fizzbuzz)
:::success
兩者個結果可交換,不管是否為 3 或 5 的倍數,都透過 `is_divisible` 函式判斷是否要 shift
KK1 = div3
KK2 = div5
:::
* 再來就是決定要印出的位置
* 可以看到 `8 >> div5` 若 `div5 = 1` 則會等於 4 會從 [4] 的位置開始印,若 `div5 = 0` 則會從 [8] 的位置開始印 "%u" 也就是原始數字
* 所以要從第 [0] 的位置開始印的話,必須把 8 透過 shift , 變成 0,所以 8 要至少要 shift 4 次,才能將 8 變成 0
* KK3 要能等於 4 由 div3 決定,也就是 `div3 = 1` 可透過左移 2 位得到 4
:::success
KK3 = div3 << 2
:::
```c=
int main(int argc, char **argv)
{
for (size_t i = 1; i <= 100; i++) {
uint8_t div3 = is_divisible(i, M3);
uint8_t div5 = is_divisible(i, M5);
unsigned int length = (2 << KK1) << KK2;
char fmt[9];
strncpy(fmt, &"FizzBuzz%u"[(8 >> div5) >> (KK3)], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
}
return 0;
}
```
:::info
在下列程式碼中第 9 行考慮到字串的位置 F 的位置是 [0] 、B的位置是 [4]、%的位置是 [8],要印出數字應該要是 "%u" 要從第 [8] 的位置印出
原程式碼為 9 會從 "u" 開始印,則會顯示不出原始數字
:::
## 參考文獻
* [你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics)
* [ISO/IEC 9899:1999](http://www.dii.uchile.cl/~daespino/files/Iso_C_1999_definition.pdf)
* [你所不知道的 C 語言:技巧篇](https://hackmd.io/@sysprog/c-trick?type=view#%E5%96%84%E7%94%A8-GNU-extension-%E7%9A%84-typeof)