# 2022q1 Homework2 (quiz2)
contributed by < [`RoyWFHuang`](https://github.com/RoyWFHuang) >
###### tags: `linux_class`
> [2022q1 第 2 週測驗題](https://hackmd.io/@sysprog/linux2022-quiz2?fbclid=IwAR3DOwHxkJil7Jbr7UG8liqY9yPVOqfNcQQQ2w9dGDobJXz0HbmisETHvEE#2022q1-%E7%AC%AC-2-%E9%80%B1%E6%B8%AC%E9%A9%97%E9%A1%8C1)
開發環境為:
OS: ubuntu 20.04
kernel ver: 5.4.0-99-generic
CPU arch: x86_64
```shell
roy@roy-ThinkPad-T460s:$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.4 LTS
Release: 20.04
Codename: focal
roy@roy-ThinkPad-T460s:~$ uname -a
Linux roy-ThinkPad-T460s 5.4.0-100-generic
```
---
## 測驗 1
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (EXP1);
}
uint32_t average(uint32_t a, uint32_t b)
{
return (EXP2) + ((EXP3) >> 1);
}
```
EXP1 = a & b & 1
EXP2 = a & b
EXP3 = a ^ b
:::success
延伸問題:
1. 解釋上述程式碼運作的原理
2. 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章)
3. 研讀 Linux 核心原始程式碼 [include/linux/average.h](https://github.com/torvalds/linux/blob/master/include/linux/average.h),探討其 [Exponentially weighted moving average (EWMA)](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) 實作,以及在 Linux 核心的應用
> 移動平均(Moving average),又稱滾動平均值、滑動平均,在統計學中是種藉由建立整個資料集合中不同子集的一系列平均數,來分析資料點的計算方法。
移動平均通常與時間序列資料一起使用,以消除短期波動,突出長期趨勢或周期。短期和長期之間的閾值取決於應用,移動平均的參數將相應地設置。例如,它通常用於對財務數據進行技術分析,如股票價格、收益率或交易量。它也用於經濟學中研究國內生產總值、就業或其他宏觀經濟時間序列。
:::
### 1. 解釋上述程式碼運作的原理
1. EXP1 = a & b & 1
$\dfrac{a+b}{2}=\dfrac{a}{2}+\dfrac{b}{2}$ 而 $\dfrac{x}{2}$ 可以換成 (x >> 1)
所以運算式變成 (a >> 1) + (b >> 1), 但是 x >> 1 之後, 原本的 $\dfrac{x}{2}$.5 中的 0.5會因為位移而消失, 而當兩個 odd 相加的時候, 兩個的 0.5 就會造成結果進位, 所以最後要在多一個補償 (a & b & 1) 判別兩個數字是否都為奇數.
2. EXP2 = a & b, EXP3 = a ^ b
根據加法器的邏輯 a + b = (a & b) << 1 + a ^b
所以 (a + b)/2 = ((a & b) << 1 + a ^b) >> 1
展開後就變成上面的式子
### 2. 對應的組合語言輸出,並嘗試解讀
O1 下
(a >> 1) + (b >> 1) + (EXP1)
```asm
endbr64
movl %edi, %eax
shrl %eax
movl %esi, %edx
shrl %edx
addl %edx, %eax
andl %esi, %edi
andl $1, %edi
addl %edi, %eax
ret
```
(a & b) + ((a ^ b) >> 1)
```asm
average2:
endbr64
movl %edi, %eax
xorl %esi, %eax
shrl %eax
andl %esi, %edi
addl %edi, %eax
ret
```
O3 / Os 下
(a >> 1) + (b >> 1) + (EXP1)
```asm
movl %edi, %eax
movl %esi, %edx
andl %esi, %edi
shrl %eax
shrl %edx
andl $1, %edi
addl %edx, %eax
addl %edi, %eax
ret
```
(a & b) + ((a ^ b) >> 1)
```asm
movl %edi, %eax
andl %esi, %edi
xorl %esi, %eax
shrl %eax
addl %edi, %eax
ret
```
## 測驗 2
```c
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((EXP4) & -(EXP5));
}
```
EXP4 = a ^ b
EXP5 = a < b
:::success
延伸問題:
解釋上述程式碼運作的原理
針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
Linux 核心也有若干 branchless / branch-free 的實作,例如 lib/sort.c:
> ```c
> /*
> * Logically, we're doing "if (i & lsbit) i -= size;", but since the
> * branch is unpredictable, it's done with a bit of clever branch-free
> * code instead.
> */
> __attribute_const__ __always_inline
> static size_t parent(size_t i, unsigned int lsbit, size_t size)
> {
> i -= size;
> i -= size & -(i & lsbit);
> return i / 2;
> }
> ```
請在 Linux 核心原始程式碼中,找出更多類似的實作手法。請善用 git log 檢索。
:::
## 測驗 3
```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;
}
```
COND = v
RET = (u << shift)
:::success
延伸問題:
解釋上述程式運作原理;
在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升;
Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景。
:::
## 測驗 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;
}
```
EXP6 = bitset & (-bitset)
:::success
延伸問題:
解釋上述程式運作原理,並舉出這樣的程式碼用在哪些真實案例中;
設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density;
思考進一步的改進空間;
閱讀 Data Structures in the Linux Kernel 並舉出 Linux 核心使用 bitmap 的案例;
:::
## [測驗 5](https://hackmd.io/@sysprog/linux2022-quiz2?fbclid=IwAR3DOwHxkJil7Jbr7UG8liqY9yPVOqfNcQQQ2w9dGDobJXz0HbmisETHvEE#%E6%B8%AC%E9%A9%97-5)
```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;
}
```
PPP = pos--
MMM = list_add
EEE = &heads[remainder % size]
:::success
延伸問題:
1. 解釋上述程式碼運作原理,指出其中不足,並予以改進
>例如,判斷負號只要寫作 bool isNegative = numerator < 0 ^ denominator < 0;
搭配研讀 The simple math behind decimal-binary conversion algorithms
2. 在 Linux 核心原始程式碼的 mm/ 目錄 (即記憶體管理) 中,找到特定整數比值 (即 fraction) 和 bitwise 操作的程式碼案例,予以探討和解說其應用場景
:::
## [測驗 6](https://hackmd.io/@sysprog/linux2022-quiz2?fbclid=IwAR3DOwHxkJil7Jbr7UG8liqY9yPVOqfNcQQQ2w9dGDobJXz0HbmisETHvEE#%E6%B8%AC%E9%A9%97-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)->M) - (char *)X)
```
M = _h
X = 0
:::success
延伸問題:
解釋上述程式碼運作原理
在 Linux 核心原始程式碼中找出 __alignof__ 的使用案例 2 則,並針對其場景進行解說
在 Linux 核心源使程式碼找出 ALIGN, ALIGN_DOWN, ALIGN_UP 等巨集,探討其實作機制和用途,並舉例探討 (可和上述第二點的案例重複)。思索能否對 Linux 核心提交貢獻,儘量共用相同的巨集
:::
## [測驗 7](https://hackmd.io/@sysprog/linux2022-quiz2?fbclid=IwAR3DOwHxkJil7Jbr7UG8liqY9yPVOqfNcQQQ2w9dGDobJXz0HbmisETHvEE#%E6%B8%AC%E9%A9%97-7)
```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 << KK1) << KK2;
char fmt[9];
strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (KK3)], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
}
return 0;
}
```
KK1 = div3
KK2 = div5
KK3 = div3 << 2
:::success
1. 解釋上述程式運作原理並評估 naive.c 和 bitwise.c 效能落差
* 避免 stream I/O 帶來的影響,可將 printf 更換為 sprintf
2. 分析 Faster remainders when the divisor is a constant: beating compilers and libdivide 一文的想法 (可參照同一篇網誌下方的評論),並設計另一種 bitmask,如「可被 3 整除則末位設為 1」「可被 5 整除則倒數第二位設定為 1」,然後再改寫 bitwise.c 程式碼,試圖運用更少的指令來實作出 branchless;
* 參照 fastmod: A header file for fast 32-bit division remainders on 64-bit hardware
3. 研讀 The Fastest FizzBuzz Implementation 及其 Hacker News 討論,提出 throughput (吞吐量) 更高的 Fizzbuzz 實作
4. 解析 Linux 核心原始程式碼 kernel/time/timekeeping.c 裡頭涉及到除法運算的機制,探討其快速除法的實作 (注意: 你可能要對照研讀 kernel/time/ 目錄的標頭檔和程式碼)
> 過程中,你可能會發現可貢獻到 Linux 核心的空間,請充分討論
:::
### 1. 解釋上述程式運作原理並評估
首先先來分析 ==UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1== 這段的作用, 參考([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/)) 中的範例
:::info
> The intuition is as follows. To divide by four, you might choose to multiply by 0.25 instead. Take 5 * 0.25, you get 1.25. The integer part (1) gives you the quotient, and the decimal part (0.25) is indicative of the remainder: multiply 0.25 by 4 and you get 1
:::
==(0xFFFFFFFFFFFFFFFF) / 3== 這個其實就可以視為是 0.25的意義, 用圖來解釋如下 (以下縮短至16bit解釋)
```
0xFFFF 0xAAAA 0x5555 0x0000
|---------|---------|---------|
````
在系統中超過 16bit (overflow) 會自動被切斷, 所以在此數值系統中, 若落在 0x0000 ~ 0x5555之間, 則會被視為可以被整除, 0x5556 ~ 0xAAAA 則是餘一, 0xAAAB ~ 0xFFFF 則是餘二.
而 ==+1== 的目的, 是為了調整讓他確保可以落於區間內.
```
e.g.
2/3 -> 可以視為 2 * (0x5555 + 1) = 0xAAAC, 所以就是餘2
4/3 -> 可以視為 4 * (0x5555 + 1) = 0x5558, 所以就是餘1
```
PS: 這也就是為什麼需要 +1 做為調整, 必須讓數值完全落於區段內. 如果沒有 +1, 2 * (0x5555) = 0xAAAA 則會剛好落於邊界, 無法判別是 1 or 2.
基於這個原理, 於是我們可以改寫
```c
static inline bool is_divisible(uint32_t n, uint64_t M)
{
return n * M <= M - 1;
}
```
這段變成
```c
static inline uint32_t mod(uint32_t n, uint64_t M)
{
if(!(n * M <= M - 1)) {
return n * M / M;
} else {
return 0;
}
}
```
可以變成求取餘數.
但問題來了, 為什麼他只能用來計算 uint32_t 以內, 超過不行呢? 以下我們來做個實驗 n = 0x7FFFFFFFFFFFFFFF 跟 n = 0x8000000000000000 套入看看
```c
printf("M3 0x7FFFFFFFFFFFFFFF (%d)\n", mod(0x7FFFFFFFFFFFFFFF, M3));
printf("M3 0x8000000000000000 (%d)\n", mod(0x8000000000000000, M3));
```
```shell
M3 0x7FFFFFFFFFFFFFFF (1)
M3 0x8000000000000000 (0)
```
很明顯的錯誤了, 至於未什麼會出現這錯誤, 原因就在於 ==+1==, 因為每次運算的數值都會被多1出來
e.g
同樣以 16bit為例
|num | hex after mul M3| over num(compare with 0x5555)|
|-|-|-|
|1 | 0x5556 | 1 |
|2 | 0xAAAC | 2 |
|3 | 0x0002 | 2 |
|4 | 0x5558 | 3 |
|5 | 0xAAAE | 4 |
|6 | 0x0004 | 4 |
我們換個陳列方式
|N|hex|over||N|hex|over|
|-|-|-|-|-|-|-|
|1 | 0x5556 | 1 ||4 | 0x5558 | 3 |
|2 | 0xAAAC | 2 ||5 | 0xAAAE | 4 |
|3 | 0x0002 | 2 ||6 | 0x0004 | 4 |
很明顯以2為倍數去增加, 那什麼時候會 overflow? 當他超過到下個區間的時候, 也就是 0x5555 + 0x5555(over flow) 此時的數值就會是錯誤的了.
所以就可以推算出他的最大可運算的數字是多少了(0x5555/2 + 0x5555 = 0x7FFF)
:::warning
但問題來了 ==/3== ==/5== 都可以正確算出數字是多少
```shell
M3 0x7FFFFFFFFFFFFFFF (1)
M3 0x8000000000000000 (0)
M5 0x3FFFFFFFFFFFFFFF (3)
M5 0x4000000000000000 (0)
```
但是 ==/7== 跟 ==/9== 卻不是此規則
```shell
M7 0x3333333333333335 (5)
M7 0x3333333333333336 (0)
```
猜測是 0xFFFF...FFFF/7 之後不是整除, 而是 FFFFFFFFFFFFFFFE / 7 才是整除造成, 但我找不出公式可以解決, 有人可以幫忙解決嗎?
:::