---
tags: linux2022, linux
---
# 2022q1 Homework2 (quiz2)
contributed by < `AmyLin0210` >
> [2022q1 第 2 週測驗題](https://hackmd.io/@sysprog/linux2022-quiz2)
## 測驗 1
### 解釋程式碼運作原理
在以下的程式碼中,`a >> 1` 與 `b >> 1` 分別代表將 `a` 與 `b` 除二並無條件捨去到整數位,而後方的 EXP1 要處理的就是進位的問題。
在 `a` 與 `b` 皆為奇數時,會需要把前面相加的結果加一,因此 `EXP` 在這裡是 `a & b & 0x1`。若 `a` 與 `b` 都是奇數,那做完 and 之後最右邊的位元應該要是 `1` ,後面再與 `0x1` 做 and,得到最右邊的位元。
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (EXP1);
}
```
觀察二進位加法,以下面的範例為例:
```
a = 0011
b = 0010
a + b = 0011 + 0010 = 0100 + 0001 = (a & b) << 1 + (a ^ b)
```
`a & b` 可以找到 a 與 b 在相同位置是否都是 1,如果是的話,表示在那個位置他們需要進位。
`a ^ b` 則是找到在哪邊 a 與 b 分別有值,代表的是相加後不需要進位的地方。
在取平均數時,需要把 a 與 b 相加除二,因此我們把上述的算式改進一下:
```c
(a + b) / 2 = ((a & b) << 1 + (a * b)) / 2 = (a & b) + ((a ^ b) >> 1);
```
```c
uint32_t average(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) >> 1);
}
```
## 測驗 2
### 解釋程式碼運作原理
在 `max` 這個函式裡,要做的就是回傳比較大的那個值,因此先來思考,在已經給定
```c
a ^ ((EXP4) & -(EXP5));
```
這樣的算式時,我要如何分別回傳出 `a` 與 `b` 。
看到 xor 的特性:
- 一個數與 `0` 這個數字做 xor ,那會得到的便是自己。
- 一個數與自己做 xor ,那會得到 `0`。
再來看看 and 的特性:
- 若一個數與 `0` 這個數字做 and ,那會得到 0 。
- 若一個數與二進制表示時全部都是 `1` 的數字做 and ,那會得到自己。
在二補數的系統中,`-1` 在使用二進制表示時,會全部都是 1 ,因此推論 `EXP5` 這個判斷式是回傳出 `a` 或者是 `b` 的關鍵。
先去考慮 `a ^ (EXP)` 會回傳出什麼東西
- 若 EXP 為 0 ,會回傳 a 。
- 若 EXP 為 `a ^ b` 由於 `a ^ a` 會變成 `0` ,所以得到 `0 ^ b` 會是 `b` 。
推測 `EXP4` 為 `a ^ b` ,這樣才有機會去拿到一個完整的 `b`。
接下來看到 `EXP5` ,目前我們知道目標是如果我想回傳 `a` ,那會變成 `a ^ ((a ^ b) & 0)`,如果我想回傳 `b`,那會變成 `a ^ ((a ^ b) & -1)` ,這樣 `EXP5` 的答案就很顯而易見,就是 `a < b` 。
```c
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
## 測驗 3
### 解釋程式碼運作原理
GCD 的演算法特性
1. If both x and y are 0, gcd is zero $gcd(0, 0) = 0$
2. $gcd(x, 0) = x$ and $gcd(0, y) = y$ because vevrything divides 0.
3. If x and y are both even, $gcd(x, y) = 2 * gcd(\frac{x}{2}, \frac{y}{2})$ because $2$ is a common divisor. Multiplication with $2$ can be done with bitwise shift operator.
4. If x is even and y is odd, $gcd(x, y) = gcd(\frac{x}{2}, y)$.
5. On similar lines, if x is odd and y is even, then $gcd(x, y) = gcd(x, \frac{y}{2})$. It is because 2 is not a common divisor.
以下的程式碼就是根據上述 GDB 演算法特性而改寫而來:
```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 (v);
return RET;
}
```
在第 4 行可以對照到 GDB 演算法特性的第 1 點與第 2 點,若 `u` 與 `v` 其中一方或雙方是 0 時, `u | v` 的結果便是 0 (在 u 和 v 皆為 0 時) 或者是非 0 的那個數 (在 u 和 v 僅有其中一方為 0 時)。
在第 6 行到第 8 行,可以對照到 GDB 演算法特性的第 3 點,把 `u` 與 `v` 共同對 2 的因數給取出來。
在第 9 行到第 10 行與第 12 行到第 13 行,可以對照到 GDB 演算法特性的第 4 點與第 5 點,代表把 u 與 v 對 2 的倍數給拿出來。
在第 14 到 19 行,就是經典的 GDB 操作,大數減去小數後,將結果存在 `v`,因此第 21 行的 while 迴圈判斷條件為 `v`,當減完的結果還有值,就表示 GDB 還沒完全完成。
第 22 行,回去看到第 6 到第 8 行,可以發現有把 `u` 與 `v` 共同的二的因數個數礎存在 `shift` 中,因此最後需要回傳 `u << shift` 把二的倍數部份乘回去。
## 測驗 4
### 解釋程式碼運作原理
```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;
}
```
我們直接跳到程式碼的第 10 行,在這裡想要使用 GNU extenstion 的 [__builtin_ctzll](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)
在這裡的 r 是從低位元往高位元需要連續遇上多少個 0 才碰的到 1。
然後第 12 行想要做的事情就是把已經數到的位元清零,從這邊判斷,EXP6 這裡想要做的事情是找到最低位元的 1 在哪裡,並把他設為 1,其餘位元設為 0。
因此最開始我猜 EXP6 為 `1 << __builtin_ctzll(bitset)`,但顯然的不符合作答規範。
現在來思考在什麼狀況下可以拿到等同於 `1 << __builtin_ctzll(bitset)` 的數字。
由於想要把大部分的數字給清零,首先知道 `a & ~a` 會變成 0 ,但這還不是我們想要的,我們需要留下最低位元的 1 。
以下是範例:
```
bitmap = 0b00001100
~bitmap = 0b11110011
~bitmap + 1 = 0b11110100
bitmap & (~bitmap + 1) = 0b00000100
```
然後根據二補數的定義, `~bitmap + 1` 會等同於 `-bitmap`,
所以可以得到 `EXP6 = bitset & -bitset`
## 測驗 5
這裡我們先來討論一般來說求循環小數會怎麼求。
在數學式部份,以 `4 / 333` 當作範例:
```
0.0120...
----------
333) 4 --- 1
00
---------
40 --- 2
333
---------
67 --- 3
666
--------
4 --- 4
00
```
可以從上圖很直覺地看到,當我們做到 (4) 時,由於已經與 (1) 重複了,所以就會判斷他是一個循環小數。
現在來對照下方的程式碼:
- 在第 77 行之前,做的事情為判斷正負、找到整數部份等等,第 77 行之後的 for loop 才正式開始找循環小數。
- 在第 78 行的地方,去找到是否數字已經被放在裡面了,在第 13 行的 find 函式就在做這件事情,而且使用 hash table 來加速。
- 如果有找到重複的數字,執行第 79 行到第 88 行的程式碼,如果沒有找到,則執行 89 到第 96 行的程式碼。
- 我們先來看看沒有找到的情況,在第 90 到 93 行,目標就是把目前的 remainder 與位置存起來,接著做的事情就非常的接近直式除法的部份,將 remainder 乘以 10 之後除以 denominator。
- 再來是看到有找到的情況,首先我們可以由 pos 得到從哪裡開始重複,當找到重複開始的位置之後,後面的就是循環小數。
```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 (pos-- > 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;
list_add(&node->link, &heads[remainder%size]);
*q++ = (remainder * 10) / d + '0';
remainder = (remainder * 10) % d;
}
strcpy(p, decimal);
return result;
}
```
## 測驗 6
```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)->_h) - (char *)0)
```
首先我們來拆解一下這份程式碼
- 看到 `struct { char c; t _h; } *)0` ,表示有個 `struct { char c; t _h; }` 型態的指標指向了記憶體的位置 0
- `(struct { char c; t _h; } *)0)->_h` 那到當中的 `_h` 參數
- `(&((struct { char c; t _h; } *)0)->_h)` 找到 `_h` 參數的位置
- `((char *)(&((struct { char c; t _h; } *)0)->_h) - (char *)0)` 強至轉形成 `char *` 的型態,並找到與 `(char *)0` 的距離差
簡單作個小實驗,程式碼:
```c
#include <stdio.h>
struct align_int {
char c;
int _h;
};
struct align_intp {
char c;
int *_h;
};
int main () {
char *c = 0;
struct align_int *a_int = 0;
struct align_intp *a_intp = 0;
}
```
可以發現 `int` 的 align 為 4, `int *` 這個指標的 align 為 8。
```bash
(gdb) p c
$1 = 0x0
(gdb) p &(a_int->_h)
$2 = (int *) 0x4
(gdb) p &(a_intp->_h)
$3 = (int **) 0x8
```
再來看看下面的程式碼,在這邊我做的變化是,在 `int` 型態的變數前,多塞幾個 `char` 型態的變數,看看 `int` 型態變數的記憶體位置會不會有什麼變化。
```c
#include <stdio.h>
struct align_int2 {
char c[2];
int _h;
};
struct align_int4 {
char c[4];
int _h;
};
struct align_int5 {
char c[5];
int _h;
};
int main () {
char *c = 0;
struct align_int2 *a_int2 = 0;
struct align_int4 *a_int4 = 0;
struct align_int5 *a_int5 = 0;
}
```
從結果可以看到,`int` 型態變數的記憶體位置的確會以 4 為倍數對齊。
```bash
(gdb) p c
$1 = 0x0
(gdb) p &(a_int2->_h)
$2 = (int *) 0x4
(gdb) p &(a_int4->_h)
$3 = (int *) 0x4
(gdb) p &(a_int5->_h)
$4 = (int *) 0x8
```
## 測驗 7
```c=
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
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;
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 << div3) << div5;
char fmt[9];
strncpy(fmt, &"FizzBuzz%u"[(8 >> div5) >> (div3 << 2)], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
}
return 0;
}
```
is_divisible 可以參考 [Faster remainders when the divisor is a constant: beating compilers and libdivide](https://lemire.me/blog/2019/02/08/faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide/) 這篇文章
下面控制 FizzBuzz 的部份,先來看到 `length` 這個變數,以下有 3 種可能:
- 不是 3 或 5 的倍數,`length` 為 2 ( **%u** )。
- 是 3 的倍數或是 5 的倍數,`length` 為 4 (**Fizz** 或 **Buzz**)。
- 是 3 及 5 的倍數,`length` 為 8 (**FizzBuzz**)。
看到程式碼的第 20 行,這裡會是 `(2 << div3) << div5` 的原因為,div3 與 div5 分別表示是不是被 3 或被 5 整除,所以只會是 1 或 0,`2 << 0 << 0` 、 `2 << 1` 與 `2 << 1 << 1` 的結果分別為 2, 4, 8 ,對應到 `length` 為 2, 4, 8 的情形。
再看到程式碼第 23 行的地方,這裡表示了要從哪裡開始讀 **FizzBuzz%u** 這個字串。
我們先看到 `8 >> div5`,這樣出來可能的結果為 8 或者是 4,換句話說就是,如果可以被五整除,那就是從 4 這個位置開始讀,如果不行,那就從 8 這個位置開始。
再來我們來看 `div3`,如果它是 3 的倍數,那我無論如何都是從 0 開始讀,如果不是,則要維持上面是否為 5 的倍數的結果。在 `div3 << 2` 這裡,可以發現如果 `div3` 為 1,出來的數值為 4,若 8 >> 4 會變成 0,反之若 `div3` 為 0,則不會動。