---
tags: linux kernel
---
# 2023q1 Homework2 (quiz2)
contributed by < [hankTaro](https://github.com/hankTaro/quiz2/tree/master) >
## 測驗一
### 說明與改寫
考慮 next_pow2 可針對給定無號 64 位元數值 x,找出最接近且大於等於 2 的冪的值
原始程式碼為:
```c
uint64_t next_pow2(uint64_t x)
{
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> AAAA;// AAAA = 8
x |= x >> 16;
x |= x >> BBBB;// BBBB = 32
return CCCC;// x + 1
}
```
由二進位個概念可知,2 的冪必為最高位元後的所有位元皆為 0
所以其程式碼的作用是將最高位元數下的各個 bit 改為 1 ,最後在 `x + 1` 使其進位,便可以得到大於其值最接近的2的冪
可將上方的程式碼簡化成
```c
uint64_t next_pow2(uint64_t x)
{
//x // x = 0010000000000000
x |= x >> 1; // x = 0011000000000000
x |= x >> 2; // x = 0011110000000000
x |= x >> 4; // x = 0011111111000000
x |= x >> 8; // x = 0011111111111111
x |= x >> 16;
x |= x >> 32;
return x + 1;// x = 0100000000000000 = 2^14
}
```
而以上函式還可以使用 `__builtin_clz(x)` 簡化
> `int __builtin_clz (unsigned int x)`
Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
此函式可以求出 leading 1 前的 0 個數
所以使用此函式的程式碼如下:
```c
uint64_t next_pow2(uint64_t x)
{
int n = __builtin_clz(x);
return 1 << (64 - n);
}
```
但上述的程式碼有兩個問題
1. 當 x 為 0 時,n 會未定義
> int \__builtin_clz (unsigned int x):
Returns the number of leading 0-bits in x, starting at the most significant bit position. ==If x is 0, the result is undefined.==
2. 當輸入的值為 $2^n$ 時,會求得 $2^{n+1}$ 而非 $2^n$
> 要求:
> 找出最接近且大於等於 2 的冪的值
以下為更改過後的程式碼:
```c
uint64_t next_pow2(uint64_t x)
{
if (!x)
return 1;
int n = __builtin_clz(x);
// 判別 x 在二進位中是否只有一個位元為 1
if (n + __builtin_ctz(x) == 63)
return x;
return 1 << (64 - n);
}
```
### 編譯器能否產生 clz 的 x86 指令?
可以發現 `bsr` 指令,編譯器有產生對應指令
> [endbr64](https://stackoverflow.com/questions/56905811/what-does-the-endbr64-instruction-actually-do)
```
next_pow2:
.LFB23:
.cfi_startproc
endbr64
movl $1, %eax
testq %rdi, %rdi
je .L1
bsrl %edi, %edi
movl $64, %ecx
xorl $31, %edi
subl %edi, %ecx
sall %cl, %eax
cltq
.L1:
ret
.cfi_endproc
```
### 在 Linux 核心原始程式碼找出類似的使用案例並解釋
此程式碼用於求出 [Hamming Weight](https://en.wikipedia.org/wiki/Hamming_weight)
```c
//types and constants used in the functions below
//uint64_t is an unsigned 64-bit integer variable type (defined in C99 version of C language)
const uint64_t m1 = 0x5555555555555555; //binary: 0101...
const uint64_t m2 = 0x3333333333333333; //binary: 00110011..
const uint64_t m4 = 0x0f0f0f0f0f0f0f0f; //binary: 4 zeros, 4 ones ...
const uint64_t m8 = 0x00ff00ff00ff00ff; //binary: 8 zeros, 8 ones ...
const uint64_t m16 = 0x0000ffff0000ffff; //binary: 16 zeros, 16 ones ...
const uint64_t m32 = 0x00000000ffffffff; //binary: 32 zeros, 32 ones
const uint64_t h01 = 0x0101010101010101; //the sum of 256 to the power of 0,1,2,3...
//This is a naive implementation, shown for comparison,
//and to help in understanding the better functions.
//This algorithm uses 24 arithmetic operations (shift, add, and).
int popcount64a(uint64_t x)
{
x = (x & m1 ) + ((x >> 1) & m1 ); //put count of each 2 bits into those 2 bits
x = (x & m2 ) + ((x >> 2) & m2 ); //put count of each 4 bits into those 4 bits
x = (x & m4 ) + ((x >> 4) & m4 ); //put count of each 8 bits into those 8 bits
x = (x & m8 ) + ((x >> 8) & m8 ); //put count of each 16 bits into those 16 bits
x = (x & m16) + ((x >> 16) & m16); //put count of each 32 bits into those 32 bits
x = (x & m32) + ((x >> 32) & m32); //put count of each 64 bits into those 64 bits
return x;
}
```
其概念是先使用 `& 0x5555` 求出其每隔一位的 1 的數量,使用兩個 bit 保存,將兩者數量相加後,隨後用 `& 0x3333` 求出其每隔兩位的 1 的數量,使用四個 bit 保存,將兩者數量相加後,如此循環,直到最終無法再合併
```graphviz
digraph graphname {
// label="divide"
{
node[shape=none,label="x"]; i1
node[shape=none,label="a = (x >> 0) & 0x5555"]; i2
node[shape=none,label="b = (x >> 1) & 0x5555"]; i3
node[shape=none,label="c = a + b"]; i4
node[shape=none,label="d = (c >> 0) & 0x3333"]; i5
node[shape=none,label="e = (c >> 2) & 0x3333"]; i6
node[shape=none,label="f = d + e"]; i7
node[shape=none,label="",style=invis]; i
}
list_1[label="<f0>01|<f1>10|<f2>11|<f3>00|<f4>10|<f5>11|<f6>10|<f7>10",shape=record, fixedsize=false,width=6]
list_2[label="<f0>01|<f1>00|<f2>01|<f3>00|<f4>00|<f5>01|<f6>00|<f7>00",shape=record, fixedsize=false,width=6]
list_3[label="<f0>00|<f1>01|<f2>01|<f3>00|<f4>01|<f5>01|<f6>01|<f7>01",shape=record, fixedsize=false,width=6]
list_4[label="<f0>01|<f1>01|<f2>10|<f3>00|<f4>01|<f5>10|<f6>01|<f7>01",shape=record, fixedsize=false,width=6]
list_5[label="<f0>0001|<f1>0000|<f2>0010|<f3>0001",shape=record, fixedsize=false,width=6]
list_6[label="<f0>0001|<f1>0010|<f2>0001|<f3>0001",shape=record, fixedsize=false,width=6]
list_7[label="<f0>0011|<f1>0010|<f2>0011|<f3>0010",shape=record, fixedsize=false,width=6]
list_1:f0->list_2:f0[style=invis]
list_2:f0->list_3:f0[style=invis]
list_3:f0->list_4:f0[style=invis]
list_4:f0->list_5:f0[style=invis]
list_4:f7->list_5:f3[style=invis]
list_5:f0->list_6:f0[style=invis]
list_6:f0->list_7:f0[style=invis]
// list_6:f3->list_7:f1[style=invis]
i1->i2[style=invis]
i2->i3[style=invis]
i3->i4[style=invis]
i4->i5[style=invis]
i5->i6[style=invis]
i6->i7[style=invis]
}
```
而在 Liunx 核心中將其簡化並整理成以下程式碼
```c
unsigned int __sw_hweight16(unsigned int w)
{
unsigned int res = w - ((w >> 1) & 0x5555);
res = (res & 0x3333) + ((res >> 2) & 0x3333);
res = (res + (res >> 4)) & 0x0F0F;
return (res + (res >> 8)) & 0x00FF;
}
EXPORT_SYMBOL(__sw_hweight16);
```
:::warning
TODO: 搭配 Linux 核心的 `net/mac80211` 和 `net/wireless` 來解釋 Hamming Weight 的使用
:notes: jserv
:::
---
## 測驗二
### 說明
```c
int concatenatedBinary(int n)
{
const int M = 1e9 + 7;
int len = 0; /* the bit length to be shifted */
/* use long here as it potentially could overflow for int */
long ans = 0;
for (int i = 1; i <= n; i++) {
// removing the rightmost set bit
// e.g. 100100 -> 100000
// 000001 -> 000000
// 000000 -> 000000
// after removal, if it is 0, then it means it is power of 2
// as all power of 2 only contains 1 set bit
// if it is power of 2, we increase the bit length
if (!(DDDD))
len++;
ans = (i | (EEEE)) % M;
}
return ans;
}
```
此程式碼用於將 1~n 的十進位數值轉為二進位,並且將個別對應的二進位做串接,求出二進位字串 mod $10^9+7$ 的值
在 for 迴圈中要將 1~n 的值放入二進位字串,但因為不同數值 leading 1 的位置不同,所需 shift 的量也不同,所以這裡利用 `x & (x - 1) == 0` 進行判別,並用 `len` 紀錄 shift 所需的量
* C 語言中,`x & (x - 1) == 0` 的數學意義為
* power of two
* signed v.s. unsigned
在二進位中,每當增加至 2 的冪,leading 1 就會左移一格,因此每當值為 power of two 時,所需的 shift 量就要增加
```c
for (int i = 1; i <= n; i++) {
if (!(i & (i - 1) == 0))
len++;
ans = (i | (ans<<len)) % M;
}
```
### 嘗試使用 `__builtin_{clz,ctz,ffs}` 改寫
當 n 為 power of 2 時,MSB 到第一個 1 之前 0 的個數,加上 LSB 到第一個 1 之後 0 的個數,對剛好有 63 個 `0` ,也就是 `__builtin_clz(n) + __builtin_ctz(n) == 63`
```c
int concatenatedBinary(int n)
{
const int M = 1e9 + 7;
int len = 0; /* the bit length to be shifted */
/* use long here as it potentially could overflow for int */
long ans = 0;
for (int i = 1; i <= n; i++) {
if (__builtin_clz(n) + __builtin_ctz(n) == 63)
len++;
ans = (i | (ans<<len)) % M;
}
return ans;
}
```
### (待辦) 改進 mod $10^9+7$的運算
由於當 len 大於一定程度後,會不停的對 `ans` 乘以 2^len^
>想法:
>如果 N 是 $2^4-1$ 的話,那 1 將會被左移$4\times2^3+3\times2^2+2\times2^1$個bit
:::warning
參照 [Barrett reduction](https://en.wikipedia.org/wiki/Barrett_reduction)
:notes: jserv
:::
---
## 測驗三
### 說明
> continuation bytes:
> Continuation bytes have two most significant bits set to 10.
> 在 UTF-8 中,字元可由 1, 2, 3, 4 個位元組構成,由 leading byte 與 continuation bytes 構成
> 其中 110x.xxxx, 1110.xxxx, 1111.0xxx 就是 leading byte
> 而 10xx.xxxx 全是 continuation bytes
>UTF-8:
>有由單個 byte 組成的字元集(與 ASCII 一致)
>以及2,3,4 byte 組成的字元集
>用於涵蓋世界上的多種字元
> size_t:
> 在32位架構中定義為:typedef unsigned int size_t;
> 在64位架構中被定義為:typedef unsigned long size_t;
| ASCII | 0xxx.xxxx |
| ------- |:--------------------------------------- |
| 2 bytes | 110x.xxxx 10xx.xxxx |
| 3 bytes | 1110.xxxx 10xx.xxxx 10xx.xxxx |
| 4 bytes | 1111.0xxx 10xx.xxxx 10xx.xxxx 10xx.xxxx |
以下的程式是用於計算 continuation bytes 的量
```c
#include <stddef.h>
#include <stdint.h>
size_t count_utf8(const char *buf, size_t len)
{
const int8_t *p = (const int8_t *) buf;
size_t counter = 0;
for (size_t i = 0; i < len; i++) {
/* -65 is 0b10111111, anything larger in two-complement's should start
* new code point.
*/
if (p[i] > -65)
counter++;
}
return counter;
}
```
> 這裡的 `count` 所代表的是,不包含 continuation bytes 的字元數量
這裡是藉由有號數的架構規則,使得所有小於 -65 的數,都不是以 `10` 作為開頭,便以此作為判斷
將以上功能程式使用 [SWAR](https://en.wikipedia.org/wiki/SWAR) 實作,增加系統效率,程式碼如下:
```c=1
size_t swar_count_utf8(const char *buf, size_t len)
{
const uint64_t *qword = (const uint64_t *) buf;
const uint64_t *end = qword + len >> 3;
size_t count = 0;
for (; qword != end; qword++) {
const uint64_t t0 = *qword;
const uint64_t t1 = ~t0;
const uint64_t t2 = t1 & 0x04040404040404040llu;
const uint64_t t3 = t2 + t2;
const uint64_t t4 = t0 & t3;
count += __builtin_popcountll(t4);
}
count = (1 << AAAA) * (len / 8) - count;
count += (len & BBBB) ? count_utf8((const char *) end, len & CCCC) : 0;
return count;
}
```
> 在迴圈中 `count` 所代表的是,continuation bytes 的字元數量,要計算 continuation bytes 的數量的原因是,要將其數量從總 byte 中減去,以求得一字串的文字數量
> 而在 16 行中左側的 `count` ,是總字串長度減去 continuation bytes 的字元數量
由於系統是 64 位元,可以單次處理 64位元 ,所以單次就一次檢測 64位元 ,將效率最大化
在迴圈中利用 bitmask 技巧, `~bit#6 and bit#7` ,使最高兩位為 `10` 的 byte 才會得到 `0b10000000` 的結果,其餘都為 `0b0`,接著再使用 `popcount()` 獲取 64 bits 中 non-zero bit 的數量,便可求得 continuation bytes 的數量
```c
const uint64_t *qword = (const uint64_t *) buf;
const uint64_t *end = qword + len >> 3;
```
由一開始宣告的資料型態和內容得知, `qword` 一次存取 8 bytes , 第二行求出總字串中共有幾個
8 bytes (`len >> 3` 便是減去無法被 `0b1000 = 8` 整除的 bytes),將可以完整放入 64 bits 中的 bytes 用迴圈中的 bitmask 方法求 continuation bytes 數量,至於無法被整除的 bytes 最後在統一處理並用 `count_utf8` 計算 continuation bytes 數量
雖然程式中並未寫出 `len` 所代表的涵義,但在 `count_utf8` 函式中的 `for (size_t i = 0; i < len; i++)` 可得知 `len` 所代表的是總字串長度,也就是傳入的字串串列中,總共有幾個 bytes
```c
count = (1 << 3) * (len / 8) - count;
count += (len & 7) ? count_utf8((const char *) end, len & 7) : 0;
return count;
```
最後需要確認未能被 8 bytes 整除的 bytes 中有多少的 continuation bytes
`count = (1 << 3) * (len / 8) - count;` 的作用是計算出,可以被整除的部分中的字元數,其中 `(1 << 3) * (len / 8)` 的作用是將最右側的 3 個 bits 設為 0 ,以求得總字串長度中可以被 8 byte 整除的 bytes 數量,再將其減去 continuation bytes 數量
第二行則是將無法被整除的部分,使用 `count_utf8` 求出去除 continuation bytes 的字元數,最後將其相加以得到最終結果
### 在 Linux 核心原始程式碼找出 UTF-8 和 Unicode 相關字串處理的程式碼,探討其原理
在 [`unicode.c`](https://github.com/ilbers/linux/blob/747ae0a96f1a78b35c5a3d93ad37a16655e16340/fs/udf/unicode.c) 中,`udf_uni2char_utf8` 是將 [Unicode](https://en.wikipedia.org/wiki/Unicode) 轉換為 [UTF-8](https://en.wikipedia.org/wiki/UTF-8) 的函式,由於 [Unicode](https://en.wikipedia.org/wiki/Unicode) 使用固定的多位元組來保存單個字元,在保存可以用單一位元組表示的字元時,會有空間浪費,所以 [UTF-8](https://en.wikipedia.org/wiki/UTF-8) 用於解決這個問題
<!-- [OSTA Compressed Unicode](https://dicom.nema.org/medical/dicom/current/output/chtml/part12/chapter_p.html) -->
<!-- 根據 [uds.txt](https://github.com/ilbers/linux/blob/747ae0a96f1a78b35c5a3d93ad37a16655e16340/Documentation/filesystems/udf.txt) 得知 -->
根據 Unicode 的編碼範圍,`0~07F` 的編碼需要至少 7 bits 保存,`080~07FF` 則只少需要 11 bits 保存,使用以上規則,依序對應到 UTF8 的 1~4 bytes 構成
![](https://i.imgur.com/BlPZL6P.png)
得知以上概念便可實作
```c
static int udf_uni2char_utf8(wchar_t uni,
unsigned char *out,
int boundlen)
{
int u_len = 0;
if (uni >> (boundlen * 4) != 0)
return -ENAMETOOLONG;
if (boundlen <= 0)
return -ENAMETOOLONG;
if (uni < 0x80) {
out[u_len++] = (unsigned char)uni;
} else if (uni < 0x800) {
if (boundlen < 2)
return -ENAMETOOLONG;
out[u_len++] = (unsigned char)(0xc0 | (uni >> 6));
out[u_len++] = (unsigned char)(0x80 | (uni & 0x3f));
} else {
if (boundlen < 3)
return -ENAMETOOLONG;
out[u_len++] = (unsigned char)(0xe0 | (uni >> 12));
out[u_len++] = (unsigned char)(0x80 | ((uni >> 6) & 0x3f));
out[u_len++] = (unsigned char)(0x80 | (uni & 0x3f));
}
return u_len;
}
```
`boundlen` 代表可接受的 byte 上限,若此字元轉成 UTF8 後所需的位元組超過 `boundlen` 則會報錯
程式先判斷 Unicode 編碼範圍,再依照不同的編碼範圍轉換至 UTF8,並將結果存入 `*out` 中
```c
else {
if (boundlen < 3)
return -ENAMETOOLONG;
out[u_len++] = (unsigned char)(0xe0 | (uni >> 12));
out[u_len++] = (unsigned char)(0x80 | ((uni >> 6) & 0x3f));
out[u_len++] = (unsigned char)(0x80 | (uni & 0x3f));
}
```
:::warning
1. 調整程式碼縮排,使用 `lab0-c` 指定的風格。
2. 指出上述程式碼可改進之處,避免「舉燭」。
:notes: jserv
:::
:::info
【原文】
郢人有遺相國書者,夜書,火不明,因謂持燭者曰:“舉燭。”雲而過書舉燭。舉燭,非書意也,燕相受書而說之,曰:“舉燭者,尚明也,尚明也者,舉賢而任之。”燕相白王,王大說,國以治。治則治矣,非書意也。今世舉學者多似此類。
《韓非子·外儲說左上》
這則寓言尖鋭地諷刺了一些人隨意穿鑿附會的治學態度。它說明,做學問不能斷章取義,胡亂解釋前人的片言隻語,從中尋求什麼微言大義。同時也告誡人們,對待人事,要具體問題具體分析,不能主觀意斷,以免造成不良的後果。
:::
其中先將前 4 個 bit 利用 `0xe0 | (uni >> 12)` 放入第一個 byte 的後4位,接著使用 bitmask 將格式完善成 `1110xxxx`,便可完成第一個 byte,接下來是第二個 byte ,使用 `((uni >> 6) & 0x3f))` 去除後 6 個位元,再使用 bitmask 保留6個右側位元,完善成 `10xxxxxx`,最後將後 6 個位元放入 `10xxxxxx` 即可,其餘範圍的 unicode 也依循相同邏輯處理
### 改進
上述程式碼依舊有改進的空間,由於有過多的分支,並且無法支援最新的 unicode ,也就是 21 位元的版本
<!-- 但目前沒有想出可以大量減少分支的方法,盡量減少判斷次數 -->
已知最終 bytes 數會由輸入的最高位的 1-bit 決定,`7 bit` `11 bit` `16 bit` 分別對應輸出 `1 bytes` `2 bytes` `3 bytes`
換而言之,輸出若是 `1 bytes` `2 bytes` `3 bytes`,其對應的 `uni` 右移 8, 12, 17,值必定為 0,所以只要找出映射規則,就可以事先判斷好是否會超過 `boundlen`
使用 $f(x)=ax^2+bx+c$ 帶入,求得函數 $f(x)=\frac{x^2}{2}+\frac{5x}{2}+5$,將其寫入判斷式便可實現
但以上運算有次方與除法運算,可使用查表取代運算,以節省計算時間
<!-- 依據 [C Operator Precedence](http://en.cppreference.com/w/c/language/operator_precedence),`&&` (Logical AND) operator 的運算規則為左值優先,只要左值不成立,右值都不會執行,這裡便是使用此方法減少判別次數,並減少重複代碼出現次數 -->
```c
static const int fuction_table_4[4] = {0, 8, 12, 17};
static int udf_uni2char_utf8(wchar_t uni,
unsigned char *out,
int boundlen)
{
int u_len = 0;
if (uni >> fuction_table_4[4])
return -ENAMETOOLONG;
if (uni < 0x80) {
out[u_len++] = (unsigned char)uni;
} else if (uni < 0x800) {
out[u_len++] = (unsigned char)(0xc0 | (uni >> 6));
out[u_len++] = (unsigned char)(0x80 | (uni & 0x3f));
} else {
out[u_len++] = (unsigned char)(0xe0 | (uni >> 12));
out[u_len++] = (unsigned char)(0x80 | ((uni >> 6) & 0x3f));
out[u_len++] = (unsigned char)(0x80 | (uni & 0x3f));
}
return u_len;
}
```
---
## 測驗四
以下程式碼可判定 16 位元無號整數是否符合特定樣式 (pattern):
```c
#include <stdint.h>
#include <stdbool.h>
bool is_pattern(uint16_t x)
{
if (!x)
return 0;
for (; x > 0; x <<= 1) {
if (!(x & 0x8000))
return false;
}
return true;
}
```
觀察以上代碼可得知,MSB是否為 1 ,並且 MSB 與最低位的 1 之間是否有 0 是判別的關鍵
由於是無號數,若想脫離迴圈就必須 `x = 0b0` ,但若在 MSB 為 0 ,或是 MSB 到最低位的 1 之間有 0 的話,都會 `return false`
所以符合條件的無號整數必定是以下類型
```
1000000000000000
1100000000000000
1110000000000000
.
.
.
1111111111111111
```
由於上方的程式碼處理較大位元的數值時,迴圈次數也會增加,所以可以用以下程式碼改寫:
```c
bool is_pattern(uint16_t x)
{
const uint16_t n = ~x + 1;
return (n ^ x) < x;
}
```
先觀察符合條件的無號整數的共通點:
> `1` 必定連續
> `0` 必定由最低位往高位延伸
便可求出
> `~x` 必定為 `0b0111` 的型態
> `~x + 1` 會進位
> `~x + 1` 必定只會有一個 `1`
換句話說,進行 `~x + 1` 的操作後,就會取得 `x` 最右側的 `1`
這時再進行 `(~x + 1) ^ x` 便等於減去自己最右側的 1 ,這項操作若若用在 `~x + 1` 不只有一個 `1` 的值上時,`XOR` 和必定會使值變大,因為 `~x + 1` 的進位會停止在最靠近LSB的 `1` 上
:::info
為了稱呼方便,往後把【最靠近LSB的 `1`】 稱為 **key bit**
:::
```
x = 0b00101110
^
這裡
~x + 1 = 0b11010010
```
由於進位無法改變 key bit 後的值,這樣 key bit 後的值,或多或少都會因為 `XOR` 由 `0` 變 `1` ,使得值較原本的值要大,以便我們使用 `(n ^ x) < x` 來判斷回傳值
:::info
雖然理解本程式碼的意義,但對於如何找出 bitmask 的方法依舊困惑,希望理解的人可以留言告知,感謝:pray:
:::
### (待辦)在 Linux 核心原始程式碼找出上述 bitmask 及產生器,探討應用範疇
[Data Structures in the Linux Kernel](https://0xax.gitbooks.io/linux-insides/content/DataStructures/linux-datastructures-3.html)
___