---
tags: linux2022
---
# 2022q1 Homework2 (quiz2)
contributed by < [`jj97181818`](https://github.com/jj97181818) >
## 測驗 1
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (EXP1);
}
```
| | (a >> 1) + (b >> 1) + (a & b & 1) |
| -------- | -------- |
| a = 2, b = 2 | 1 + 1 + 0 |
| a = 2, b = 3 | 1 + 1 + 0 |
| a = 3, b = 2 | 1 + 1 + 0 |
| a = 3, b = 3 | 1 + 1 + 1 |
在 a 和 b 右移 1 位之後,最低位元可能會出現需要進位的狀況,因此要將最低位元考慮進來,也就是加上第三項 `EXP1` = `a & b & 1`。
```c
uint32_t average(uint32_t a, uint32_t b)
{
return (EXP2) + ((EXP3) >> 1);
}
```
在 [你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics#%E9%81%8B%E7%94%A8-bit-wise-operator) 有提到用加法器來思考:`x + y = x ^ y + (x & y) << 1`
其中,`x ^ y` 可求得位元相加不進位的總和,`x & y` 則求得進位,並將進位的值左移一位,與總和相加。
```
(x + y) / 2
= (x ^ y + (x & y) << 1) >> 1
= ((x ^ y) >> 1) + (x & y)
```
現在要求 `(x + y) / 2` 就是將 `x ^ y + (x & y) << 1` 整個右移一位,求得 `((x ^ y) >> 1) + (x & y)`,因此,EXP2 = `a & b`, EXP3 = `a ^ b`。
### 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出
> 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章)
### 研讀 Linux 核心原始程式碼 include/linux/average.h
> 研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用
## 測驗 2
先看 [解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation#%E4%B8%8D%E9%9C%80%E8%A6%81%E5%88%86%E6%94%AF%E7%9A%84%E8%A8%AD%E8%A8%88) 提供 min 的程式碼:
```c
int32_t min(int32_t a, int32_t b) {
int32_t diff = (a - b);
return b + (diff & (diff >> 31));
}
```
+ a > b:會因為 diff 是正的,`diff >> 31` 為 0,`(diff & (diff >> 31))` 就會是 0,直接回穿 b。
+ a < b:會因為 diff 是負的,`diff >> 31` 32 個 bit 都是 1,因此 `(diff & (diff >> 31))` 就會是 `diff` 本身,所以 `b + diff` = `b + (a - b)` = `a`,最後回傳 a。
這題 max 的程式碼:
```c
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((EXP4) & -(EXP5));
}
```
限制:
+ EXP4 為 a 和 b 進行某種特別 bitwise 操作,限定用 OR, AND, XOR 這三個運算子。
+ EXP5 為 a 和 b 的比較操作,亦即 logical operator,限定用 >, <, ==, >=, <=, != 這 6 個運算子。
1. 這個程式最後要回傳 a 或是 b,看誰比較大。而 `a ^ 0` 會是 a 自己,而 `a ^ (b - a)` 會是 b(會寫 `b - a` 是參考 min 程式碼中 `diff` 的想法),所以 `((EXP4) & -(EXP5))` 會是 0 或是 `(b - a)`。
2. 因為 EXP5 是做比較運算,會回傳 0 或 1,在 EXP5 為 0 的情況下,不論 EXP4 是什麼,`((EXP4) & -(EXP5))` 都會是 0,也就是最後會回傳 a,是 a 比較大的情況。
3. 因此 EXP5 為 1 就會是 b 比較大的情況,EXP5 即為 `a < b`,-(EXP5) 為 -1,每個 bit 皆為 1,任何值與 -1 做 AND 運算都還會是自己,也就是 `((EXP4) & -(EXP5))` 的值會被 `EXP4` 決定。
4. 會希望 `EXP4` 會是 `b - a`,但是限定使用 OR, AND, XOR。當 `EXP4` 為 `a ^ b` 時,回傳的值會是 `a ^ (a ^ b)`,a 和 a 自己做 XOR 會是 0,0 再和 b 做 XOR 就會是 b 本身了。
因此 `EXP4` = `a ^ b`, `EXP5` = `a < b`
### 針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
針對有號整數的實作和無號整數的實作一樣,因為這裡做的 XOR 的結果對於有號與無號沒有差別,後面 `a < b` 的部分就是按照兩者的大小比較,是 0 或 1,對有號無號也沒有影響。
```c
#include <stdint.h>
int32_t max(int32_t a, int32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
### Linux 核心也有若干 branchless / branch-free 的實作
> 請在 Linux 核心原始程式碼中,找出更多類似的實作手法,請善用 git log 檢索。
在 `drivers/gpu/drm/radeon/r600_blit.c` 的[第 492 行](https://github.com/torvalds/linux/blob/747f49ba67b8895a5831ab539de551b916f3738c/drivers/gpu/drm/radeon/r600_blit.c#L492)有 branchless 的實作:
int2float 是將 int 轉成 float,這裡避免 branch 的方法是先用 `__fls` 找出 MSB 的位置,然後用旋轉指令將 unsigned int 擴展成浮點數。
```c
/* 23 bits of float fractional data */
#define I2F_FRAC_BITS 23
#define I2F_MASK ((1 << I2F_FRAC_BITS) - 1)
/*
* Converts unsigned integer into 32-bit IEEE floating point representation.
* Will be exact from 0 to 2^24. Above that, we round towards zero
* as the fractional bits will not fit in a float. (It would be better to
* round towards even as the fpu does, but that is slower.)
*/
uint32_t int2float(uint32_t x)
{
uint32_t msb, exponent, fraction;
/* Zero is special */
if (!x) return 0;
/* Get location of the most significant bit */
msb = __fls(x);
/*
* Use a rotate instead of a shift because that works both leftwards
* and rightwards due to the mod(32) behaviour. This means we don't
* need to check to see if we are above 2^24 or not.
*/
fraction = ror32(x, (msb - I2F_FRAC_BITS) & 0x1f) & I2F_MASK;
exponent = (127 + msb) << I2F_FRAC_BITS;
return fraction + exponent;
}
```
也一併附上原本有 branch 的[程式碼](https://github.com/torvalds/linux/commit/747f49ba67b8895a5831ab539de551b916f3738c?diff=split):
```c
uint32_t int2float(uint32_t input)
{
u32 result, i, exponent, fraction;
if ((input & 0x3fff) == 0)
result = 0; /* 0 is a special case */
else {
exponent = 140; /* exponent biased by 127; */
fraction = (input & 0x3fff) << 10; /* cheat and only
handle numbers below 2^^15 */
for (i = 0; i < 14; i++) {
if (fraction & 0x800000)
break;
else {
fraction = fraction << 1; /* keep
shifting left until top bit = 1 */
exponent = exponent - 1;
}
}
result = exponent << 23 | (fraction & 0x7fffff); /* mask
off top bit; assumed 1 */
}
return result;
}
```
## 測驗 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;
}
```
1. 在做 do while 的輾轉相除法之前,如果 u 和 v 皆為 2 的倍數,就將 u, v 都除以 2,直到至少其中一個不為 2 的倍數,shift 紀錄著除以 2 的次數(右移的次數)。
2. 輾轉相除法只關心每次除法的餘數是否為 0,為 0 時即得到最大公因數。這裡我們用相減的方式,當 `t = u - v` 的差為零時,表示得到最大公因數,又 `v = t`,因此`COND` = `v`。
3. 最後回傳的最大公因數為 `u`,但因為在做輾轉相除法之前有先將 u 除以 2(右移),因此要乘 2 shift 次(左移)。
因此 `COND` = `v`, `RET` = `u << shift`
### 在 x86_64 上透過 `__builtin_ctz` 改寫 GCD,分析對效能的提升
將原本判斷是偶數時,用迴圈不停地除以 2 的程式,都改成用 `__builtin_ctz` 算出從 LSB 到最低位元 1 之前的 0 的數量,然後透過右移來達到除以 2 的效果。
```c
#include <stdint.h>
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;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (v);
return u << shift;
}
```
### 解釋其實作手法和探討在核心內的應用場景
> Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景。
在 `lib/math/gcd.c` 中有實作兩種 GCD,如果能使用 `__ffs`(find first bit in word),就用第一種 GCD 實作,反之用第二種。`__ffs.h` 會針對不同架構做不同的實作,通用版本的 `__ffs.h` 在 `linux/include/asm-generic/bitops/__ffs.h` 中:
在使用 `__ffs.h` 的 GCD 實作中,和以 `__builtin_ctz` 改寫 GCD 程式中的 `__builtin_ctz` 很像,只是換成使用 `__ffs`(這裡的 `__ffs` 是從 0 開始從 LSB 算到最低位元 1,`__builtin_ctz` 是從 1 開始算到最低位元 1 的前一個,剛好結果會是一樣的)。
`r & -r` 和測驗 4 的答案 `EXP6` = `bitwise & -bitwise` 是同樣意思,會留下最低位元的 1,其餘位元為 0。
```c
#if !defined(CONFIG_CPU_NO_EFFICIENT_FFS)
/* If __ffs is available, the even/odd algorithm benchmarks slower. */
/**
* gcd - calculate and return the greatest common divisor of 2 unsigned longs
* @a: first value
* @b: second value
*/
unsigned long gcd(unsigned long a, unsigned long b)
{
unsigned long r = a | b;
if (!a || !b)
return r;
b >>= __ffs(b);
if (b == 1)
return r & -r;
for (;;) {
a >>= __ffs(a);
if (a == 1)
return r & -r;
if (a == b)
return a << __ffs(r);
if (a < b)
swap(a, b);
a -= b;
}
}
#else
/* If normalization is done by loops, the even/odd algorithm is a win. */
unsigned long gcd(unsigned long a, unsigned long b)
{
unsigned long r = a | b;
if (!a || !b)
return r;
/* Isolate lsbit of r */
r &= -r;
while (!(b & r))
b >>= 1;
if (b == r)
return r;
for (;;) {
while (!(a & r))
a >>= 1;
if (a == r)
return r;
if (a == b)
return a;
if (a < b)
swap(a, b);
a -= b;
a >>= 1;
if (a & r)
a += b;
a >>= 1;
}
}
#endif
```
## 測驗 4
```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 陣列裡面所有 uint64_t 型別的整數中,bit 為 1 的位置都記錄到 out 裡面。
```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;
}
```
限制:
+ 考慮到 -bitwise 的特性
1. 如果 bitwise = 01101**100**,那 -bitwise = 10010**100**,可以發現 bitwise 和 -bitwise 最低位元的 1 往右的位元都一樣。
2. 在 bitset 不是 0 的時候就繼續在迴圈裡面找最低位元的 1,因此在第 12 行的 `bitset ^= t;` 應該要將 bitwise 該次找到最低位元的 1 改成 0,這裡的 t 要只有最低位元的 1,其餘位元都是 0,才能讓 bitset 和 t 做 XOR 之讓最低位元的 1 改為 0。
3. `t = bitwise & -bitwise` 可以讓 t 只留下最低位元的 1。
因此 `EXP6` = `bitwise & -bitwise`
### 舉出這樣的程式碼用在哪些真實案例中
因為只儲存 1 的位置,所以可以用在壓縮儲存大小的場景,例如:壓縮圖片。
### 檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進
> 設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density;
### 思考進一步的改進空間
### 舉出 Linux 核心使用 bitmap 的案例
> 閱讀 Data Structures in the Linux Kernel 並舉出 Linux 核心使用 bitmap 的案例
## 測驗 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;
}
```
+ 13~23 行:`hash = key % size;` 用 remainder % size 的方式決定要把節點塞在哪一個 heads 的 linked list。在這個 function 裡面,我們嘗試在特定 linked list 找尋是否有相同的餘數(key)的節點,如果有就回傳 index(value),如果找不到就回傳 -1。
+ 72~75 行:先替 heads 配置記憶體,產生有 1333 個 bucket 的 hashtable,可以用 index 去指定特定 bucket。並將每個 bucket 的頭都先指向自己,因為還沒有任何值放進來。
+ 77~93 行:當還有餘數 remainder 的時候,就在這個迴圈裡處理小數點後面的數字。先去 hash table 找特定 linked list 有沒有一樣的餘數。
+ 79~88 行:如果有一樣的餘數,pos >= 0,處理循環小數的部分。
+ 89~96 行:如果沒有,pos 為 -1,並配置一個節點加到 linked list 上面,其中 key 為餘數,index 為小數點後第幾位(從第 0 位開始算)。要將節點加在 hashtable 上的某個 bucket,用 find 函式尋找特定餘數的方式一樣,用 `ramainder % size` 來決定要在哪個 heads 的 linked list 找餘數。
(remainder * 10) / d
+ 95~96 行: `(remainder * 10) / d` 是算出這位的小數數字,`+ '0'` 讓前面數字轉成字元,然後加到 `dicimal` 裡。`(remainder * 10) % d` 會算出下一輪的餘數。
因此 `PPP` = `pos--`, `MMM` = `list_add`, `EEE` = `&heads[remainder % size]`
### 指出其中不足,並予以改進
#### 改進一:
第 58 行,不需要特別做 division > 0 的條件判斷:
```c
sprintf(p, "%ld", division > 0 ? (long) division : (long) -division);
```
因為在第 45 行已經處理過為負數的 n 和 d 了:
```c
/* deal with negtive cases */
if (n < 0)
n = -n;
if (d < 0)
d = -d;
```
所以可以改成:
```c
sprintf(p, "%ld", (long) division);
```
#### 改進二:
第 51 行的判斷正負號:
```c
bool sign = (float) numerator / denominator >= 0;
```
可以改成:
```c
bool sign = numerator < 0 ^ denominator < 0;
```
### 找到特定整數比值 (即 fraction) 和 bitwise 操作的程式碼案例
> 在 Linux 核心原始程式碼的 mm/ 目錄 (即記憶體管理) 中,找到特定整數比值 (即 fraction) 和 bitwise 操作的程式碼案例,予以探討和解說其應用場景
## 測驗 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)
```
1. 解釋 ALIGNOF(t)
+ `(struct { char c; t _h; } *)0` 表示將 0 轉型成指向 `struct { char c; t _h; }` 的 pointer,他的特殊意義為空指標,但不能確定空指標指向的位置。
+ `(&((struct { char c; t _h; } *)0)->M)` 是取得 struct 中某個成員 `M` 的位址
+ `(char *)(&((struct { char c; t _h; } *)0)->M)` 是將 struct 中某成員 `M` 的位址轉型成 char*
+ `((char *)(&((struct { char c; t _h; } *)0)->M) - (char *)X)` 就是將 struct 中某成員 `M` 的位址減去 X
2. [offsetof](https://en.wikipedia.org/wiki/Offsetof) 是找某個 struct st 中的成員 m 與 struct 起始位置間的距離(指定成員和起始位置的偏移量)。比較好的寫法是第二種,用位址減去另外一個位址,得到位址的偏移量,第一種是直接得到 m 的位址,但是不能保證空指標的值為多少。
```c
#define offsetof(st, m) ((size_t)&(((st *)0)->m))
```
```c
#define offsetof(st, m) ((size_t)((char *)&((st *)0)->m - (char *)0))
```
3. ALIGNOF 可以用 offsetof 來實作,這題 ALIGNOF 實作的前半 `(char *)(&((struct { char c; t _h; } *)0)->M)` 和 offsetof 一樣作用,後半減掉一個 X,X = 0。
ALIGNOF 改用 offsetof 實作:
```c
#define ALIGNOF(t) offset(struct { char c; t _h; }, M)
```
4. 有發現 struct 的第一個成員放 char 可能是因為它做 alignment 的大小是最小的(1 byte),如果後面 t 型別變數 `_h` 需要比較大的 alignment 的話,那 `char c` 也需要 padding 到跟 `_h` 一樣的 alignment size,所以才會去算 `char c` 的大小(指定成員 `_h` 和 struct 起始位置的偏移量),所以這裡的 M 為 `_h`,即可得到 t 型別 `_h` alignment 的大小。
因此 `M` = `_h`, `X` =`0`
### 在 Linux 核心原始程式碼中找出 `__alignof__` 的使用案例 2 則
> 針對其場景進行解說
#### 第一則:
在 `linux/include/linux/kstrtox.h` 的[第 30 行](https://github.com/torvalds/linux/blob/35e43538af8fd2cb39d58caca1134a87db173f75/include/linux/kstrtox.h#L30):
```c
static inline int __must_check kstrtoul(const char *s, unsigned int base, unsigned long *res)
{
/*
* We want to shortcut function call, but
* __builtin_types_compatible_p(unsigned long, unsigned long long) = 0.
*/
if (sizeof(unsigned long) == sizeof(unsigned long long) &&
__alignof__(unsigned long) == __alignof__(unsigned long long))
return kstrtoull(s, base, (unsigned long long *)res);
else
return _kstrtoul(s, base, res);
}
```
kstrtoul 是將一個字串轉成 unsigned long,實作有用到 `__alignof__` 來判斷 `unsigned long` 和 `unsigned long long` 的 alignment 大小是否一樣。可能是要確保在該 data model 下 `unsigned long` 和 `unsigned long long` 的大小是一樣的,例如在 LP64 下,就都是 64 bit。
#### 第二則:
在 `linux/crypto/aegis.h` 的[第 18 行](https://github.com/torvalds/linux/blob/68453767131a5deec1e8f9ac92a9042f929e585d/crypto/aegis.h#L18):
```c
union aegis_block {
__le64 words64[AEGIS_BLOCK_SIZE / sizeof(__le64)];
__le32 words32[AEGIS_BLOCK_SIZE / sizeof(__le32)];
u8 bytes[AEGIS_BLOCK_SIZE];
};
struct aegis_state;
extern int aegis128_have_aes_insn;
#define AEGIS_BLOCK_ALIGN (__alignof__(union aegis_block))
```
有一個巨集 `AEGIS_BLOCK_ALIGN` 會取得 union `aegis_block` 的 alignment 大小。
### 在 Linux 核心原始程式碼找出 `ALIGN`, `ALIGN_DOWN`, `ALIGN_UP` 等巨集
> 探討其實作機制和用途,並舉例探討 (可和上述第二點的案例重複)
+ 在 `linux/tools/testing/selftests/vm/pkey-helpers.h` 的[第 180 行](https://github.com/torvalds/linux/blob/bf4eebf8cfa2cd50e20b7321dfb3effdcdc6e909/tools/testing/selftests/vm/pkey-helpers.h#L180)有 `ALIGN_DOWN` 和 `ALIGN_UP` 的巨集:
```c
#define ALIGN_UP(x, align_to) (((x) + ((align_to)-1)) & ~((align_to)-1))
#define ALIGN_DOWN(x, align_to) ((x) & ~((align_to)-1))
```
+ 在 `linux/tools/testing/selftests/vm/hmm-tests.c` 的[第 54 行](https://github.com/torvalds/linux/blob/68453767131a5deec1e8f9ac92a9042f929e585d/tools/testing/selftests/vm/hmm-tests.c#L54)有 `ALIGN` 的巨集:
```c
#define ALIGN(x, a) (((x) + (a - 1)) & (~((a) - 1)))
```
#### ALIGN_UP
ALIGN_UP 的參數 `x` 是原本的大小,align_to 是要用多少 bit 為倍數來對齊。
左段的程式 `((x) + ((align_to) - 1)) ` 是將 `x` 加上 `align_to` 的大小再減去 1,讓原本大小不足 `align_to` 的部分,能向上進 1 位到 `align_to`,例如:`x` = 5, `align_to` = 32,為了讓 5 能夠對齊變成 32 位元,就讓 5 + (32 - 1) = 36,讓值超過 32。
右段的程式 `~((align_to)-1))` 是要去除不足 align_to 的餘數,因為最後算出來的值應該是 align_to 的倍數。延續上面的例子來解釋,就是 ~(32 - 1) 用二進位來表達 1110 0000,再與 36 做 AND 運算,會將不足 32 的部分都 clear 掉,就從 5 向上對齊成 32 了。
#### ALIGN_DOWN
向下對齊不需要加上`(align_to) - 1` 來補足不到 `align_to` 的餘數,而是直接捨棄不到 `align_to` 的部分。同樣用 `~((align_to)-1))` 去除不足 `align_to` 的餘數,最後算出來的值也是 `align_to` 的倍數。
#### ALIGN
在 linux 核心程式碼找到的 ALIGN 巨集看起來和向下對齊一樣。
## 測驗 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;
}
```
1. length 代表著要印出字串的長度,可被 3 整除要輸出 Fizz(4 個字元),可被 5 整除要輸出 Buzz(4 個字元),同時可被 3 和 5 整除要輸出 FizzBuzz(8 個字元),其餘的輸出數字本身(length 預設是 2 個字元)。
2. `unsigned int length = (2 << KK1) << KK2;` 會先將 2(0010)左移 KK1 位,再左移 KK2 位。當 KK1 為 div3,KK2 為 div5 時,就可以根據 div3 和 div5 為 1(可被整除)或 0(不可被整除),來調整 length,如下表。
| | 左移位數 | length |
| -------- | -------- | -------- |
| 只被 3 整除 | 一位 | 0100 = 4 |
| 只被 5 整除 | 一位 | 0100 = 4 |
| 被 3 和 5 整除 | 兩位 | 1000 = 8 |
| 其餘 | 零位 | 0010 = 2 |
3. `strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (KK3)], length);` 的 9 要改成 8,因為一個數在不能被 3 和 5 整除的時候,是要將 "%u" 複製到 fmt 裡面,而 % 的位置在第 8 位。
4. 當 div5 為 1 時(被 5 整除),8 右移一位為 4,從第四位開始複製 4 個字元長度,剛好是 Buzz。所以接下來要處理只被 3 整除和被 15 整除的情況,都需要從第 0 位開始複製,因為 div5 的關係,這時候的值可能是 8(1000)或 4(0100),要保證能變成 0 的話需要右移 4 位,因此 KK3 為 div3 << 2,就可以右移 4 位了。
因此 `KK1` = `div3`, `KK2` = `div5`, `KK3` = `div3 << 2`
### 評估 naive.c 和 bitwise.c 效能落差
> 避免 stream I/O 帶來的影響,可將 printf 更換為 sprintf
### 改寫 bitwise.c 程式碼,試圖運用更少的指令來實作出 branchless
> 分析 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
### 提出 throughput (吞吐量) 更高的 Fizzbuzz 實作
> 研讀 The Fastest FizzBuzz Implementation 及其 Hacker News 討論,提出 throughput (吞吐量) 更高的 Fizzbuzz 實作
### 解析 Linux 核心原始程式碼 kernel/time/timekeeping.c
> 解析 Linux 核心原始程式碼 kernel/time/timekeeping.c 裡頭涉及到除法運算的機制,探討其快速除法的實作 (注意: 你可能要對照研讀 kernel/time/ 目錄的標頭檔和程式碼)
> + 過程中,你可能會發現可貢獻到 Linux 核心的空間,請充分討論