# 2020q3 Homework (quiz2)
contributed by < `hankluo6` >
### `測驗 1`
```cpp
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
bool is_ascii(const char str[], size_t size)
{
if (size == 0)
return false;
int i = 0;
while ((i + 8) <= size) {
uint64_t payload;
memcpy(&payload, str + i, 8);
if (payload & MMM)
return false;
i += 8;
}
while (i < size) {
if (str[i] & 0x80)
return false;
i++;
}
return true;
}
```
#### `MMM`
- [x] `(d)` `0x8080808080808080`
#### 說明
`(i + 8) <= size` 用來檢查現在字元數量 `i` 有沒有超過要檢測的字串長度 `size`,並將要檢測的記憶體位置 `str + i` 後面 8 個 bytes 的值複製至 `payload` 中,下方回傳 `false` 可得知 `payload` 用來檢驗是否為 128 內的 ASCII 碼。
從 `uint64_t` 可知 `payload` 為 8 bytes 位元之變數,每個 bytes 代表一個字元 `char`,而每個字元要檢查其值是否在 128 以內,以 16 進為表示就是為 0x00 ~ 0x79。如果該字元與 0x80 做 AND 運算不為 0 的話,表示該字元之 ASCII 碼為 128 以上。
可將上述字元操作擴充成 8 個字元同時檢測,故答案為 `(d)`。
#### 延伸問題
##### 1. 解釋上述程式的運作原理,特別是為何用到 memcpy 呢?提示: 與 data alignment 有關,詳閱 [C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory)
* 使用 `memcpy` 可以讓我們一次操作 8 bytes 的資料,如不使用 `memcpy` 而是直接將字串元素與 `(d)` 做 AND 運算,如 `*(str + i) & 0x8080808080808080`,會被解釋為該字元與 `(d)` 的 AND 運算。
* 使用強制轉型可解決上述問題,將 `payload` 改寫成`uint64 *payload = (uint64_t *) (str + i);` 並做 AND 運算,也許你一樣會得到正確的結果,但其實這種結果並非每次都正確。
* 此種記憶體轉型有可能會造成 [Unaligned data access](https://www.kernel.org/doc/Documentation/unaligned-memory-access.txt),因編譯器不能保證轉型完後,新的 data type 符合它自身的 alignment 需求,在不同的 Arm 處理器下,unaligned data access 造成的行為[不盡相同]((https://heyrick.eu/armwiki/Unaligned_data_access)),但在 intel 處理器下通常能透過多次抓取資料來處理 unaligned data,所以上述轉型才有可能會正確。
* 可以透過編譯指令來提示是否有 unalignmeent data access 的情況發生
1. gcc 8 以上的版本提供編譯選項 `-Wcast-align=strict` 來檢驗是否有 unaligned memory access 的情況發生
```
gcc -Wcast-align=strict align.c
```
3. 或是透過交叉編譯器 gcc-arm-linux-gnueabi 將編譯目標設定在 Arm 處理器上:
```
arm-linux-gnueabi-gcc -Wcast-align align.c
```
上述兩種方法在改寫為 `uint64 *payload = (uint64_t *) (str + i);` 的程式上均會發出下列警告
```
align.c:14:29: warning: cast increases required alignment of target type [-Wcast-align]
uint64_t* payload = (uint64_t *)(str+i);
```
所以此種轉型方法並不通用。
* 回到 `memcpy` 上,使用 `memcpy` 為甚麼不會有 unaligned data access 的問題?參考 glibc 裡 [memcpy.c](https://github.com/lattera/glibc/blob/master/string/memcpy.c) 的實作, `memcpy` 內部會先將 unalignment 的部分使用 byte copy 來對齊,才使用 word copy 來複製接下來的資料,而當剩餘資料長度小於 word 時,再使用 byte copy 複製剩餘資料。
##### 2. 將上述技巧應用於「給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母」
詳細可參考測驗 5,概念為當字元在 `A-Z` 或 `a-z` 時,將第該字元的第 8 個 bit 設為 1,用該 bit 判斷是否為英文大小寫字母。
```c
bool is_alpha(char *s, size_t n)
{
size_t k = n / 8;
for (size_t i = 0; i < k; i++, s += 8) {
uint64_t *chunk = (uint64_t *) s;
uint64_t A = *chunk + PACKED_BYTE(128 - 'A');
uint64_t Z = *chunk + PACKED_BYTE(127 - 'Z');
uint64_t a = *chunk + PACKED_BYTE(128 - 'a');
uint64_t z = *chunk + PACKED_BYTE(127 - 'z');
if (!(((A ^ Z) & PACKED_BYTE(0x80) | (a ^ z) & PACKED_BYTE(0x80)) & PACKED_BYTE(0x80)))
return false;
}
k = n % 8;
while (k--) {
char c = *(s++);
if (!(('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')))
return false;
}
return true;
}
```
##### 3. 承 (2),考慮常見的英文標點符號,判斷輸入字串是否為有效的字元集,避免逐一字元比對
先來定義何謂 **"常見的英文標點符號"**,這邊我使用 [7esl.com](https://7esl.com/punctuation-marks/) 提供的 14 個常用的標點符號。
接著把標點符號與對應的 ASCII Code 列出來觀察
| Marks | Decimal | Hexadecimal |
| ------- | ------- | ------------------- |
| (space) | 32 | 0010 0000 |
| ! | 33 | 0010 0001 |
| " | 34 | 0010 0010 |
| ' | 39 | 0010 0111 |
| () | 40,41 | 0010 1000,0010 1001 |
| , | 44 | 0010 1100 |
| - | 45 | 0010 1101 |
| . | 46 | 0010 1110 |
| / | 47 | 0010 1111 |
| : | 58 | 0011 1010 |
| ; | 59 | 0011 1011 |
| ? | 63 | 0011 1111 |
| \ | 92 | 0101 1010 |
| [] | 91,93 | 0101 1011,0101 1101 |
為了計算方便,我將 `\`也加入至符號中。
:::info
…(Ellipsis) 不在 Ascii Code 裡,故不考慮
:::
接著來看一個 SSE 常用到的 type:`__m128i`,其定義為
```c
typedef union __declspec(intrin_type) __declspec(align(16)) __m128i {
__int8 m128i_i8[16];
__int16 m128i_i16[8];
__int32 m128i_i32[4];
__int64 m128i_i64[2];
unsigned __int8 m128i_u8[16];
unsigned __int16 m128i_u16[8];
unsigned __int32 m128i_u32[4];
unsigned __int64 m128i_u64[2];
} __m128i;
```
可以看到為一個裝有 128 bits 的 union,因為是 union,所以內部記憶體空間是共享的,總共佔有 128 bits。
接著來看使用到的指令,以下指令皆可從 [Intel Intrinsics Guide](https://software.intel.com/sites/landingpage/IntrinsicsGuide/) 中找到。
1. `_mm_setr_epi8` 將參數裡的字元以相反的順序放入到目標變數(`__m128i`)中
e.g. `_mm_setr_epi8(0x10, 0x11, 0x12, ..., 0x24, 0x25);` 會回傳一個 type 為 `__m128i` 且內容為 `0x2524232221...121110` 的數字
2. `_mm_cmpestrm` 會依照所選擇的 mode 來決定如何比對兩個不同的字串,如果符合就在該字元的位置存放 `0xFF`,而 mode 選擇以下:
* `_SIDD_UBYTE_OPS` 表示字元為 8 bits
* `_SIDD_CMP_RANGES` 表示比較 [range](https://doc.rust-lang.org/core/arch/x86_64/constant._SIDD_CMP_RANGES.html),檢測字元是否在給定的範圍內
* `_SIDD_UNIT_MASK` 回傳 bytes/word 的 mask
3. `_mm_test_all_ones` 如果數字內每個 bit 都為 1 則回傳 1,否則回傳 0
主程式的邏輯很簡單,首先使用 `_mm_setr_epi8` 將合理字元的範圍 pack 成 16 bytes 的變數。接著,每次對 16 bytes 的字串進行 `_mm_cmpestrm`,如果符合定義的範圍(`valid_bits`),則該函式會回傳 16 bytes 的 1,並透過 `_mm_test_all_ones` 來檢查。
如果剩餘字串少於 16 bytes,則需要另外判斷。我透過 `check` 函式將 16 bytes 的值拆分成兩個 8 bytes,並進行 bitwise 的檢查,當出現不為 1 時表示出現不在範圍內的字元。
:::warning
這邊原本是想直接用函式來檢查未滿 16 bytes 的值是否全為 1,但翻閱 Guide 找不到相關的函式,也不能直接對 `__m128i` 進行 bit 操作,故只能寫成冗長的程式碼,如果有其他想法歡迎提供。
:::
```c
bool check(__m128i var, size_t last_size)
{
uint64_t val[2];
memcpy(val, &var, sizeof(val));
last_size *= 8; //bytes to bits
if (last_size > 64) {
if (val[0] != 0xFFFFFFFFFFFFFFFF)
return false;
last_size -= 64;
while (last_size) {
if (!((var[1] >> (last_size - 1)) & 1))
return false;
--last_size;
}
} else {
while (last_size) {
if (!((var[0] >> (last_size - 1)) & 1))
return false;
--last_size;
}
}
return true;
}
bool is_valid(const char str[], size_t size)
{
__m128i valid_bits = _mm_setr_epi8('a', 'z', 'A', ']', '?', '?', ',', '/', ' ', '"', ':', ';', '\'', ')', 0, 0);
const char mode = _SIDD_UBYTE_OPS | _SIDD_CMP_RANGES | _SIDD_UNIT_MASK;
unsigned int i = 0;
while (i != size) {
size_t last_size = (i + 16) <= size ? 16 : (size - i);
__uint128_t payload;
memcpy(&payload, str + i, last_size);
__m128i *ptr = (__m128i*)&payload;
const __m128i mask = _mm_cmpestrm(valid_bits, 14, *ptr, last_size, mode);
if (last_size != 16 && !check(mask, last_size)) {
return false;
} else if (!_mm_test_all_ones(mask))
return false;
i += last_size;
}
return true;
}
```
:::info
踩坑紀錄:
使用 `_mm_cmpestrm` 時輸入將 `const char mode = _SIDD_UBYTE_OPS | _SIDD_CMP_RANGES | _SIDD_UNIT_MASK;` 放入第五個參數,卻一直跳出 `the fifth argument must be an 8-bit immediate`。因為 gcc 預設不會將在編譯時期決定 function 內的 const 的值,所以才一直報錯。
解決方法為使用任一編譯器優化選項 `-O1~O3` 來讓編譯器在編譯時期就決定 `mode` 的值。
參考 stack overflow:
1. [_mm256_slli_si256: error “last argument must be an 8-bit intermediate](https://stackoverflow.com/questions/31315491/mm256-slli-si256-error-last-argument-must-be-an-8-bit-intermediate)
2. [Is variable defined by const int determined at compilation time?](https://stackoverflow.com/questions/27048025/is-variable-defined-by-const-int-determined-at-compilation-time)
:::
### `測驗 2`
```c
uint8_t hexchar2val(uint8_t in)
{
const uint8_t letter = in & MASK;
const uint8_t shift = (letter >> AAA) | (letter >> BBB);
return (in + shift) & 0xf;
}
```
#### `MASK`
- [x] `(c)` `0x40`
#### `AAA`
- [x] `(a)` `3`
#### `BBB`
- [x] `(b)` `6`
#### 說明
先將各字元的編碼寫下來
| char | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | hex |
| ----- | --- |:---:| --- | --- | --- | --- | --- |:---:| --- |
| `'0'` | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0x30 |
| `'9'` | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 0x39 |
| `'a'` | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0x61 |
| `'f'` | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0x66 |
| `'A'` | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0x41 |
| `'F'` | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 |0x46
可以看到數字 `0` ~ `9` 與英文字 `a` ~ `f` 或 `A` ~ `F` 的差別在第 6 個 bit 是否為 1,而 `a` ~ `f` 與 `A` ~ `F` 的差別則是在第 5 個 bit 是否為 1。
從變數名稱 `letter` 可以猜測應為判斷是否為字母,判斷字母的 bit 為第 6 個 bit,所以 `MASK` 應為 0x40;再來因為 `return` 的值會與 0xf 做 AND,可以只觀察後 4 個 bits,數字的後 4 個 bits 剛好就是對應到的該數字的值,字母的後 4 個 bits 不管是大寫還是小寫皆相同,所以可以一起做判斷。
字母的後 4 個 bits 為該字母所代表的數字之個位數字,亦即:`'a'` 表示 `10`,則後 4 個 bits 為 `1`,`'d'` 表示 `13`,則後 4 個 bits 為 3。所以我們只需將後 4 個 bits 加上數字 10 就行了,用第 6 個 bit 的位移來組合出數字 10,故答案為 `(a)` 與 `(b)`。
#### 延伸問題
##### 1. 將 hexchar2val 擴充為允許 "0xFF", "0xCAFEBABE", "0x8BADF00D" 等字串輸入,且輸出對應的十進位數值 Hexspeak
```c
/* input must start with 0x and the string length need to less than 32 */
uint64_t hex2val(char *s) {
s += 2;
uint64_t val = 0;
while (*s != '\0') {
if (*s == 'x' || *s == 'X')
break;
val <<= 4;
val |= hexchar2val((uint8_t)(*(s++)));
}
return val;
}
```
程式邏輯很簡單,開頭 `s += 2` 用來移除 hex 中 `0x` 的部分,之後從字串頭開始將每個字元轉換為對應的數值。向左位移 4 bits 是因為每個字元在 16 進制表示中只佔用 4 個 bits。所以每次 `*s` 往低位元移動時,轉換的值皆要往高位元移動 4 bits 來維持數字的正確性,而 OR 運算則是將新轉換的字元之值加入到現有的 `val` 當中。
### `測驗 3`
```c
const uint32_t D = 3;
#define M ((uint64_t)(UINT64_C(XXX) / (D) + 1))
/* compute (n mod d) given precomputed M */
uint32_t quickmod(uint32_t n)
{ uint64_t quotient = ((__uint128_t) M * n) >> 64;
return n - quotient * D;
}
```
#### `XXX`
- [x] `(f)` `0xFFFFFFFFFFFFFFFF`
#### 說明
取餘數的公式可以寫成 $n\ mod\ d = n - \lfloor{\frac{n}{d}}\rfloor \times d$,而 $\lfloor\frac{n}{d}\rfloor$ 根據測驗 3 的說明可以改寫為 $\lfloor n \times \frac{\frac{2^N}{d}}{2^N}\rfloor$。對照程式碼,右移 64 bytes 表示除以 $2^{64}$,所以 $N$ 應為 64。
接著我們嘗試將 $\lfloor{\frac{n}{d}}\rfloor$ 改寫為 $\lfloor \frac{n}{2^N} \times \lceil\frac{2^N}{d}\rceil\rfloor$,可以從後面推導回來:
$$
\begin{equation}
\begin{split}
\lfloor \frac{n}{2^N} \times \lceil\frac{2^N}{d}\rceil\rfloor &= \lfloor \frac{n}{2^N} \times \frac{2^N + r}{d}\rfloor \\
&= \lfloor \frac{n \times 2^N}{d \times 2^N} + \frac{n \times r}{d \times 2^N}\rfloor \\
&= \lfloor \frac{n}{d} + \frac{n \times r}{d \times 2^N}\rfloor \\
&= \lfloor q + \frac{k}{d} + \frac{n \times r}{d \times 2^N}\rfloor \\
&= q + \lfloor \frac{k}{d} + \frac{n \times r}{d \times 2^N} \rfloor\\
&= \lfloor\frac{n}{d}\rfloor \quad \text{if} \quad \frac{k}{d} + \frac{n \times r}{d \times 2^N} < 1
\end{split}
\end{equation}
$$
其中:
* $r = -2^N\ mod\ d$,用來將 $\frac{2^N}{d}$ 無條件進位到整數。
* $k = n\ mod\ d$,用來將 $\frac{n}{d}$ 無條件捨去到整數,並用 $q$ 表示。
* $q$ 為一整數,所以可以從 floor function 中提出。
再來就能進行完整的推導:
$$
\begin{equation}
\begin{split}
\lfloor\frac{n}{d}\rfloor &= \lfloor n \times \frac{\frac{2^N}{d}}{2^N}\rfloor \\
&= \lfloor \frac{n}{2^N} \times \lceil\frac{2^N}{d}\rceil\rfloor \quad \text{if} \quad \frac{k}{d} + \frac{n \times r}{d \times 2^N} < 1 \\
&= \lfloor \frac{n}{2^N} \times (\lfloor\frac{2^N - 1}{d}\rfloor+1)\rfloor \\
\end{split}
\end{equation}
$$
上述 ceil function 的轉換可參考 [wikipedia](https://en.wikipedia.org/wiki/Floor_and_ceiling_functions#Quotients) 的 Quotients 的部分。
與程式碼比對,$M$ 為 $\lfloor\frac{2^N - 1}{d}\rfloor + 1$,所以 `XXX` 為 $2^N - 1$,答案為 `(f)`,特別注意 `quotient` type 為 `uint64_t`,與 `UINT64_C`,其目的皆是在實現公式中 floor function 的部分。
我們對再來深入探討條件成立的情形,因 $n < 2^{32},N = 64$,所以:
$$
\begin{equation}
\begin{split}
\frac{k}{d}+\frac{n \times r}{d \times 2^N} &< \frac{k}{d} + \frac{r}{d} \times \frac{2^{32}}{2^{64}} \\
&=\frac{k}{d} + \frac{r}{d} \times \frac{1}{2^{32}} \\
&<\frac{d-1}{d} + \frac{d-1}{d} \times \frac{1}{2^{32}} \\
&= 1
\end{split}
\end{equation}
$$
上述式子要成立,在 $N = 64$ 且 $n = 2^{32}$ 的情況下,$d$ 必須小於 $2^{32} + 1$。
再來探討 $n$ 的範圍,從上面敘述已經得知 $d < 2^{32} + 1$,所以將情況對調,當 $d = 2^{32} + 1$ 時,$n$ 就必須小於 $2^{32}$,隨著 $d$ 減少,$n$ 能容許的範圍就越大。當 $d = 3$ (此例) 時,$n$ 在 $2^{63}$ 以下都能滿足條件。
從以上兩點可知此程式在 32 bits 的範圍下能夠正常運作,我們能將 $d$ 寫成更通用的形式,$d < \frac{2^N}{n} + 1$。當我們想要在更高位元的操作,只需要調整 $N$ 與 $n$ 來滿足需要的位元數。
#### 延伸問題
##### 1. 由 Facebook 公司所維護的 [jemalloc](https://github.com/jemalloc/jemalloc) 是個高效能的記憶體配置器 (memory allocator,即 malloc 和 free 函式對應的實作),特別在多核多執行緒的環境有優異表現,在其原始程式碼 [include/jemalloc/internal/div.h](https://github.com/jemalloc/jemalloc/blob/dev/include/jemalloc/internal/div.h) 和 [src/div.c](https://github.com/jemalloc/jemalloc/blob/dev/src/div.c) 也運用類似的技巧,請閱讀原始程式碼,並抽離快速除法實作為獨立的程式碼,說明運作原理,也該撰寫測試程式,比較透過處理器的整數除法指令及本技巧的效能差距;
將 `jemalloc` 的除法操作抽取出來
```c
typedef struct div_info_s div_info_t;
struct div_info_s {
uint32_t magic;
};
void div_init(div_info_t *div_info, size_t divisor);
static inline size_t div_compute(div_info_t *div_info, size_t n) {
assert(n <= (uint32_t) - 1);
size_t i = ((uint64_t)n * (uint64_t)div_info->magic) >> 32;
return i;
}
void div_init(div_info_t *div_info, size_t d) {
assert(d != 0);
assert(d != 1);
uint64_t two_to_k = ((uint64_t)1 << 32);
uint32_t magic = (uint32_t)(two_to_k / d);
if (two_to_k % d != 0) {
magic++;
}
div_info->magic = magic;
}
```
其內容與本測驗中的實作非常相似,只有兩個地方不同:
1. `jemalloc` 使用的 $M$ (或是稱為 **Magic Number**) 為 $2^{32}$,而此測驗中用的為 $2^{64}$。
2. 在上面推導中出現的 $\lceil\frac{2^N}{d}\rceil$,因 c 語言沒有有效率的 ceil function,在 `jemalloc` 中使用餘數判斷是否為整除,而此測驗中則是轉換為 floor function 來實作。
:::danger
`jemalloc` 使用 $2^{32}$ 精度太低,註解只有寫道 We bound n < 2^32, and don't support dividing by one.,好像沒有關於 divisor 的限制。根據我在上面的推導,使用 $2^{32}$ 只能保證在 16 bits 下答案正確
:::
這裡給出其中一個例子:$\lfloor\frac{9510000}{16655}\rfloor=570$,但 `jemalloc` 會給出 $571$。
接著量測兩種除法的效率,分別是考慮計算 **Magic Number** 的時間與不考慮的時間。
分別測試 3 種除法之效率:
1. 內建除法運算子
2. 此測驗題中使用 floor function (轉型) 的除法,或稱為 [lkk ](https://arxiv.org/pdf/1902.01961.pdf)
3. `jemalloc` 使用的 `div`
為了讓結果更具有代表性,將本測驗題中的 $N$ 改為 $32$。我將從 $1\sim2^{20}$ 中以 $1000$ 的間隔抽取一次 $n$,並對該數字測試 1000 次來盡量縮小標準差。注意 $d$ 的選擇必須符合公式 $d < \frac{2^N}{n} + 1$。並且為了減少 function call 造成的時間差異 (雖然有 inline,但編譯器不一定會使用),我將 `jemalloc` 的程式碼展開成 one line 的形式。並將 Magic Number 的精度改為 $2^{64}$
首先是不考慮 Magic Number 的除法效率,可以發現 `jemalloc` 與 `lkk` 在計算除法的部分是一樣的,所以只考慮 `jemalloc` 與除法運算子的效率就好。
測試程式:
```c
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <assert.h>
#include <time.h>
#include <stdlib.h>
#define MIN 3
#define MAX 0x80000000
int main() {
srand(time(NULL));
div_info_t div;
struct timespec tt1, tt2;
for(uint32_t i = MIN; i < MAX; i += 100000) {
size_t d = 0;
size_t ans[100001][2];
size_t times = 0;
time_t time1, time2;
while ((d = rand() % i) < 2)
;
jemalloc_init(&div, d);
clock_gettime(CLOCK_MONOTONIC, &tt1);
while (times++ <= 100000)
ans[times][0] = (i * (__uint128_t)div.magic) >> 64;
clock_gettime(CLOCK_MONOTONIC, &tt2);
time1 = (tt2.tv_nsec - tt1.tv_nsec);
times = 0;
clock_gettime(CLOCK_MONOTONIC, &tt1);
while (times++ <= 100000)
ans[times][1] = i / d;
clock_gettime(CLOCK_MONOTONIC, &tt2);
time2 = (tt2.tv_nsec - tt1.tv_nsec);
assert(ans[100000][0] == ans[100000][1]);
printf("test %d / %ld, time on jemalloc = %ldns, time on basic divide = %ldns\n", i, d, time1, time2);
}
}
```
我將 divisor 設為 i 以內,原因是我怕 divisor 大於 dividend 時編譯器會利用比大小來回傳(實際上會不會尚未研究)。每個測資皆跑 100000 次來減少標準差。另外我也將 `jemalloc` 的精度調整為 $2^{64}$。

可以看到 `-O0` 的效能差距相當大,~~而當編譯器加速後,兩者除法之間也有些許的差異。不管怎樣,在當 divisor 不用重新計算時,`jemalloc` 的效率皆比內建除法還好。~~
:::info
原先有測試 `O1~O3` 的效能,但發現編譯器加速的部分過於複雜,擔心測試不夠完整,故先移除。
:::
再來是考慮當 divisor 要重新計算時,計算 **Magic Number** 的除法效率
餘數判斷是非常耗時的操作,在 `jemalloc` 中的 `two_to_k % d != 0` 這個判斷式應該可以再簡化成判斷 `d` 是否為 2 的倍數,改為 `d & (d - 1) == 0` 來看看時間是否比較快。
所以這部分有 4 種待測除法
1. 內建除法運算子
2. `lkk` 使用的 `div`
3. `jemalloc` 使用的 `div`
4. 更改 `jemalloc` 後的 `div`

可以看到如果每次 divisor 都不同,計算 Magic Number 的成本比直接使用一般除法還高,~~而當編譯器加速得越多,不同方法之間的速度差也會越來越少。~~
這邊也順便證明了我去掉模運算的版本速度比原先 `jemalloc` 還要快上許多,但還是比不上 `lkk` 中使用轉型實現 floor function 的方法。
所以只要在固定的 divisor 上跑多次除法,先決定好 Magic Number 的值後,之後的除法效率就能夠超越內建的除法運算,但當每次 divisor 都會變動時,把計算 Magic Number 的成本加進來後,效率就可能比內建的除法運算差!
### `測驗 4`
```c
bool divisible(uint32_t n)
{
return n * M <= YYY;
}
```
#### `YYY`
- [x] `(c)` `M - 1`
#### 說明
證明參考 [sammer1107](https://hackmd.io/@sammer1107/ryHs1sESP#%E6%B8%AC%E9%A9%97-4) 同學的推導
* 當 $n \ {\mid} \ d$ 時,$n \times M <= M - 1$
* 當 $n \ {\nmid} \ d$ 時,$n \times M > M - 1$
分成兩個 case,$n \ {\mid} \ d$ 與 $n \ {\nmid} \ d$,算式內數值需對 $2^{64}$ 取餘數,因 type 為 `uint64_t`。
1. $n \ {\mid} \ d$,$n = 3q$:
$$
\begin{align}
n \times M &= 3q \times \lceil\frac{2^{64}}{3}\rceil \\
&= 3q \times \frac{2^{64} + 2}{3} \\
&= q2^{64} + 2q \quad (\text{mod} \ \ 2^{64})\\
&= 2q
\end{align}
$$
再來思考一下 $q$ 的上界:
$$
\begin{align}
n = 3q < 2^{32} \\
q < \frac{2^{32}}{3}
\end{align}
$$
帶回原式完成證明:
$$
\begin{align}
n \times M &= 2q \\
&< \frac{2^{33}}{3} \\
&< M - 1 \\
&= \frac{2^{64}+2}{3} - 1\\
&= \frac{2^{64} - 1}{3}
\end{align}
$$
2. $n \ {\mid} \ d$,$n = 3q + r,0<r<3$
我們將 $r$ 取 1,因 $3q + 1 > M - 1 \implies 3q + 2 > M - 1$:
$$
\begin{align}
n \times M &= (3q+r) \times \lceil\frac{2^{64}}{3}\rceil \\
&= (3q + 1) \times \frac{2^{64} + 2}{3} \\
&= q2^{64} + 2q + \frac{2^{64} + 2}{3} \quad (\text{mod} \ \ 2^{64})\\
&= 2q + \frac{2^{64} + 2}{3} \\
&\geq \frac{2^{64}+2}{3} \\
&> M - 1 \\
&= \frac{2^{64}-1}{3}
\end{align}
$$
$n \times M$ 是有可能等於 $M$ 的,如果 rhs 為 $M$ 而不是 $M - 1$ 的話,演算法有可能會有誤。如 $n = 1$ 時等號相等,`divisible` 會 return 1 而不是 0。
### `測驗 5`
```c
#include <ctype.h>
#include <stddef.h>
#include <stdint.h>
#define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u)
/* vectorized implementation for in-place strlower */
void strlower_vector(char *s, size_t n)
{
size_t k = n / 8;
for (size_t i = 0; i < k; i++, s += 8) {
uint64_t *chunk = (uint64_t *) s;
if ((*chunk & PACKED_BYTE(VV1)) == 0) { /* is ASCII? */
uint64_t A = *chunk + PACKED_BYTE(128 - 'A' + VV2);
uint64_t Z = *chunk + PACKED_BYTE(128 - 'Z' + VV3);
uint64_t mask = ((A ^ Z) & PACKED_BYTE(VV4)) >> 2;
*chunk ^= mask;
} else
strlower(s, 8);
}
k = n % 8;
if (k)
strlower(s, k);
}
```
#### `VV1`
- [x] `(e)` `0x80`
#### `VV2`
- [x] `(b)` `0`
#### `VV3`
- [x] `(a)` `(-1)`
#### `VV4`
- [x] `(e)` `0x80`
#### 說明
先了解 `PACKED_BYTE` 的作用,從前面的 `& 0xff` 可知前面這部分只有前 1 個 byte 有值,再來後面的乘法可看成 8 個 bytes,每個 bytes 皆為 1,所以 `PACKED_BYTE` 可以知道為將 1 個 byte 的資料擴充成 8 個 bytes。
接著來看 `VV1`,從第一個 `if` 後方的註解 `is ASCII?` 可知該判斷式用來判斷是否為 ASCII code,下方 `else` 裡使用 `strlower`,應是用來處理 extend ASCII 的部分,所以要判斷是否為 ASCII code 的話 `VV1` 需選擇 0x80 (原因可參考測驗1)。
來看 `mask` 的用途,從 `*chunk ^= mask` 可以知道 `mask` 要將 `*chunk` 內 `A-Z` 的 ASCII code 轉為 `a-z`。根據[...](),對每個 `A-Z` 字元的第 6 個 bit 做 XOR 運算就能轉成 `a-z`。故 `mask` 為在 `*chunk` 內每個為 `A-Z` 字元的部分的第 6 個 bit 為 1,其餘為 0。
由上述可知,`((A ^ Z) & PACKED_BYTE(VV4)) >> 2` 應該會在 `A-Z` 的字元中第 6 個 bit 產生 1,所以 `PACKED_BYTE(VV4)` 在第 8 個 bit 上產生 1,故 `VV4` 為 0x80。而 `A ^ Z` 就是用來檢查是否為 `A-Z` 間的字元,並將結果設置在第 8 個 bit 上。
要讓 XOR 為 True,必須一邊為 True,一邊為 False,但仔細觀察 `A` 與 `Z` 的程式碼,會發現在第 8 個 bit 上,`Z` 為 True 時,`A` 必定為 True。因 `Z` 比 `A` 還要多減了 `'Z' - 'A'` 的 ASCII code。
所以我們目的是讓 `A` 的第 8 個 bit 為 1,`Z` 的第 8 個 bit 為 0。則要讓 `128 - 'A' + VV2 + c` 內當 `c` 的 ASCII code 為 `A` 以上時,第 8 個 bit 為 1,`VV2` 需為 0,而在 `c` 的 ASCII code 大於 `Z` 時,`128 - 'Z' + VV3 + c` 的第 8 個 bit 要為 0,故 `VV3` 為 -1。
#### 延伸問題
##### 1. 將前述測試程式 main 函式的 char str[] 更換為 char *str,會發生什麼事?請參閱 C 語言規格書,說明 string literal 的行為
改成 `char *str` 會造成 Segmentation fault,試著用 pointer to char 指向已經建立 String Literals 上,`char *p_str = str`。程式卻能正常執行。
所以推測問題不是 pointer 的問題,而是建立變數時期的問題,從規格書裡找答案。
在規格書 [6.4.5 String literals](http://c0x.coding-guidelines.com/6.4.5.html) 中提到
> a byte or code of value zero is appended to each multibyte character sequence that results from a string literal or literals. The multibyte character sequence is then used to initialize an array of static storage duration and length just sufficient to contain the sequence.
>
> If the program attempts to modify such an array, the behavior is undefined.
在 [6.7.8 Initialization](http://c0x.coding-guidelines.com/6.7.8.html) 中也有寫道
>**`char *p = "abc";`**
defines p with type “pointer to char” and initializes it to point to an object with type “array of char” with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined.
所以 `p` 為 "pointer to char" 的 type,指向一個 "array of char" 這個 object。在 [Frequently Asked Questions](http://c-faq.com/decl/strlitinit.html) 中也有提到,除了初始化時的變數 type 為 array of char 以外,string literal 會建立一個 unnamed, static array of characters 然後存放在 read-only 的記憶體位置,再將該位置回傳給 pointer to char 這個變數。
統整一下:
1. `char p[] = "hello";` 會被轉譯成 `char p[] = {'h', 'e', 'l', 'l', 'o', '\0'}`,所以操作該變數就跟一般的 array 操作一樣。
2. `char *p = "hello";` 此行為會在 read-only 的記憶體上建立 `hello` 這個字串,並將 `p` 指向該記憶體位置。
一個在 [stack overflow](https://stackoverflow.com/questions/1704407/what-is-the-difference-between-char-s-and-char-s) 上的例子我覺得能幫助理解:
```c
char *s0 = "hello world";
char s1[] = "hello world";
0x01 0x02 0x03 0x04
0x00008000: 'h' 'e' 'l' 'l'
0x00008004: 'o' ' ' 'w' 'o'
0x00008008: 'r' 'l' 'd' 0x00
...
s0: 0x00010000: 0x00 0x00 0x80 0x00
s1: 0x00010004: 'h' 'e' 'l' 'l'
0x00010008: 'o' ' ' 'w' 'o'
0x0001000C: 'r' 'l' 'd' 0x00
```
當使用 `char *s0 = "hello world";`時,
編譯器實際上做了以下事情
```c
static char __unnamed[] = "hello world";
char *c = __unnamed;
```
至於這個存放字串常量的 read-only 空間在哪裡呢?是放在一個名為 rodata 的地方,這邊 ro 指的就是 read-only,在程式內宣告的 `const` 變數,`#define` 的常量皆放置在此空間內,此位置與 [Data segment](https://en.wikipedia.org/wiki/Data_segment) 為兩種不同的空間。
可以寫個小程式來測試:
```c
#include <stdio.h>
char *s = "hello";
int main() {
char *str1 = "hello";
char *str2 = "world";
char str3[] = "!!";
return 0;
}
```
輸出 assembly code 來看編譯器是如何處理這些字串的
```
gcc -S str.c
```
可以看到以下程式碼:
```c
3 .globl s
4 .section .rodata
...
11 s:
12 .quad .LC0
13 .section .rodata
```
可以看到編譯器將字串常量放入 .rodata 裡面,且在 main 裡面的 `str1` 也會指到字串 `"hello"` 所在的地方,意思是 `s == str1`,而使用陣列方式宣告的 `char str3[]` 則是將字串內的值直接存入變數中
```c
35 movw $8481, -11(%rbp)
```
可以印出四個變數所指向的位置來檢查:
```c
s = 0x556e92e397a4
str1 = 0x556e92e397a4
str2 = 0x556e92e397aa
str3 = 0x7ffd2a9f0366
```
與預測的結果符合,`s` 與 `str1` 都指向在 rodata 中的 `"hello"` 字串位置。也可以注意到 `s, str1, str2` 三個位置都為 0x55 開頭,表示放在同樣的區塊,也就是 rodata 區塊,而 `str3` 與其他三著不同,是放置在 stack 區塊。
也可以透過 objdump 來查看 rodata 內的內容
```
objdump -s -j .rodata a.out
-------------------------------------
pointer: file format elf64-x86-64
Contents of section .rodata:
0730 01000200 68656c6c 6f00776f 726c6400 ....hello.world.
```
清楚的看到 `"hello"` 與 `"world"` 都被放到 rodata 中。
### `測驗 6`
```c
int singleNumber(int *nums, int numsSize)
{
int lower = 0, higher = 0;
for (int i = 0; i < numsSize; i++) {
lower ^= nums[i];
lower &= KKK;
higher ^= nums[i];
higher &= JJJ;
}
return lower;
}
```
#### `KKK`
- [x] `(c)` `~higher`
#### `JJJ`
- [x] `(d)` `~lower
`
#### 說明
要用 XOR 來比對出現一次與出現三次的差別,只用一個變數是不夠的,因一次 XOR 與三次 XOR 會造成相同結果。所以我們使用兩個變數 `lower` 與 `higher` 用來記錄出現一次與出現兩次。
我們先只看一個數 `nums[i]`,把 `lower` 與 `higher` 看成兩個 set,用來記錄使用過的 bits。
`lower` 有兩種情形,分別是 `lower` 存有 `nums[i]` 的 bits 與不存有 `nums[i]` 的 bits。經過 `lower ^= nums[i]` 後,`lower` 進到下個狀態,存有 `nums[i]` 的 bits 會被移除,沒有存 `nums[i]` 的話,則會將 `nums[i]` 加入 `lower` 中。
`higher` 同理,有兩種情形,存有 `nums[i]` 的 bits 與不存有 `nums[i]` 的 bits。故可以推測當 `nums[i]` 出現一次時,存到 `lower` 中,出現第二次時,從 `lower` 中移除,並加入至 `higher` 中,第三次出現則從 `higher` 中移除。
這樣答案就很清楚了,可以對照每行程式碼的行為:
1. `lower ^= nums[i]` 將 `nums[i]` 從 `lower` 中加入或移除
2. `lower &= ~higher` 如果 `nums[i]` 已經在 `higher` 當中(第二次出現),則 `lower` 不需要再加入 `nums[i]`,將其移除
3. `higher ^= nums[i]` 將 `nums[i]` 從 `higher` 中加入或移除
4. `higher &= ~lower` 如果 `nums[i]` 已經在 `lower` 當中(第一次出現),則 `higher` 不需要再加入 `nums[i]`,將其移除
畫成 state diagram 可以更好理解,可以想像當 `nums[i]` 不同時,出現 3 次的值還是會從 set 中移除,而出現 1 次的值永遠會存在 `lower` 當中,所以最後回傳的 `lower` 就是答案。
```graphviz
digraph G
{
rankdir=LR;
node node1 = [fixedsize=true, width = 2, height = 0.8, label = "lower = {}
higher = {}"] n1;
node node2 = [fixedsize=true, width = 2, height = 0.8, label = "lower = {nums[i]}
higher = {}"] n2;
node node3 = [fixedsize=true, width = 2, height = 0.8, label = "lower = {}
higher = {nums[i]}"] n3;
n1->n2 [label = "first times"];
n2->n3 [label = "second times"];
n3->n1 [label = "third times"];
}
```
#### 延伸問題
##### 1. 請撰寫不同上述的程式碼,並確保在 LeetCode [137. Single Number II](https://leetcode.com/problems/single-number-ii/) 執行結果正確且不超時;
我使用 64 bits 來儲存 32 bits 內每個 bit 出現的情況。
首先,我將每個數字擴充成 64 bits,並且中間間隔 1 個 0 用來存放出現 2 次以上的 bit。
類似下方表格這種轉換:
| number | 4 bits | 8 bits |
| -------- | -------- | -------- |
| 5 | <font color="blue">0101</font> | <font color="#f00">0</font><font color="blue">0</font><font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">0</font><font color="#f00">0</font><font color="blue">1</font> |
| 14 | <font color="blue">1110</font> | <font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">0</font> |
| 16 | <font color="blue">1111</font> | <font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">1</font> |
這樣每個數字原本 32 bits 都擁有兩個 bit 可以儲存每個 bit 出現的次數,接著就是當出現 3 次,也就是該 bit 在擴充的位元裡出現 11 的時候,要將這兩個 bit 清空。
我先對現在累加的數字 `bit_set` 進行 `mask`,`mask` 的值為 `0x101010...1010`,用來檢查該 bit 是否累加超過 2 次以上。如果該結果為 1,再來右移 1 個 bit 檢查前一個 bit 是否也為 1,假如這兩個 bit 都為 1,就從 bit_set 內部清空。
最後再把結果的 64 bits 還原成 32 bits 並回傳。
```c
int singleNumber(int *nums, int numsSize)
{
uint64_t mask = 0xAAAAAAAAAAAAAAAA;
uint64_t bit_set = 0;
int ans = 0;
for (int i = 0; i < numsSize; ++i) {
uint64_t num_bits = 0;
for (int j = 0; j < 32; ++j) {
uint64_t offset = nums[i] & ((uint64_t)1 << j);
num_bits |= (offset << j);
}
bit_set += num_bits;
uint64_t t = (bit_set & mask) >> 1;
t = ((bit_set & t) << 1) | t;
bit_set &= ~t;
}
for (int i = 0; i < 32; ++i) {
ans |= (bit_set & ((uint64_t)1 << (i << 1))) >> i;
}
return ans;
}
```
##### 2. 延伸題意,將重複 3 次改為改為重複 4 次或 5 次,並找出可容易推廣到多次的程式碼;
詳細說明可參考[討論區](https://leetcode.com/problems/single-number-ii/discuss/43295/Detailed-explanation-and-generalization-of-the-bitwise-operation-method-for-single-numbers),概念上與此題的解法相似,持續相加出現的 bit,直到出現的 bit 到達上限 `k`,將之移除。而 `k` 為每個數字出現的上限,可以知道,要記錄 `k` 個數字的 bit,必須至少要有 $\lceil\log_{2}k\rceil$ 個變數才行。
這邊 `k` 表示同樣數字出現的次數,以下為 `k` 任意數時迴圈內的程式碼:
```c
/* k == 2 */
x2 ^= x1 & i;
x1 ^= i;
mask = ~(x1 & x2);
x2 &= mask;
x1 &= mask;
/* k == 3 */
x3 ^= x2 & x1 & i;
x2 ^= x1 & i;
x1 ^= i;
mask = ~(x1 & ~x2 & x3);
x3 &= mask;
x2 &= mask;
x1 &= mask;
/* k == n */
xn ^= xn-1 & xn-2 ... & x1 & i;
xn-1 ^= xn-2 & xn-3 ... & x1 & i;
...
x1 ^= i;
mask = ~(x1 & x2 ... & xn);
xn &= mask;
xn-1 &= mask;
...
x1 &= mask;
```
要注意上面 `mask` 的值只是參考,實際上要隨著 `k` 值來決定 True 或 False,用來處理當 `xn, xn-1, ..., x1` 在某個 bit 上的值等於 `k` 時的情形,將這些 bit 清空。
假設表格內 `x5, x4, ..., x1` 都為 1 bit 的值,mask 的計算如下:
| k | $x_{5}$ | $x_{4}$ | $x_{3}$ | $x_{2}$ | $x_{1}$ | mask |
| --- | ------- | ------- | ------- | ------- | ------- | -------------------- |
| 1 | False | False | False | False | True | ~(x1) |
| 4 | False | False | True | False | False | ~(x3) |
| 7 | False | False | True | True | True | ~(x3 & x2 & x1) |
| 14 | False | True | True | True | False | ~(x4 & x3 & x2) |
| 29 | True | True | True | False | True | ~(x5 & x4 & x3 & x1) |
擴展到 32 bits 的 integer,當所有變數內的某個位置的 bit 與 `k` 的位元相符合時,表示已經達到上限了,將之從所有變數內刪除(NOT + AND)。
接著就是將這些步驟擴充成能隨著 `k` 來改變,底下程式碼邏輯與上面完全相同,只是用到了迴圈來解決變數數量不同的問題。
```c
int singleNumber(int* nums, int len, int k) {
uint32_t num_variable = ceil(log2(k)); //c99
uint32_t mask;
int* x = calloc(num_variable, sizeof(int));
for (int i = 0; i < len; ++i) {
mask = 0xFFFFFFFF;
for (int v = 0; v < num_variable; ++v) {
int tmp = nums[i];
for (int j = v + 1; j < num_variable; ++j) {
tmp &= *(x + j);
}
*(x + v) ^= tmp;
}
for (int v = 0; v < num_variable; ++v) {
mask &= ((1u << v) & k) > 0 ? *(x + v) : ~*(x + v);
}
mask = ~mask;
for (int v = 0; v < num_variable; ++v) {
*(x + v) &= mask;
}
}
int ans = 0;
for (int v = 0; v < num_variable; ++v)
ans |= *(x + v);
free(x);
return ans;
}
```
:::info
記得使用到 `math.h` 編譯時要輸入 `-lm` 連結至函式庫
:::
###### tags: `linux2020`