owned this note
owned this note
Published
Linked with GitHub
---
tags: linux, kernel
---
# 2022q1 Homework2 (quiz2)
contributed by < `Destiny0504` >
> [題目連結](https://hackmd.io/@sysprog/linux2022-quiz2)
## 測驗一
``` c
uint32_t average(uint32_t a, uint32_t b)
{
// 判斷 a 和 b 都是奇數的情況下,才加一
return (a >> 1) + (b >> 1) + (a & b & 1);
}
```
用 a & b 來實現 a - b 的操作,
``` c
uint32_t average(uint32_t a, uint32_t b)
{
return (a & b) + ((a | b) >> 1);
}
```
### 兩種實作方式的組合語言比較
- 第一種實作方式的 x86 組合語言版本
```objectivec
average(unsigned int, unsigned int):
// 將 edi 的值複製到 eax
mov eax, edi
// 將 esi 的值複製到 edx
mov edx, esi
// edi = edi & esi
and edi, esi
// eax = edi >> 1
shr eax
// edx = esi >> 1
shr edx
// edi = edi & exi & 1
and edi, 1
// eax = (esi >> 1) + (edi >> 1)
add eax, edx
// eax = (esi >> 1) + (edi >> 1) + (edi & exi & 1)
add eax, edi
ret
```
- 第二種實作方式的 x86 組合語言版本
```objectivec
average(unsigned int, unsigned int):
// 將 edi 的值複製到 eax
mov eax, edi
// edi = edi & esi
and edi, esi
// eax = edi | esi
or eax, esi
// eax = (edi | esi) >> 1
shr eax
// eax = ((edi | esi) >> 1) + (edi & esi)
add eax, edi
ret
```
||第一種實作方式|第二種實作方式|
|-|-|-|
|register 使用數量|4|3|
|add指令數|2|1|
|and指令數|2|1|
|mov指令數|2|1|
|shr指令數|2|1|
|or指令數|0|1|
### 結論
第一種實作方式需要用到 a 、b 個別的值,所以需要花費額外的 register 將它存起來,所以在實作的要盡量避免這種所有要加起來的值都有關係的情況。
### Exponentially weighted moving average ( EWMA )
#### 優點
- 只要紀錄上一次的值就可以得到下一項應該是多少,能減少使用的記憶體空間。
- 可以動態的計算結果,不需要一次就看完所有的資料。
#### 在 linux kernel 中的實作
- linux kernel 中是以一個 pointer 指向一個 unsigned long 的變數,而這個變數就是用來儲存過去計算過得結果的。
- 考慮以下實作的方法,internal 先乘以 2 的 weight_rcp 次方減去原本的 internal,相當於 $\text{internal}\times\frac{1}{2^{weight\_rcp}}$,在加上新的 val 。
- e.g. 如果 weight_rcp = 8 ,相當於每次把過去運算的結果乘以 $\frac{7}{8}$,再加上新的 val 乘以 $\frac{1}{8}$ 。
``` c
WRITE_ONCE(e->internal, internal ? \
(((internal << weight_rcp) - internal) + \
(val << precision)) >> weight_rcp : \
(val << precision));
```
## 測驗二
由 a <= b 來判斷我們應該回傳 a 還是 b
- 當 a > b 的時候 -(a <= b) 會得到零,所以可以直接無視 (b ^ a) 會得到的值,而零跟 a 做 XOR 則會得到 a ,所以回傳值為 a
- 當 a > b 的時候-(a <= b) 會得到 1 ,所以可以直接視為回傳的值為 a ^ (b ^ a),而 a 跟 b ^ a 做 XOR 則會得到 b ,最後得到的回傳值為 b
``` c
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((b ^ a) & -(a <= b));
}
```
- 正確答案是給 a ^ b 和 a < b, 但我想我的實作也可以達到同樣的效果。
### Branch-free 版本
利用 a < b 時,a - b 會 overflow 的特性,所以只要判斷是否有 overflow 的情況即可。
- 因為 a 和 b 都是 unsigned int,所以不能只由最高位來判斷是否出現 overflow,還要接著判斷 a 的最高位是否大於 $2^{31} - 1$,如果兩者皆符合,才能判斷為遇到 overflow
``` c
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((b ^ a) & (-((a - b) >> 31) ^ -(a >> 31)));
}
```
## 測驗三
以下程式碼的運作原理為 Euclidean algorithm ,差別只在於每一步找都先把 v 換成奇數而已。而之所以可以只留下奇數的部份,是因為我們前面已經把 u 跟 v 所擁有的 2 的倍數的公因數都去除掉了。( 可以參考 reference 提到的演算法 )
``` c
#include <stdint.h>
uint64_t gcd64(uint64_t u, uint64_t v)
{
// 如果 u 跟 v 其中一個是 0,直接回傳不為 0 的那一個
if (!u || !v) return u | v;
int shift;
// 用來找出 u 跟 v 公因數有 2 的幾次方
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);
// 最後回傳的時候要把前面除去的 2 的次方的公因數乘回來
return u << shift;
}
}
```
### 用 ```__builtin_ctz``` 改寫 GCD 的版本
``` c
uint64_t gcd64(uint64_t u, uint64_t v)
{
if (!u || !v) return u | v;
int shift = min(__builtin_ctz(u) , __builtin_ctz(v));
u >>= shift, v >>= shift;
u >>= __builtin_ctz(u);
do {
v >>= __builtin_ctz(v);
if (u < v) {
v -= u;
printf("%ld %ld\n",u, v);
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (v);
return u << shift;
}
```
### Reference
[GCD 演算法](https://hackmd.io/@sysprog/linux2022-quiz2#%E6%B8%AC%E9%A9%97-3)
## 測驗四
- 因為 2 complement 的編碼特性,所以一個數字 ```a``` 在跟 ```-a``` 取 bitwise and 會剛好找出目前最低位元的 1 。
- e.g. a = 12 轉換為二進位 = ```01100``` , -a 轉換為二進位 = ```10100```,取 bitwise and 之後剛好等於 ```00100```
``` 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;
}
```
## 測驗五
``` 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;
}
```
## 測驗六
- 我們在測驗中所作的版本能夠直接回傳 _h 的 datatype 在 memory 中所佔有的 byte 數量。
- 運作的方式是先拿到 _h 的記憶體位置,減去 char 的 pointer 的位置。
- 作法有點類似 container_of,差別在於一個是加上 offset 得到指向某一個變數的 pointer,而 alignof 是算出 offset
- 注意:這邊 char c 要擺在 t _h 前面,否則無法算出 t 的記憶體大小。( 因為 struct 為連續的記憶體區塊的關係 )
``` 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)
```
### align function 說明
- ALIGN(x, a) 要拿 x 以 a 作為對齊的基準進行向上對齊
- e.g. a = 128, x = 12 會回傳 128
- 注意:這個 function 只有 a 為 2 的整數次方算出來的對齊結果才是對的。因為這個 function 通常是用來計算記憶體的位置的,所以只有在上述的情況下算出來的結果才是對的也沒問題。
``` c
#define ALIGN(x, a) __ALIGN_KERNEL((x), (a))
```
- ALIGN_DOWN(x, a) 要拿 x 以 a 作為對齊的基準進行向下對齊
- e.g. a = 128, x = 12 會回傳 0
- 注意:這個 function 跟 ALIGN(x, a) 的使用條件一樣
``` c
#define ALIGN_DOWN(x, a) __ALIGN_KERNEL((x) - ((a) - 1), (a))
```
### Reference
[linux 實作的 align](https://github.com/torvalds/linux/blob/master/include/linux/align.h)
[linux 實作的 align_kernel](https://github.com/torvalds/linux/blob/master/tools/firmware/ihex2fw.c)
``` c
#define __ALIGN_KERNEL_MASK(x, mask) (((x) + (mask)) & ~(mask))
#define __ALIGN_KERNEL(x, a) __ALIGN_KERNEL_MASK(x, (typeof(x))(a) - 1)
```
## 測驗七
因為在 compile 的時候 M3 、M5 都已經被計算好,而且乘法的運算速度又比除法快,所以會比[另一種](https://hackmd.io/@sysprog/linux2022-quiz2#%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 << div3) << div5;
char fmt[9];
strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (div3 << 2)], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
}
return 0;
}
```
### 兩種方法的時間比較
- 用 linux 的 time 指令進行測量,且迴圈執行 ```100000``` 次
- 方法一為題目中的 [naive.c](https://hackmd.io/@sysprog/linux2022-quiz2#%E6%B8%AC%E9%A9%97-7) ,方法二為上面的程式碼
- 從下方的結果來看,方法二確實有更好的 performance
||方法一|方法二|
|-|-|-|
|實驗一|0.438|0.330|
|實驗二|0.442|0.445|
|實驗三|0.478|0.447|
|實驗四|0.427|0.464|
|實驗五|0.457|0.478|
|實驗六|0.494|0.451|
|實驗七|0.518|0.303|
|實驗八|0.480|0.375|
|實驗九|0.394|0.447|
|實驗十|0.586|0.269|
|average|0.4714|0.4009|
|max|0.586|0.478|
|min|0.394|0.269|