---
tags: linux2022
---
# 2022q1 Homework2 (quiz2)
contributed by < `ChenBoSyun` >
## 測驗一
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (EXP1);
}
```
解題思路: `(a >> 1) + (b >> 1)` 等同於 `a / 2 + b / 2`
因此推斷 EXP1 是為了加回當 a, b 都是奇數時,被向下取整捨去的 1 (0.5 + 0.5)
**EXP1 = a & b & 1**
```c
uint32_t average(uint32_t a, uint32_t b)
{
return (EXP2) + ((EXP3) >> 1);
}
```
解題思路: average 可以視為相加後再除以 2
我們先做出一個加法器 add, `a & b` 是進位的部份,因此需要左移 1,`a ^ b` 是沒有進位的部份,兩者相加後就是完整的加法。
```c
uint32_t add(uint32_t a, uint32_t b)
{
return ((a & b) << 1) + (a ^ b);
}
```
現在已經跟 average 的答案很相近了,只差了一個除以 2,也就是右移 1。
**EXP2 = a & b**
**EXP3 = a ^ b**
### 解析組合語言
透過 `gcc -S -O3 add.c` 生成最佳化後的組合語言
version1
```c
#include <stdint.h>
uint32_t average(uint32_t low, uint32_t high)
{
return low + (high - low) / 2;
}
```
```
average:
.LFB0:
.cfi_startproc
endbr64
movl %esi, %eax
subl %edi, %eax
shrl %eax
addl %edi, %eax
ret
.cfi_endproc
```
:::info
%edi, %esi, %eax 都是常見的暫存器,分別是第一個參數,第二個參數,返回值
%edi 的首字母 e 代表該暫存器存取 32bit
movl, subl 等指令的尾字母 l 代表指令操作對象的大小為 4 byte
:::
| 指令 | 指令說明 | 執行完的結果 |
| -------- | -------- | -------- |
| `movl %esi, %eax` | 把參數2 high 賦予給回傳值 | `%eax: high` |
| `subl %edi, %eax` | %eax 減去參數1 low | `%eax: high - low` |
| `shrl %eax` | %eax 做 1 bit 的邏輯右移 | `%eax: (high - low) / 2` |
| `addl %edi, %eax` | %eax 加上參數1 low | `%eax: low + (high - low) / 2` |
version2
```c
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (a ^ b ^ 1);
}
```
```
average:
.LFB0:
.cfi_startproc
endbr64
movl %edi, %eax
movl %esi, %edx
xorl %esi, %edi
shrl %eax
shrl %edx
xorl $1, %edi
addl %edx, %eax
addl %edi, %eax
ret
.cfi_endproc
```
| 指令 | 指令說明 | 執行完的結果 |
| -------- | -------- | -------- |
| `movl %edi, %eax` | 把參數1 a 賦予 %eax | `%eax: a` |
| `movl %esi, %edx` | 把參數2 b 賦予 %edx | `%edx: b` |
| `xorl %esi, %edi` | 參數1 a 跟參數2 b 做 XOR 並把結果放進參數2 | `%edi: a ^ b` |
| `shrl %eax` | %eax 做 1 bit 的邏輯右移 | `%eax: a >> 1` |
| `shrl %edx` | %edx 做 1 bit 的邏輯右移 | `%edx: b >> 1` |
| `xorl $1, %edi` | %edi 跟 1 做 XOR 並把結果放進 %edi | `%edi: a ^ b ^ 1` |
| `addl %edx, %eax` | %edx 加進 %eax | `%eax: (a >> 1) + (b >> 1)` |
| `addl %edi, %eax` | %edi 加進 %eax | `%eax: (a >> 1) + (b >> 1) + (a ^ b ^ 1)` |
version3
```c
uint32_t average(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) >> 1);
}
```
```
average:
.LFB0:
.cfi_startproc
endbr64
movl %edi, %eax
andl %esi, %edi
xorl %esi, %eax
shrl %eax
addl %edi, %eax
ret
.cfi_endproc
```
| 指令 | 指令說明 | 執行完的結果 |
| -------- | -------- | -------- |
| `movl %edi, %eax` | 把參數1 a 賦予 %eax | `%eax: a` |
| `andl %esi, %edi` | 參數1 a 跟參數2 b 做 and 並把結果放進參數1 | `%edi: a & b` |
| `xorl %esi, %eax` | 參數2 b 跟 %eax 做 XOR 並把結果放進 %eax | `%eax: a ^ b` |
| `shrl %eax` | %eax 做 1 bit 的邏輯右移 | `%eax: (a ^ b) >> 1` |
| `addl %edi, %eax` | %eax 跟參數 1 相加 | `%eax: (a & b) + ((a ^ b) >> 1) ` |
### 研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用
## 測驗二
```c
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((EXP4) & -(EXP5));
}
```
解題思路: 這題會用到 XOR 的一個特性,相同的數字做 XOR 會抵銷掉,例如 x ^ x = 0
EXP5 是條件判斷式,結果為 false 或 true
當 EXP5 結果為 false 時
EXP4 & -0 = 0
a ^ 0 = a
因此推斷 **EXP5 = a < b**
當 EXP5 = a < b 為 true 時
EXP4 & -1 = EXP4
a ^ EXP4 需要等於 b,因為 a < b 為 true,要回傳 b
推斷 **EXP4 = a ^ b**
### 針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
### 找出 Linux 核心原始程式碼中,branchless / branch-free 的實作
## 測驗三
解釋下面這段 gcd 程式碼運作原理
```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;
}
```
> 註: 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 everything 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.
> 5. If x is even and y is odd, $gcd(x,y) = gcd(\frac{x}{2}, y)$
> 6. 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.
* `if (!u || !v) return u | v;` 對應到演算法 1, 2 點,當 u, v 有任一個數字為 0 時,回傳 `u | v`
若 `u == 0, v != 0` 回傳 v,同理 `u != 0, v == 0` 回傳 u,當 u, v 皆為 0 時回傳 0
* 6 ~ 8 行對應到演算法第 3 點,提出公因數為 2 的倍數
```c
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
```
* 9 ~ 10 行對應到演算法第 4 點
```c=
while (!(u & 1))
u /= 2;
```
* 11 ~ 22 行,求非 2 的倍數的公因數,並乘上先前計算的 2 倍數的公因數,回傳結果
```c
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;
```
算法核心是輾轉相減法,結束條件是當其中一方減至 0
**COND = v > 0**
結束時,剩餘的那方就是公因數,乘上之前計算的 2 倍數的公因數
**RET = u << shift**
以下用兩個實際例子說明演算法運作
表格的每列是每次迴圈執行完的結果
以 u = 3, v = 5 為例
| u | v |
| --- | --- |
| 3 | 5 |
| 3 | 2 |
| 3 | 1 |
| 1 | 2 |
| 1 | 1 |
| 1 | 0 |
回傳 u << shift 即 1 << 0,符合 $gcd(3,5) = 1$
以 u = 21, v = 14 為例
| u | v |
| --- | --- |
| 21 | 14 |
| 21 | 7 |
| 7 | 14 |
| 7 | 7 |
| 7 | 0 |
回傳 u << shift 即 7 << 0,符合 $gcd(21,14) = 7$
### 在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升
> 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.
`__builtin_ctz` 可以用來把
```c
while (!(v & 1))
v /= 2;
```
取代成
```c
v = v >> __builtin_ctz(v);
```
改寫後的 gcd 程式碼
```c
#define MIN(x, y) (((x) < (y)) ? (x) : (y))
uint64_t gcd64(uint64_t u, uint64_t v)
{
if (!u || !v) return u | v;
int shift;
int ctz_u = __builtin_ctz(u);
int ctz_v = __builtin_ctz(v);
shift = MIN(ctz_u, ctz_v);
u = u >> ctz_u;
v = v >> ctz_v;
do {
v = v >> __builtin_ctz(v);
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (v > 0);
return u << shift;
}
```
執行 100 萬次,觀察兩者的執行時間
```c
int main(){
clock_t begin = clock();
int i = 0;
srand(time(0));
for(i = 0; i < 1000000; i++){
gcd64((uint64_t)rand(), (uint64_t)rand());
}
clock_t end = clock();
double time_spent = (double)(end - begin) / CLOCKS_PER_SEC;
printf("gcd64 spend time for naive is %f second\n", time_spent);
}
```
```
gcd64 spend time for naive is 0.309016 second
gcd64 spend time for __builtin_ctz is 0.116132 second
```
__builtin_ctz 方式性能提昇 2.66 倍
另外嘗試用 objdump 觀察組合語言跟原始程式碼的對照
```
$gcc -o gcd -g gcd.c
$objdump -S gcd
```
:::info
紀錄一下使用 perf record 也可以看到組合語言跟原始程式碼的對照
```
$gcc -o gcd -g gcd.c
$perf record ./gcd
$perf report
```
:::
原始版本的組合語言
```
do {
while (!(v & 1))
1240: eb 0b jmp 124d <gcd64+0x84>
v /= 2;
1242: 48 8b 45 e0 mov -0x20(%rbp),%rax
1246: 48 d1 e8 shr %rax
1249: 48 89 45 e0 mov %rax,-0x20(%rbp)
while (!(v & 1))
124d: 48 8b 45 e0 mov -0x20(%rbp),%rax
1251: 83 e0 01 and $0x1,%eax
1254: 48 85 c0 test %rax,%rax
1257: 74 e9 je 1242 <gcd64+0x79>
```
__builtin_ctz 版本的組合語言
```
do {
v = v >> __builtin_ctz(v);
1229: 48 8b 45 d0 mov -0x30(%rbp),%rax
122d: f3 0f bc c0 tzcnt %eax,%eax
1231: 89 c1 mov %eax,%ecx
1233: 48 d3 6d d0 shrq %cl,-0x30(%rbp)
```
從組合語言可以看出
原始版本的 gcd 需要 7 個 instruction
__builtin_ctz 需要 3 個 instruction,雖然 tzcnt 需要 2 個 clock cycle,但總和 clock cycle 還是比較少
### Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景。
## 測驗四
```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;
}
```
上述程式碼,用來檢查 bitmap 中有哪些 bit 數值為 1,並且回傳他們在 butmap 中的位置
可以看到裡面的 for 迴圈,會逐一檢查 bit 是否為 1
若是使用 `__builtin_ctzll` 就不需要逐一檢查,它會回傳從尾端數過來會經過幾個 0,才會遇到第一個 1
使用 `__builtin_ctzll` 的程式碼
```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 跟 12 行,推測這兩行的目的是把 rightmost 1 變成 0,其他部份保留原樣
再看第 12 行
bitset 想要保留非 rightmost 1
XOR 有一個特性是 x ^ 0 = x
從這兩點來看 EXP6 要做的事情就是保留 rightmost 1,把其他的部份都變成 0
**EXP6 = bitset & -bitset**
原理: 因為二補數的特性 -bitset = ~bitset + 1
將 bitset 拆成兩個部份觀察,rightmost 1 左邊的部份,與右邊的部份
左邊的部份不會因為加上 1 而改變
因此左邊的部份就是 x & ~x = 0,全變成 0
右邊的部份,因為原本都是 0,反轉之後加上 1,就變回跟原本一樣 x & x = x
因此 rightmost 1 會被保留,而其他 1 都變成 0
### 設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density;
### 閱讀 Data Structures in the Linux Kernel 並舉出 Linux 核心使用 bitmap 的案例;
## 測驗五
測驗五的目的是給定一個分子跟分母,計算該分數的數值並用小數形式表示,最後以字串的形式回傳,該題目最關鍵的地方在於若是循環小數,需要用小括號包住循環的部份
例如: numerator = 7, denominator = 10
7/10 =1.42857142857...
回傳結果是 "1.(428541)"
```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 來紀錄小數點後的每個數字
當找到 hash table 中有重複的數字時,代表找到循環小數
小數點部份的程式碼
```c=
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;
```
13行到20行是當 remainder 不在 hash table 中
因此17行要做的事情是把 node 加進 linked list
MMM 是把 node 加進 list 的 define macro
**MMM = list_add**
EEE 是要加進的 list_head 指標
`EEE = &heads[remainder % size]`
19, 20行做除法運算
注意這邊移動的是指標 q,並沒有移動指標 decimal
再看第四行 `while (PPP > 0)` 就很明顯了,當第三行 `if (pos >= 0)` 偵測到循環小數,先取出非循環的部份
**PPP = (q - decimal)**
### 改寫程式碼
原本的程式碼有一些地方可以修改的更好
1. 處理負數的情況,可以用 bitwise 來實作 abs 來移除 branch
```c
/* deal with negtive cases */
if (n < 0)
n = -n;
if (d < 0)
d = -d;
```
abs with bitwise
```c
n = ((n >> 63) ^ n) - (n >> 63);
d = ((d >> 63) ^ d) - (d >> 63);
```
2. sign 判斷也可以用 bitwise 來做
```c
bool sign = (float) numerator / denominator >= 0;
if (!sign)
*p++ = '-';
```
```c
bool sign = (numerator ^ denominator) > 0;
if (!sign)
*p++ = '-';
```
## 測驗六
```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 macro 定義了結構 `struct { char c; t _h; }`
並將 0 轉型成該結構的指標,只要計算 t _h 成員與結構開頭的距離就可以知道 alignment 多少
**M = _h
X = 0**
###
查看 linux 原始碼實作Alignof(https://github.com/coreutils/gnulib/blob/master/lib/stdalign.in.h#L71)