# F05: review contributed by < `afcidk` > * [F05: review](https://hackmd.io/s/S1-wcjavN) ## W2 測驗 2 population count 簡稱 popcount 或叫 sideways sum,是計算數值的二進位表示中,有多少位元是 1,在一些場合下很有用,例如計算 0-1 稀疏矩陣 (sparse matrix)或 bit array 中非 0 元素個數、計算兩個字串的 Hamming distance。Intel 在 2008 年 11 月 Nehalem 架構的處理器 Core i7 引入 SSE4.2 指令集,其中就有 CRC32 和 POPCNT 指令,POPCNT 可處理 16-bit, 32-bit, 64-bit 整數。 對應到 C 程式的實作: ```clike unsigned popcnt_naive(unsigned v) { unsigned n = 0; while (v) v &= (v - 1), n = -(~n); return n; } ``` 可改寫為以下常數時間的實作: ```clike unsigned popcnt(unsigned v) { unsigned n; n = (v >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; v = (v + (v >> 4)) & 0x0F0F0F0F; v *= 0x01010101; return v >> 24; } ``` ### 原理 我們先看到 ```clike n = (v >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; ``` 由 0x7 的二進位表示 0b0111 可以猜測這邊以 4 bit 為一組取出各組的 lsb 3 bit。 展開這3個部份會得到的 v 如下表示 $\begin{align} v&=(2^3a_3+2^2a_2+2^1a_1+2^0a_0)-(2^2a_3+2^1a_2+2^0a_1)-(2^1a_3+2^0a_2)-(2^0a_3) \\ &= a_3+a_2+a_1+a_0 \end{align}$ 這裡的 $a_n$ 是以 4 bit 為一組的,到了這個步驟我們已經把每 4 bit 的 population count 整理好了。v 現在表示為 $A_0A_1A_2A_3A_4A_5A_6A_7A_8$。 > $A_n$=$a_0a_1a_2a_3$ ```clike v = (v + (v >> 4)) & 0x0F0F0F0F; ``` 類似上面的步驟,只是這次我們以 **4** 個 4 bit 為單位,這裡我們將偶數單位的 population count 加到奇數單位上 ($A_1=A_1+ A_2$, $A_3=A_2+ A_3$...) 再把偶數單位清空,這時候我們得到以 8 bit 為單位的 $v=B_1B_2B_3B_4$ ``` A0 A1 A2 A3 A4 A5 A6 A7 <-- v +) A0 A1 A2 A3 A4 A5 A6| A7 <-- v>>4 &) 0 F 0 F 0 F 0 F <-- mask ------------------------------- B1 B2 B3 B4 ``` ```clike v *= 0x01010101; ``` 對 v 乘上 0x01010101,我們用直式來看 ``` B1 B2 B3 B4 x 1 1 1 1 -------------------- | B1 B2 B3 B4 B1 | B2 B3 B4 B1 B2 | B3 B4 B1 B2 B3 | B4 ---------------------------- will be cut|32 bit unsigned integer ``` 發現在 第 8\*3=24 bit 的地方會得到 $B_1+B_2+B_3+B_4$,因為我們使用的是 32 bit 無號整數,而 $B$ 以 8 bit 為一單位,後面的數字會被截斷因此不需要做特別處理。 取得 population count 為 `v>>24` ### 應用場景 ## W2 測驗 3 考慮以下程式碼: ```clike= #include <stdio.h> #define cons(x, y) (struct llist[]){{x,y}} struct llist { int val; struct llist *next; }; int main() { struct llist *list = cons(9, cons(5, cons(4, cons(7, NULL)))); struct llist *p = list; for (; p; p = p->next) printf("%d", p->val); printf("\n"); return 0; } ``` 我們需要補完的是 cons 的巨集,我們看到在第 7 行的地方被包了 4 層 `cons`。因為我們直接把展開後的 `cons(.., cons(.., ....))` 指派給 list,所以可以知道展開後的巨集應該是 compound literal 或是有需要的話再加上 designated initializer。 > As a GNU extension, GCC allows **initialization of objects with static storage** duration by compound literals (which is not possible in ISO C99, because the initializer is not a constant). 如果我們想要在 compound literal 中配置空間給 structure 中的 `*next` 的話,我們必須要在前面加上 `(struct llist[])` 告訴 compiler 這個地方需要配置空間。 :::info 找不太到相關資料為什麼在巨集的地方需要兩個大括號,只用一個括號會出現 -Wmissing-braces 警告 ::: ## W3(上) 測驗 2 考慮以下計算平方根的程式: ```clike #include <stdint.h> /* The algorithm uses the property that computing x² is trivial compared to * sqrt. It will search the biggest x so that x² <= v, assuming we compute * sqrt(v). */ int32_t sqrt_I2I(int32_t v) { uint32_t r = v; // r = v - x² uint32_t b = 0x40000000; // a² uint32_t q = 0; // 2ax while (b > 0) { uint32_t t = q + b; // t = 2ax + a² q >>= 1; // if a' = a/2, then q' = q/2 if (r >= t) { // if (v - x²) >= 2ax + a² r -= t; // r' = (v - x²) - (2ax + a²) q += b; // if x' = (x + a) then ax' = ax + a², // thus q' = q' + b } b >>= 2; // if a' = a/2, then b' = b / 4 } return q; } ``` 該函式利用 $(x + a)^2 = x^2 + 2ax + a^2$ 漸進找出平方根。不過上面的程式僅能處理整數,我們引入 fixed point 來表達浮點數,改寫程式碼如下: ```clike= /* A fixed point is a 32 bit value with the comma between * the bits 15 and 16, where bit 0 is the less significant bit of the value. * FIXME: Larger numbers might cause overflow and rounding error. */ typedef int32_t fixed; fixed sqrt_I2F(int32_t v) { if (!v) return 0; uint32_t r = v; uint32_t b = 0x40000000; uint32_t q = 0; while (b > 0) { uint32_t t = q + b; if (r >= t) { r -= t, q = t + b; } r<<=1, b>>=1; } if (r >= q) ++q; return q; } ``` ### 原理 我們先理解 `sqrt_I2I` 的運作方式。 給定一個 32 bit 無號數 v,回傳一個無條件設去小數點的整數。 我們設定兩個變數 a, x。a 的初始值是 0x40000000,往下收斂到 0。x 的初始值是 0,會慢慢變大,直到 $(a+x)^2 \gt v$,這時候我們就可以得到 $x \approx sqrt(v)$ 在 while 迴圈中,我們每個 iteration 都會將範圍除以二減少搜尋範圍,當發現 $v-x^2 \ge a^2+2ax$ 的時候,代表 $(a+x)^2 \le v$ 有成立,這時候我們更新 a 以及 x 的值,但是還不能停下來,因為我們希望 $x^2$ 最逼近 $v$。 當 $a$ 變成 0 的時候,代表我們沒辦法繼續逼近了,於是回傳最後的 x 值。 再來以 `sqrt_I2I` 的概念擴展成 fixed point 版本,從程式碼上面看可以發現其實我們不需要做太多更改,唯一的差異就是在每次該如何收斂 a 以及 x。 看到關鍵的收斂方式 ```clike r <<= 1; b >>= 1; ``` r 每次會向左位移,相對來說我們可以視為小數點往右位移了。這樣有點像是二進位版本的[十分逼近法](https://medium.com/@pakls/%E5%8D%81%E5%88%86%E9%80%BC%E8%BF%91%E6%B3%95%E9%80%9F%E7%AE%97%E6%B3%95-46c7a29f4ff6),我們每次都會往下一個小數點逼近。 ### 修正 overflow 可能發生 overflow 的地方在第 17 行的 `r <<= 1` 中,我們每次將 r 向右位移為了得到更精準的小數。因為整數的表示位數有限,向又位移可能就會導致 overflow 的發生。 上面的程式碼沒有修正這個問題, 舉例來說,當我們輸入 2000000000 時,會得到 -23923.929688 (轉換回浮點數後)。 第1個解法是我們可以先將 `fixed` type 改為 `uint32_t`,因為我們不預期應該得到負數,如果得到負數就是從 overflow 得到的,又因為我們把他視為有號整數,所以才會出錯。 第2個方式是我們可以將 r 的型態從 `uint32_t` 改為 `uint64_t`,輸入的型態依然使用 `int32_t`,透過這樣的方式,我們可以確保 r 向右位移不會產生 overflow,因為演算法中,我們最多位移 30 次,加上 32 bit 的整數並不會產生 overflow。 > 30 = log2(0x40000000) > 30 + 32 = 62 bits < 64 bits 實際在程式碼驗證時,我們可以在 `sqrt_I2F` 中加上 `assert(r < r<<1);` 並讓函式的參數跑過整個 32 bit 大小。