---
tags: linux2022
---
# 2022q1 Homework2 (quiz2)
contributed by < [Eddielin0926](https://github.com/Eddielin0926) >
[2022q1 第 2 週測驗題](https://hackmd.io/@sysprog/linux2022-quiz2)
## 測驗一
對二個無號整數取平均值
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (a & b & 1);
}
```
`a >> 1` 和 `b >> 1` 代表將 a 和 b 個別除以 2,`a & b & 1` 用來檢查兩個數是不是都是奇數。
另一個對二個無號整數取平均值的實作。
```c
uint32_t average(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) >> 1);
}
```
我認為可以想像成全加器除二。
![](https://hackmd.io/_uploads/B1un5A2ec.png)
原本是 `(a + b) >> 1`,將 carry 提出來後就變成 `(a & b) + ((a ^ b) >> 1)`。
### 比較不同時做的組合語言輸出
使用 `gcc -S -masm=intel -Os` 來產生組合語言的輸出。
- `(a + b) / 2`
```c
lea eax, [rdi+rsi] // Load effective address, 將 rdi+rsi 的結果放到 eax
shr eax // logical shift right
ret // return
```
- `low + (high - low) / 2`
```c
mov eax, esi
sub eax, edi
shr eax
add eax, edi
ret
```
- `(a >> 1) + (b >> 1) + (a & b & 1);`
```c
mov eax, edi
mov edx, esi
and edi, esi
shr eax
shr edx
and edi, 1
add eax, edx
add eax, edi
ret
```
- `(a & b) + ((a ^ b) >> 1)`
```c
mov eax, edi
and edi, esi
xor eax, esi
shr eax
add eax, edi
ret
```
單純的比較指令數 `(a + b) / 2`,會是最少的,我嘗試以下程式碼來測試 x64 的電腦是否在計算過程中會溢位,結果是**不會的**。
```c
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) >> 1);
}
int main() {
uint32_t a = UINT_MAX, b = UINT_MAX;
printf("%u\n", average(a, b));
}
```
## 測驗二
```c
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
我們從 `a < b` 開始思考,
- 當 a < b 結果會是 1,取負數後會是 `0xFFFFFFFF` 與 `(a ^ b)` 做 AND 還是 `(a ^ b)`,最後的結果會是 `a ^ a ^ b` 也就是 b。
- 當 a => b 結果會是 0,取負數後還是 0,與 `(a ^ b)` 做 AND 還是 0,最後的結果會是 `a ^ 0` 也就是 a。
## 測驗三
```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 u << shift;
}
```
最前面迴圈,目的是為了計算兩個數字的公因數且是 2 的指數,先計算這個的好處就是方便,因為以二進位來說判斷最後一個 bit 是不是 0 就能知道是不是 2 的倍數。因此這邊迴圈結束後,可得到他們的公因數 `1 << shift` ,且 u 和 v 都不會是 2 的倍數了。
再來看 do-while 迴圈,這邊就是類似 GCD 的作法,但去除了取於數的方式,改成不斷相減,當 v 等於 0 的時候,就是餘數為零了。最後的結果 u 要在乘上一開始除掉的公因數,最後結果就是 `u * (1 << shift)` 也就是 `u << shift`。
## 測驗四
先觀察原本函式的功能
- `bitmap` 是要處理的資料,以 64 bits 為區間
- `bitmapsize` 是總共有要處的資料大小
- `out` 是結果存放的位置
在第一層迴圈中 `bitset` 會儲存目前處理資料,第二層的迴圈目的是要找到資料的位元是 1 的位置並且去除掉 1 後,將位置記錄在 `out` 中。
```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;
}
```
嘗試一個簡單的[例子](https://onlinegdb.com/qxgIaXhvp)
```
bitmap[0] = 0x11
bitmapsize = 1
```
我們可以得到 `bitmap` 中 1 的數量以及 1 的位置
```
pos = 2
out = {0, 4}
```
接下來透過 [`__builtin_ctzll`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 將函式改寫
`__builtin_ctzll` 用來找到由低位往高位的第一個 1 中間會有幾個 0,因此可以用來取代重複判斷的 `if ((bitset >> i) & 0x1)` ,就能減少分支的次數,在硬體有支援的情況下可以加速運算,再來看 `while` 的條件是 `bitset != 0`,加上 `__builtin_ctzll` 的特性,因此我們可以知道,當我們找到 `bitset` 的 1 時,要將 1 去處除掉,才有辦法利用 `__builtin_ctzll` 計算 1 的位置。
```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 = bitset & -bitset;
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
}
return pos;
}
```
當我們在思考如何去除掉某個位元的 1 時,第一個想法可能會是與一個在對應位置的 0 做 bitwise AND
```c
bitset &= ~(1 << r)
```
<!--
```
bitset = ((bitset >> (r + 1)) << r)
```
-->
但在這邊的做法利用了**二補數**來去除最低位元的 1。
```c
bitset ^= bitset & -bitset
```
在我們剛剛的計算中,我們目標都是 `bitset` 中最低位的 1,例如 XXX**1**00。
當我們對 `bitset` 做二補數計算時,最低位的 1 左邊的位元會是原本的補數,右邊則會與原本一樣。
$$
0110100_2 \xrightarrow{取二補數} 1001100_2 \\
0110100_2 \land 1001100_2 = 0000100_2 \\
0110100_2 \oplus 0000100_2 = 0110000_2
$$
與原本的 `bitset` 做 XOR 就能將最右邊的 1 變成 0 了。
## 測驗五
```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;
}
```
## 測驗六
```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)
```
`alignof` 的目的在於對齊位置,不同的機器在某些資料類型上大小並不相同,因此不能直接將位移量寫死,這時就需要 `alignof` 的幫助。
## 測驗七
```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;
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"[(9 >> div5) >> (div3 << 2)], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
}
return 0;
}
```
`(9 >> div5) >> (div3 << 2)], length)` 的 9 應為 8,才不會印出 u 來。