# 2023q1 Homework2 (quiz2)
###### tags: `linux2023`
contributed by < `Paintako` >
## 測驗一
### 說明
考慮 next_pow2 可針對給定無號 64 位元數值 x,找出最接近且大於等於 2 的冪的值
原始程式碼是使用了 `binary search` 的概念去找出最接近且大於等於 2 的冪的值
而改寫後的版本:
首先,給的數值範圍是 `64 bit, 且 unsigned`
只要把給的數值的 `MSB` 右邊全部補 1,再 + 1 , 這樣即可得出最接近且大於等於 2 的冪的值
例如:
數值為 `00001001`,MSB 之後補 1 後變成 `00001111`,最後 + 1 得 `00010000`
但是,如果情況變成原本的數值等於 2 的冪的值,則會得到比正確答案還大的數值
例如:
數值為 `00001000`,MSB 之後補 1 後變成 `00001111`,最後 + 1 得 `00010000`
所以針對以上情況,要結束前需要對原本的數值作 `XOR` 確保原本數值是 2 的冪的值的情形
例如:
數值為 `00001000`,MSB 之後補 1 後變成 `00001111`
和原本的數值做 `XOR`, ` 00001000 ^ 00001111` 所以答案變成 `00000111`,故 +1 仍然是正確答案!
經 [paulpeng](https://hackmd.io/@normal) 提醒後,仍然需要一個 `if` 來判斷原本數值是否 2 的冪的值的情形,依舊會有一個 branch 的 overhead
程式碼應該改寫成:
```c
uint64_t next_pow2(uint64_t x)
{
uint64_t y = x;
y |= y >> 1;
y |= y >> 1;
y |= y >> 1;
y |= y >> 1;
y |= y >> 1;
y |= y >> 1;
y |= y >> 1;
y |= y >> 8;
y |= y >> 16;
y |= y >> 32;
return x%y==0?y+1:(y^x)+1; // if x%y==0 return y+1, else return (y^x)+1
}
```
為了解決這個 branch 的 overhead,可以把原本數值 -1 使得原本等於 2 的冪的數值不等於 2 的冪,解決了這個不必要的 branch
```c
uint64_t next_pow2(uint64_t x)
{
uint64_t y = x-1;
y |= y >> 1;
y |= y >> 1;
y |= y >> 1;
y |= y >> 1;
y |= y >> 1;
y |= y >> 1;
y |= y >> 1;
y |= y >> 8;
y |= y >> 16;
y |= y >> 32;
return y + 1 ;
}
```
延伸問題:
使用 ` __builtin_clzl ` 改寫
此函式返回 `leading 0-bits in x` , 得知 `leading 0` 的數量後, 將 `64 - leading 0-bits數` 算出後再對1做算數位移,即可得此數最接近且大於等於 2 的冪的值
但同樣需要考慮此數原本就是 2 的冪的情況,如果算出的大於等於 2 的冪的值 % 原本的值 =0 的話代表此數原本就是 2 的冪
```c
uint64_t func(uint64_t x)
{
int lead_zero = __builtin_clz(x);
int cmp = 1<<(64-lead_zero);
if ((cmp%x) != 0 )
return cmp;
return x;
}
```
在 Linux 核心原始程式碼找出類似的使用案例並解釋
使用 `$ grep -r "<<" ./include/linux/ ` 來找出相似程式碼
有一個看起來也是跟數字處理相關的案例 `./include/linux/log2.h`
[#define const_ilog2(n)](https://github.com/torvalds/linux/blob/master/include/linux/log2.h#L77)
:::warning
這是一個定義了常數log2的C程式碼的巨集(macro)。對於給定的正整數 n,該 define 返回最大的 k,使得 2^k ≤ n,即 2 的 k 次冪不超過 n。
這個 define 使用了GCC inline function __builtin_constant_p(n) ,該函數在編譯時會計算是否為常數,如果是,則返回 true,否則返回 false。 如果 n 是一個常量,那麼 define 使用二進制運算來快速找到最大的 k。 如果 n 不是常量,則使用逐位檢查的方法來找到 k。
From Chatgpt
:::
## 測驗二
原本的程式碼中已經有提示了,如果這個數字是 2 的冪的話,則 `len++`, 此 len 表示 `ans` 的位移量
例如:
| n| 2 的冪 | len | rst |
| ----- | ------ | --- | --- |
|1 | yes | 1 | 1 |
|2 | yes | 2 | 101 |
|3 | no | 2 | 10111 |
|4 | yes | 3 | 101110100 |
開發過程中,原本的程式碼如下:
```c
if (!(i & (i-1)))
len++;
ans = (i | (ans<<(len)+1)) % M;
```
其中 `i & (i-1)` 用於判斷此數是否是 2 的冪
但是這是錯的! 因為忘記了其實 1 也是 2 的冪($2^0$),原本的寫法中忘記了這點才 + 1 的,故正確的版本如下
```c
if (!(i & (i-1)))
len++;
ans = (i | (ans<<(len))) % M;
```
嘗試使用 `__builtin_{clz,ctz,ffs}` 改寫
把 `(i & (i-1)` 改成 `(__builtin_ffs(i) == (64 - __builtin_clzl(i))` 即可
但是這樣改寫後會變得比較快嗎? Todo: 使用 perf 來判斷 cycle數的增減情況
## 測驗三
SIMD: `Single Instruction Multi Data`
指在同個 `clock cycle` 中,同時處理多個向量
例如:
沒有支援 SIMD 執行 $B*=2$, 其中 B 是 1*n 的向量
| Vector B | Clock Cycle 1 | Clock Cycle 2 | Clock Cycle 3 |
| --------- | ------------- | ------------- | ------------- |
| [1,2,3,4] | [2,2,3,4] | [2,4,3,4] | [2,4,6,4] |
有支援 SIMD:
| Vector B | Clock Cycle 1 |
| -------- | ------------- |
| [1,2,3,4] | [2,4,6,8] |
而 SWAR 表示在 `同一個 Register 中做 SIMD`
---
在筆記中的程式碼中,發現了一些可能是白作工的部份
```c
static inline uint64_t bit_compound(uint32_t x, uint32_t y) {
return ((0L + x) << 32) | ((y + 0L) & (-1L >> 32));
}
```
這裡的 `(y + 0L) & (-1L >> 32)`
`y + 0L` 還算好理解,要把 y 擴充成 64 bit
但是 `-1L >> 32` 這裡就匪夷所思了,即便 -1L 寫成二進位是 64個 1,對這 64 個 1 做 32次算數位移仍然是 64個 1,而且跟 `y + 0L` 做 and 也是白做工,因為擴充完 32 bit 的 `y+0L` 前半部份也全部都是 0, 0 跟任何東西做 and 都是 0 ,所以應該可以把程式碼改寫成:
```c
static inline uint64_t bit_compound(uint32_t x, uint32_t y) {
return ((0L + x) << 32) | (y + 0L);
}
```
---
參考以下程式:
```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;
}
```
這段程式在做的事情是: 找出一個字串中所有合法的 `UTF-8` 字元組
其中, `magic number` 為何是 -65?
因為對應到 ASCII 的規則, 除了首位元組外,後續位元組皆是以 `10xx xxxx` 的形式存在
故只要某一個數字大於 `1011 1111` 的話,那此數必不會是後續位元組,~~而是一個新的 ASCII !~~ ( 不是新的 ASCII, ASCII 的 MSB 皆是 0)
---
使用 `SWAR` 改寫程式碼:
參考小考中的提示,如果某個 byte 可以寫成 `10xx xxxx` 的形式,那此 byte 必是某 ASCII 的後續位元組,並且可以將這種表達方式寫成 `not bit6 and bit7`,也就是第七個 bit 必 1,第六個 bit 必 0,找出所有這種特殊 pattern 的位元組
只保留把每一個 byte 中的 6、7 bit , 其餘 bit 全部設為 0 , 再使用 `__builtin_popcount` 即可得答案
也可參考老師上課的說法,"找到 `10` 交接處"
上述方式用 `SWAR` 的方式呈現出來
首先, `qwords` 代表一次讀取的 byte 數,這裡指定 `uint64`,代表一次讀取 `8 byte`,用意是配合 64 bit 系統,當 pointer ++ 時,移動的步伐等於 64 bit,也就是每一次比較的 Byte 數為 8
:::warning
Note: 即便使用者的系統是 32 bit 的系統 , 仍存在 64 bit 的操作 ( 例如乘法 )
:::
這邊 `len` 沒有更詳細的說明,只能用推測的,因為是改寫自原本的題目,從下面程式碼來推測,是整個字串的字元組( Bytes ) 大小
len >> 3 的目的在於把無法湊到 8 個 byte的去除掉(因為無法對它做 64 bit 的運算),故減去,所以 `*end` 代表了傳入的字串有多少的 Byte!
```c
const uint64_t *end = qword + len >> 3;
```
以上程式碼,根據 C語言規格書 ( from ChatGpt ),得知符號的結合律優先度,故會先做 `加法` 再做 `>>`
`>> 3` 代表以 8 個 byte為單位
![](https://i.imgur.com/yYIznlY.png)
每一次都對 64 bit 做 SWAR,結束後統計 1 的個數即可得知這 8 Byte 中包含幾個後續位元組
當傳入的字串長度不是 Byte 的倍數時 ( i.e. 字串 / 8 != 0 ),則需要考慮剩餘的 Byte 中是否含後續位元組
```c
count = (1 << 3) * (len / 8) - count;
```
首先, `len/8` 代表 "可以完整湊出 8 Bytes的個數",乘上 8 bit 即可得完整的位元組數,再減去 `continuation byte` 即可得目前合法的 `UTF-8` 數
```c
count += (len & 7) ? count_utf8((const char *) end, len & 7) : 0;
```
這段 `len & 7` 很好理解,原本剩下的 Byte 數量不足 8,所以跟 7 做 &,可以得到剩餘的 Byte數
而後面的 `if/ else` 也很好理解,將剩下的範圍丟入 `count_utf8` 可得後續位元組數量
## 測驗四
:::warning
Note:
x <<= 1 可以寫成 x = x<<1
:::
參考: [TimChen731](https://hackmd.io/@TimChen731/quiz2)
程式碼要的是: 給定一個 pattern ,不斷向右移的過程中如果有符合該 pattern 就 return true
該 pattern 是只有MSB是 1 其餘皆是 0
字串要符合該 pattern ,並且不斷向左移動時不跳出 for 迴圈的話,那此字串必須符合:
MSB是1 且 1 後續不能有 0
先對 x 做 `not`,再 +1 的話,則跟原本的字串做 `xor` 後原本字串中最右邊的 1 會消失
例如:
| 原本 | 11111000 |
| ---------------- | -------- |
| not | 00000111 |
| not+1 | 00001000 |
| 原本 xor (not+1) | 11110000 |
在和原本的字串做比較,如果比較小的話代表它符合此 pattern
| 原本 | 11111010 |
| ---------------- | -------- |
| not | 00000101 |
| not+1 | 00001010 |
| 原本 xor (not+1) | 11110010 |
在和原本的字串做比較,如果比較大的話代表它不符合此 pattern