# 2020q3 Homework2 (quiz2) contributed by <`hsiehong`> [github](https://github.com/hsiehong/quiz02) ###### tags: `進階電腦系統理論與實作` > [quiz2](https://hackmd.io/@sysprog/2020-quiz2) > [已提交的作業共筆](https://hackmd.io/@sysprog/2020-homework2) ---- ## 測驗1 #### 判斷指定的記憶體範圍內是否全是有效的 ASCII 字元 ```c= #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 = 0x8080808080808080` #### 題目原理解釋 因為有效的 ASCII 範圍是 0 - 127,二進制表示是 `0x0000 0000` - `0x0111 1111` ,所以可以透過 `alpha & 1000 0000` 來檢測字母的 `MSB` 是否為 1,為 `1` 表示不為有效的 ASCII 字元 (ASCII > 127),在 64 位元觀點此 mask 即為 `0x8080808080808080` #### 為何用到 `memcpy` ? * 參閱 [memcpy - Linux manual page](https://man7.org/linux/man-pages/man3/memcpy.3.html) 可以得知若系統沒有 memory alignment,在 `memcpy` 會做 memory alignment,可以避免 [Unaligned Memory Accesses](https://www.kernel.org/doc/Documentation/unaligned-memory-access.txt),因為並不是所有的硬體都有支援 unaligned memory acces,`memcpy` 就是透過軟體來達到 memory alignment,雖然執行速度會較硬體慢,但不在這邊的討論範圍。 * 其中 `OPSIZ` 會因不同的架構而異,可能是 8 bytes 或 16 bytes,而把多出來不足 8( or 16) bytes 的部分先行複製 ```c=38 len -= (-dstp) % OPSIZ; BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ); ``` :::info Question 1. why (-dstp) ? ::: * reference: 1. [memcpy](https://blog.xiocs.com/archives/181/) 2. [alignment fault](https://www.itread01.com/content/1544492598.html) #### 將上述技巧應用於「給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母」 * 判斷是否為字母的部分使用第 5 題的技巧,利用 `A` `Z` 的 MSB 判斷是否是大寫字母,利用 `a` `z` 的 MSB 判斷是否是小寫字母,並利用 `PACK_BYTE` 來增加可讀性 * [detectEffectiveChar.c](-) #### 考慮常見的英文標點符號,判斷輸入字串是否為有效的字元集,避免逐一字元比對。Intel SSE 4.2 加入 STTNI (String and Text New Instructions) 指令,可加速字串比對,閱讀 [Schema Validation with Intel® Streaming SIMD Extensions 4](https://software.intel.com/content/www/us/en/develop/articles/schema-validation-with-intel-streaming-simd-extensions-4-intel-sse4.html) :::success TODO reference: [`eecheng`](https://hackmd.io/@eecheng/HJgpR7RNw) ::: --- ## 測驗2 #### 將十六進位表示的字串 (hexadecimal string) 轉為數值 ```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 = 0x40` `AAA = 3` `BBB = 6` #### 題目原理解釋 觀察一個 byte 的狀況 | 字元 | ASCII(in) | output(dec) | output(bin) | |:----:|:---------:|:-----------:|:-----------:| | '0' | 0011 0000 | 0 | 0000 0000 | | '6' | 0011 0110 | 6 | 0000 0110 | | '9' | 0011 1001 | 9 | 0000 1001 | | 'A' | 0100 0001 | 10 | 0000 1010 | | 'a' | 0110 0001 | 10 | 0000 1010 | | 'F' | 0100 0110 | 15 | 0000 1111 | | 'f' | 0110 0110 | 15 | 0000 1111 | * `MASK` 為 `0100 0000`,推測是判斷字元是**數字**還是**字母**,若是字母則 `letter` 為`0100 0000`,數字的話 `letter` 為`0000 0000` * 從 line5 回傳值 `(in + shift) & 0xf` 推測,`0xf` 二進制為 `0000 1111`,即保留 `(in + shift)`末四位,所以只須探討這末四位是何方神聖 * 若字元是字母,`A` 和 `a` 的末四位都是 `0001`,要變成正確輸出的 `1010` 需要加上 `1001`,可以推斷正確的 `offset` 為 `1001` 。這裡利用 `letter` `0100 0000` 來產生 shift,`letter >> 3 | letter >> 6` 可以得到正確的 offset `0000 1001`。 * 若字元是數字,`letter` 就是 `0`,line 5 就沒有作用,`in` 末四碼就是正確的結果 #### 將 hexchar2val 擴充為允許 "0xFF", "0xCAFEBABE", "0x8BADF00D" 等字串輸入,且輸出對應的十進位數值 * 將參數改成一次處理一個字串,且能處理的範圍不大於 64 bits (即最多只能處理八個字元,不包含 `0x`),並檢查是否為 [Hexspeak](https://en.wikipedia.org/wiki/Hexspeak) * [hexchar2val.c](-) ```c= uint64_t hexchar2val(char *in) { uint64_t result = 0; int len = strlen(in); if (in[0] == '0' && in[1] == 'x') { for (int i = 2; i < len; i++) { const uint8_t letter = *(in + i) & 0x40; uint8_t shift = (letter >> 3) | (letter >> 6); result = (result << 4) | (in[i] + shift) & 0xf; } } return result; } ``` * 測試結果 ```c= int main(void) { printf("%llu\n", hexchar2val("0xFF")); printf("%llu\n", hexchar2val("0xCAFEBABE")); printf("%llu\n", hexchar2val("0x8BADF00D")); return 0; } ``` ``` 255 3405691582 2343432205 ``` --- ## 測驗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 = 0xFFFFFFFFFFFFFFFF` #### 題目原理解釋 根據數學式 $$ \dfrac{n}{d} = n \times (\dfrac{\dfrac{2^N}{d}}{2^N}) = (n \times \dfrac{2^N}{D}) \times \dfrac{1}{2^N} $$ * line 6 可以推得是除以$2^{64}$,所以 N 應為64,`((__uint128_t) M * n) >> 64` 即為 $n/d$ 的整數部份( quotient ) * 精確值 `XXX` 應為 $2^{64}$,但 `XXX` 最大只能儲存 $2^{64}-1$,就是 `0xFFFFFFFFFFFFFFFF`,最後 `+1` 代表無條件進位,確保 $\dfrac{2^{64}}{D}$ 與 $\dfrac{2^{64}-1}{D}+1$ 的整數部份是會相同的 * 從上述可以得知, `M` 是對應到 $\lceil{\dfrac {2^{32}}{d}}\rceil$ * line7 算出商之後,`n - q * D` 即可得到餘數 [測試程式](https://github.com/hsiehong/quiz02/blob/master/quickmod/measure.c) #### fastmod 實驗 * 對 `fast_mod` 與 `normal_mod` 分別跑 10 萬次 * [繪圖 script](https://github.com/hsiehong/quiz02/blob/master/quickmod/perf.gp) * 比較不做最佳化與做 O1, O2, O3 最佳化的結果 * 可以看出有做最佳化的話其實結果不會差太多,但 normal 版本還是會稍微較好 * O0 ![](https://i.imgur.com/XKPzbKH.png) * O1 ![](https://i.imgur.com/kQpITf7.png) * O2 ![](https://i.imgur.com/FbENpg9.png) * O3 ![](https://i.imgur.com/3spZyrO.png) :::info Question 1. `fast_mod` 在全部的情況下表現都沒有比較優異? 2. 如何驗證實驗結果是正確的? ::: #### 關於 [jemalloc 快速除法](https://github.com/jemalloc/jemalloc) * 由 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) 也運用類似的技巧,請閱讀原始程式碼,並抽離快速除法實作為獨立的程式碼,說明運作原理,也該撰寫測試程式,比較透過處理器的整數除法指令及本技巧的效能差距 * 假設 $n = q\times d$,$n, q, d$ 都是整數,已知 $n, d$ 求 $q$,$q = \dfrac{n}{d}$,可以轉換成下面的形式 $q =$$\left \lfloor \lceil \dfrac{2^k}{d} \rceil \times \dfrac{n}{2^k} \right \rfloor = \left \lfloor \dfrac{2^k+r}{d} \times \dfrac{n}{2^k} \right \rfloor$,其中 $r$ = $(-2)^k$ mod $d$ $=\left \lfloor \dfrac{2^k}{d} \times \dfrac{n}{2^k} + \dfrac{r}{d} \times \dfrac{n}{2^k} \right \rfloor = \left \lfloor \dfrac{n}{d} + \dfrac{r}{d} \times \dfrac{n}{2^k} \right \rfloor$,因為 $n = q\times d$,所以$\dfrac{n}{d}$ 為整數 $=\dfrac{n}{d} + \left \lfloor \dfrac{r}{d} \times \dfrac{n}{2^k} \right \rfloor$,推導到這邊,只要能確保$\left \lfloor \dfrac{r}{d} \times \dfrac{n}{2^k} \right \rfloor < 1$ ,就代表 $\dfrac{n}{d}$ 可以被改寫成$\left \lfloor \lceil \dfrac{2^k}{d} \rceil \times \dfrac{n}{2^k} \right \rfloor$ * 其中 $r$ = $(-2)^k$ mod $d$,可以確定 $r < d$,而 $\dfrac{n}{2^k}$ 可以令 $k = 32$ 來確保其 $< 1$ * 推導完再來看快速除法的資料結構 [div.h](https://github.com/jemalloc/jemalloc/blob/dev/include/jemalloc/internal/div.h) ```c= #ifndef JEMALLOC_INTERNAL_DIV_H #define JEMALLOC_INTERNAL_DIV_H #include "jemalloc/internal/assert.h" /* * This module does the division that computes the index of a region in a slab, * given its offset relative to the base. * That is, given a divisor d, an n = i * d (all integers), we'll return i. * We do some pre-computation to do this more quickly than a CPU division * instruction. * We bound n < 2^32, and don't support dividing by one. */ typedef struct div_info_s div_info_t; struct div_info_s { uint32_t magic; #ifdef JEMALLOC_DEBUG size_t d; #endif }; 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); /* * This generates, e.g. mov; imul; shr on x86-64. On a 32-bit machine, * the compilers I tried were all smart enough to turn this into the * appropriate "get the high 32 bits of the result of a multiply" (e.g. * mul; mov edx eax; on x86, umull on arm, etc.). */ size_t i = ((uint64_t)n * (uint64_t)div_info->magic) >> 32; #ifdef JEMALLOC_DEBUG assert(i * div_info->d == n); #endif return i; } #endif /* JEMALLOC_INTERNAL_DIV_H */ ``` * 在 structure `div_info_s` 中有一個 `uint32_t magic`,`magic` 就是上述推導中的 $\left \lceil \dfrac{2^k}{d} \right \rceil$,在 [div.c](https://github.com/jemalloc/jemalloc/blob/dev/src/div.c) 中實做 * `div_compute` 回傳的是 $\left \lfloor \lceil \dfrac{2^k}{d} \rceil \times \dfrac{n}{2^k} \right \rfloor$ * 再來實做部份 [div.c](https://github.com/jemalloc/jemalloc/blob/dev/src/div.c) (省略推導過程) ```c= #include "jemalloc/internal/jemalloc_preamble.h" #include "jemalloc/internal/div.h" #include "jemalloc/internal/assert.h" void div_init(div_info_t *div_info, size_t d) { /* Nonsensical. */ assert(d != 0); /* * This would make the value of magic too high to fit into a uint32_t * (we would want magic = 2^32 exactly). This would mess with code gen * on 32-bit machines. */ assert(d != 1); uint64_t two_to_k = ((uint64_t)1 << 32); uint32_t magic = (uint32_t)(two_to_k / d); /* * We want magic = ceil(2^k / d), but C gives us floor. We have to * increment it unless the result was exact (i.e. unless d is a power of * two). */ if (two_to_k % d != 0) { magic++; } div_info->magic = magic; #ifdef JEMALLOC_DEBUG div_info->d = d; #endif } ``` * 這邊有避免除數為 0 的狀況,在 [ISO/IEC 9899](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf) 中 > 6.5.5-5 The result of the / operator is the quotient from the division of the first operand by the second; the result of the % operator is the remainder. In both operations, if the value of the second operand is zero, the behavior is undefined. * `two_to_k` 即 $2^{32}$ * `magic` 為 $\left \lceil \dfrac{2^k}{d} \right \rceil$,但 `/` 預設是取 floor,所以在 line 24 中還要做取 ceiling 的動作 :::info Questions 1. 在這裡 `.h` file 與 `.c` file 的區分 ? 2. 在 `div.c` 中使用到 `%`,影響? ::: #### jemalloc 實驗 測試程式 ```c= int main(void) { div_info_t DIV; size_t temp; struct timespec start, end; srand(time(NULL)); printf("divisor fast_time norm_time\n"); for (size_t i = 2; i <= 10000; i++) { div_init(&DIV, i); uint64_t dividend = rand() % UINT_MAX; clock_gettime(CLOCK_MONOTONIC, &start); temp = div_compute(&DIV, dividend); clock_gettime(CLOCK_MONOTONIC, &end); long long fast_cost = (long long)(end.tv_sec * 1e9 + end.tv_nsec) - \ (long long)(start.tv_sec * 1e9 + start.tv_nsec); clock_gettime(CLOCK_MONOTONIC, &start); temp = dividend / i; clock_gettime(CLOCK_MONOTONIC, &end); long long norm_cost = (long long)(end.tv_sec * 1e9 + end.tv_nsec) - \ (long long)(start.tv_sec * 1e9 + start.tv_nsec); printf("%-7ld %-7lld %-7lld\n", i, fast_cost, norm_cost); } return 0; } ``` 下面分別比較編譯時分別使用不同最佳化的結果 * O0 ![](https://i.imgur.com/wGWcBuv.png) * O1 ![](https://i.imgur.com/vQfjt3i.png) * O2 ![](https://i.imgur.com/HbOda7S.png) * O3 ![](https://i.imgur.com/ElrQ03E.png) :::info Question 1. 與同學的實驗結果有落差 >參考同學的實驗: >[WeiCheng14159](https://hackmd.io/@WeiCheng14159/HkesfMvBP#%E5%BB%B6%E4%BC%B8%E5%95%8F%E9%A1%8C2) >[RinHizakura](https://hackmd.io/i22yIu2aTveDbrregY3Hug?view#%E6%B8%AC%E9%A9%97-3) 2. 實驗結果的震盪探討 ::: --- ## 測驗4 ```c= bool divisible(uint32_t n) { return n * M <= YYY; } ``` ```c const uint32_t D = 3; #define M ((uint64_t)(UINT64_C(XXX) / (D) + 1)) ``` `YYY = M-1` #### 題目原理解釋 * 已知 `0xFFFFFFFFFFFFFFFF / 3` = `0x5555555555555556` = `M`,又 `d` = 3,`n` 只可能有 3 種情況: `3k + 1`, `3k + 2`, `3k`,k 為正整數,分別討論 `n` 可能的 3 種狀況 * `n` = `3k`: $n * M = 2k$ * `n` = `3k + 1`: $n * M = (3k + 1) * M = 3KM + M = 2k + M$ * `n` = `3k + 2`: $n * M = (3k + 2) * M = 3KM + 2M = 2k + 2M$ * 可以得知只有 `n = 3k` 時 `n` 才能被整除,所以 `n * M = 2k` 才有可能,也可以表示成 `n * M <= M - 1` 或 `n * M < M` (`k` 有可能為 0) #### 閱讀 [Faster remainders when the divisor is a constant: beating compilers and libdivide](https://lemire.me/blog/2019/02/08/faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide/) * [note](https://hackmd.io/@hsieh22/r1XfXlP0w) --- ## 測驗5 #### 將指定的字串 (或部分字串) 的英文字母向量化地全改為小寫(一次改變8個 byte) ,使用 in-place 作法 ```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 = 0x80` `VV2 = 0` `VV3 = -1` `VV4 = 0x80` #### 題目原理解釋 * `define PACKED_BYTE(b)` `PACKED_BYTE(b)` 的作用是取出 b 的最後 8 個位元並複製 8 次形成 8 個 byte,比如說 `b` 的後 8 bits 是 `0xab`,則 `PACKED_BYTE(b)` = `0xabababababababab` * line13: 要確認是否為有效的 ASCII 字元,使用技巧同第一題,又這裡要一次處理 8 個 char (8 bytes),故將 `0x80` -> `0x8080808080808080` * line17,推測 `mask` 的作用,若大寫字母才需要做轉換,小寫字母不須轉換,字母為大寫的話 `mask` 是 `0b0010 0000` (原本是`0x80`,在 line16 做了 `>> 2` 的動作),反之是小寫字母的話 `mask` 為 `0b0000 0000` * 我們要判斷一個字母 `c` 是否在大寫字母範圍簡單的方法如下,有兩個部分, `c` > 'A' **且** `c` < 'Z' ```c c > 'A' && c < 'Z' ``` * 128 二進制表示是 `1000 0000`,這裡設計兩個變數 `A` `Z` 檢查字母是否在 `A`-`Z` 之間,變數 `A` 判斷 c 是否大於等於 'A',若 c 大於 'A' 的話 `128 - 'A' + VV2` 的 MSB 會是 1 (>127),故 `vv2 = 0` 。變數 `Z` 同理,紀錄字母 c 是否大於 'Z',但因為 'Z' 是允許的(計算`127 - 'Z'`的距離就好,故 `vv3 = -1`),大於 'Z' 的話變數 `Z` 的 MSB 才要為 1,故要再減 1 。 * 這裡只需要 `A^Z` MSB 的資訊來判斷是否在大寫字母的範圍,所以此處 mask 取 `0x1000 0000` ,取出 MSB 的資訊,來判斷要做大寫還是小寫的動作。 #### 延伸問題 ```c= #include <stdio.h> #include <string.h> int main() { /* quote from https://www.gutenberg.org/files/74/74-h/74-h.htm */ char str[] = "This eBook is for the use of anyone anywhere at no cost and with \ almost no restrictions whatsoever. You may copy it, give it away or \ re-use it under the terms of the Project Gutenberg License included \ with this eBook or online at www.gutenberg.net"; int n = strlen(str); strlower_vector(str, n); puts(str); } ``` * 若將 `char str[] ...` 改成 `char *str ...` 執行時會產生錯誤 參閱 [ISO/IEC 9899](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf) (即 C99 規格) * **6.4.5 String literals** * 第 2 點 > 6.4.5 > A character string literal is a sequence of zero or more multibyte characters enclosed in double-quotes, as in "xyz". > 5.1.1.2-6 > Adjacent string literal tokens are concatenated. * 第 5 點 > In translation phase 7, 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**. For character string literals, the `array elements` have type `char`, and are initialized with the individual bytes of the multibyte character sequence; for wide string literals, the array elements have type `wchar_t`, and are initialized with the sequence of wide characters corresponding to the multibyte character sequence, as defined by the `mbstowcs` function with an implementation-defined current locale. The value of a string literal containing a multibyte character or escape sequence not represented in the execution character set is implementation-defined. * 第 6 點 > It is unspecified whether these arrays are distinct provided their elements have the appropriate values. If the program attempts to modify such an array, the behavior is undefined. * string literal 的型態是 `array of char` * char array 會在 static storage 做初始化的動作,且被分配到的空間大小是剛好的 (大小是有加入 `null byte` 的) * 試圖修改其內容是 **undefine behavior** #### supplement :::spoiler Description of `mbstowcs` function in **7.20.8.1** > The mbstowcs function converts a sequence of multibyte characters that begins in the initial shift state from the array pointed to by s into a sequence of corresponding wide characters and stores not more than n wide characters into the array pointed to by pwcs. No multibyte characters that followanull character (which is converted into a null wide character) will be examined or converted. Each multibyte character is converted as if by a call to the mbtowc function, except that the conversion state of the mbtowc function is not affected. ::: :::spoiler Description of `wchar_t` in **7.17** > which is an integer type whose range of values can represent distinct codes for all members of the largest extended character set specified among the supported locales; the null character shall have the code value zero. Each member of the basic character set shall have a code value equal to its value when used as the lone character in an integer character constant if an implementation does not define > ::: #### `char str[]` 與 `char *str` 的差別參考: * **6.7.8 Initialization** * 第 32 點 ``` char s[] = "abc" ``` > defines **plain** char array objects `s` whose elements are initialized with character string literals. > The contents of the arrays are modifiable. 定義一個字元陣列,其內容是 string literal 所初始的,且陣列的內容是可以更改的 ``` 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. 首先會宣告一個指向 `char` 的 pointer,並將其初始化,初始化會指向一個 **字元陣列**,這個陣列是 string literal 的,如上所述,嘗試對其內容修改是 undefined behavior 的 * some other introduction of [string literal](https://www.cs.uic.edu/~jbell/CourseNotes/C_Programming/CharacterStrings.html) --- ## 測驗6 LeetCode [題目敘述](https://leetcode.com/problems/single-number-ii/),完成以下程式碼 ```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 = ~higher` `JJJ = ~lower` #### 題目原理解釋 * 這題是運用 bitwise 操作中,**每個 bits 都是獨立的個體**的特性,透過 {higher,lower} 紀錄每個 bit 出現的次數,因為每個數只會出現3次或1次,所以有三種狀態要記錄,分別是出現1次,出現2次和出現3次的,{higher,lower} 有 2 bits,其實最多可以記錄4種狀態。 * 可以利用 01 表示該 bit 出現了1次,10 表示該 bit 出現2次,00 表示出現3次,其實就是一個 Mealy 有現狀態機,當前的狀態會與輸入還有目前的狀態有關,狀態轉換如下 * current state 和 after state 分別是紀錄每個位元在該 iteration 的狀態 {`higher`, `lower`} | current state | input | after state | |:-------------:|:-----:|:-----------:| | 00 | 0 | 00 | | 00 | 1 | 01 | | 01 | 0 | 01 | | 01 | 1 | 10 | | 10 | 0 | 10 | | 10 | 1 | 00 | ```graphviz digraph{ rankdir=LR 00->01 [label=1] 01->10 [label=1] 10->00 [label=1] 00->00 [label=0] 01->01 [label=0] 10->10 [label=0] } ``` ```c lower ^= nums[i]; ``` 將 `nums[i]` 加入 `lower` (LSB),用 `xor` operation。若此 bit 是 `1` (代表前面已經出現過1次了),那會被消成 `0`,代表這個位置出現兩次了,後面要再加入 `higher` ```c lower &= ~higher; ``` 檢查 `higher` 的狀態,若 `higher` 是 `1` (表示前面已經出現過 2 次了),那最終的狀態應該要是 `0` `0`。若 `higher` 是 0 ,表示還沒出現過,所以做 `~higher` 運算可以達到這個效果 ```c higher ^= nums[i]; ``` 將 `nums[i]` 加入 `higher` (MSB),用 `xor` operation,若此 bit 是 `1` (代表前面已經出現過 2 次了,現在是第 3 次出現,這樣狀態應該要是 `0`),那會被消成 `0` ```c higher &= ~lower; ``` 檢查 `lower` 的狀態,若 `lower` 是 1 (表示這個 bit 是第 1 次出現,所以並不用加到 `higher`),那狀態應該要是 `0` `1`。若 `lower` 是 0 ,表示不是第 1 次出現,可能是第 2 或 3 次,此時上一個動作就已經幫我們做好處理了,所以做 `~higher` 運算可以達到這個效果 #### 另解 * 思路:利用一個陣列 `bitCnt[32]`,記錄每個位置的 bit 出現的次數,利用本題只會有出現3次或1次的狀況,所以可以用 `bitCnt[i] % 3` 來判斷該位置的 bit 是否是只出現一次的數字 * 缺點:另外宣告了一個 int 陣列,問題是這個陣列並不會被有效的使用,而且下面的迴圈內用到了 mod 運算,所以還有蠻多地方可以加強的 ```c= int singleNumber(vector<int>& nums) { int numSize = nums.size(); int bitCnt[32] = {0}; for (int i = 0; i < numSize; ++i) { int num = nums[i]; for (int j = 0; j < 32; ++j) { if (num & (1 << j)) bitCnt[j]++; } } int res = 0; for (int i = 0; i < 32; ++i) { if (bitCnt[i] % 3) res |= (1 << i); } return res; } ``` #### 執行結果 ![](https://i.imgur.com/qXxvory.png) #### 推廣 * 推廣到出現 n 次,可以先分成兩種狀況,出現偶數次跟奇數次 * 偶數次比較簡單,根據 `xor` operation 的特性: `A ^ A = 0`,出現偶數次的 bit 在運算時可以直接被消去,所以只需記錄兩種狀態,出現 1 次或出現偶數次,這樣一個 bit 就可以代表了,實作方式跟 [Single Nnumber](https://leetcode.com/problems/single-number/) 一樣 ```c= int evenNumber(int *nums, int numsSize) { int one = 0; for (int i = 0; i < numsSize; i++) one ^= nums[i]; return one; } ``` * 討論奇數次的狀況,若出現 n 次,理論上需要 $\lceil$ log~2~n $\rceil$ 個變數來記錄狀態 * 比較直覺的方法同上面,利用 1 個大小為 32 個 int 的 `int` 陣列 `bitCnt` 記錄每個 bit 出現的次數,最後改成 `bitCnt[i] % n` 即可,但問題也相同,可能大多數並不需要用到 32 個 int,只需要$\lceil$ log~2~n $\rceil$ 個 int 即可,在 n < 2^31^ 的情況下都能較有效的利用到 memory,同時也盡量避免 module operation * 參考 [這篇](https://leetcode.com/problems/single-number-ii/discuss/43295/Detailed-explanation-and-generalization-of-the-bitwise-operation-method-for-single-numbers) 的解說我覺得講的非常仔細,圖示也把核心概念解釋得很清楚如下: ![](https://i.imgur.com/HskKEtE.png) 其中的 `m` 就是 $\lceil$ log~2~n $\rceil$,也是所需 counter 的數量 * 這裡考慮出現 5 次的要求,因為最多會出現 5 次,所以需要 $\lceil$ log~2~5 $\rceil$ = 3 個 counter `x1`, `x2`, `x3`, bit 的轉換狀態如下: ``` 000 -> 001 -> 010 -> 011 -> 100 -> 101 -> 000 一次 兩次 三次 四次 五次 ``` 注意第 X~i~ 個 bit 從 `0 -> 1` 的過程中,X~i-1~, X~i-2~, ... , X~1~ 這些 bits 必須全為 1 才能成立,要注意實做時要 X~m~ 計算到 X~1~ ,不然會 data loss 導致進位錯誤,到這邊我們已經可以正常統計每個位置的 bit 出現的次數,但接下來會有一個問題,m bits counter 理論上會統計到 2^m^ - 1 而不是我們要的 n,根據上面的範例解釋就是該如何讓 `100 -> 000` 而不是 `100 -> 101`,所以還需要設立一個機制能統計到 m 時歸零而不是繼續累計。 這裡很巧妙的利用到了 `cutting` 的機制,當 counter 累積到 `n` 的時時候會歸零,可以透過使用一個 `mask` 對所有的 counter 做檢查,mask 是個人覺得這題最 tricky 的地方,mask 的建制如下: mask = ~ (y~1~ & y~2~ & ... & y~m~) y~i~ = x~i~ if k~i~ is 1 y~i~ = ~ x~i~ if k~i~ is 0 (k~i~ 是數字出現次數二進制表示的第 i 個 bit) 說明:假設各 counter 的狀態如下 MSB 是 `101`,即將要備歸零,而 LSB 的狀態是 `110`,各 `counter` 表示如下: ``` x1: 1xx...x1 x2: 0xx...x1 x3: 1xx...x0 ``` 做取 mask 的動作後 `MSB` 會得到 `1` (需要歸零的 signal) (1 & ~0 & 1),`LSB` 會得到 `0` (不需要歸零,所以並沒有效果)(1 & ~1 & 0),mask 再取 `~` 即可達到將 `counter` 歸零的效果 `mask` 建立完成之後再依序對每個 counter 做檢查即可 #### 程式碼 ```c= #include <stdio.h> int main(void) { int arr[] = {2,5,9,7,7,6,9,2,5,9,5,2,7,2,9,5,5,7,7,9,2}; int x1, x2, x3; x1 = x2 = x3 = 0; int size = sizeof(arr) / sizeof(int); for (int i = 0; i < size; i++) { x3 ^= (x2 & x1 & arr[i]); x2 ^= (x1 & arr[i]); x1 ^= arr[i]; /* in this situation k = 5, which is 101 */ int mask = ~(x1 & ~x2 & x3); x3 &= mask; x2 &= mask; x1 &= mask; } printf("The single number is %d\n", x1); return 0; } ``` 執行結果 ``` The single number is 6 ``` * 推廣到 r 次也是以此類推,並利用`陣列`的形式紀錄 `counter` 就可以了 ---