# 2024q1 Homework4 (quiz3+4) contributed by < `millaker` > ## 第三週 ### 第一題 #### 程式碼運作原理 本題運用 [Digit by digit caculation](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Digit-by-digit_calculation) 估計一個整數的平方根,$N^2=(a_n+a_{n-1}+...+a_m)^2$ 其中 $a_m=2^m$ 或 0。算法從 $2^n$ 依序檢查到 $2^0$ ,建構出平方根估計值 $P_m=a_0+a_1+...+a_m$。檢查的方法是比較 $P_m^2 <= N^2$,若為真則 $a_m=2^m$ 反之則為零。為了避免每次檢查需要重新計算 $P_m^2$ ,我們利用一個額外變數 $X_m = N^2 - P_m^2$ 儲存差值,並在每個回合更新。更新的大小可由以下推導得出: $$ X_m - X_{m+1} = N^2 - P_m^2 - N^2 -P_{m+1}^2$$ 移項整理後可得 $$ X_m = X_{m+1} - Y_m$$ 其中 $$Y_m=P_m^2-p_{m+1}^2=2P_{m+1}a_m+a_m^2$$ 在更進一步最佳化 $Y_m$ ,可以將其拆成前後兩項 $c_m$ 和 $d_m$: $$c_m=P_{m+1}2^{m+1}$$ $$d_m=(2^m)^2$$ $$Y_m= \begin{cases} c_m+d_m & \text{if } a_m=2^m \\ 0 & \text{if } a_m=0 \end{cases} $$ 其中 $c_m$ 和 $d_m$ 可以利用以下方法更新, $$c_{m-1} = P_m2^m= (P_{m+1}+a_m)2^m = P_{m+1}2^m + a_m2^m = \begin{cases} c_m/2+d_m & \text{if } a_m=2^m \\ c_m & \text{if } a_m=0 \end{cases} $$ 用到$P_m=P_{m+1}+a_m$和$a_m=2^m$ $$d_{m-1}=d_m/4$$ 程式碼實作中 ```c int i_sqrt(int x) { if (x <= 1) /* Assume x is always positive */ return x; int z = 0; for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= 2) { int b = z + m; z >>= 1; if (x >= b) x -= b, z += m; } return z; } ``` `z` 即 $c_m$ , `m`即 $d_m$, `m` 的更新在每一回合的 `m >>= 2`, `z` 則在 `z >>= 1` 更新為 $c_{m-1}$ 並利用 `z += m` 加上當前的 $d_m$ 。 #### 取代 `__builtin_clz` 實作 用 binary search 實作 `clz` ```c static inline unsigned clz(uint32_t x) { if (x == 0) return 32; int n = 0; if (x <= 0x0000FFFF) { n += 16; x <<= 16; } if (x <= 0x00FFFFFF) { n += 8; x <<= 8; } if (x <= 0x0FFFFFFF) { n += 4; x <<= 4; } if (x <= 0x3FFFFFFF) { n += 2; x <<= 2; } if (x <= 0x7FFFFFFF) { n += 1; x <<= 1; } return n; } ``` 每個 `if` 都會檢查一半的 `x` 是否有 1 ,若沒有則回傳值 `n` 便會加上和零一樣數量的數字,並將 `x` 位移至下個檢查位置,此實作的 `clz` 和 `__builtin_clz` 在輸入不等於零的情況回傳值相同。 ### 第二題 此題因為輸入值範圍僅有 0 ~ 19,題目描述僅需考慮計算到小數點後第一位是精確即可,透過計算 $$1.9 \leq \frac{19}{x} \leq 1.99$$,可以得出符合的 $x$ 範圍是 $$ 9.55 \leq x \leq 10$$ 從中挑選容易以二進位表達的數字 9.84 即 $\frac{128}{13}$,其中 $128 = 2^7$ , $13=2^3+2^2+2^0$。將輸入值 `tmp` 乘上 9.84 的倒數就等於除以 9.84,所以得到以下實作 ```c q = ((((tmp >> 3) + (tmp >> 1) + tmp) << 3)) >> 7; r = tmp - (((q << 2) + q) << 1); ``` 第一行中括號內 `(tmp >> 3) + (tmp >> 1) + tmp) << 3)` 可以得到 $(\frac{tmp}{8}+\frac{tmp}{2}+tmp) * 8$ 再右移 7 位得到 $\frac{13}{128}$,再利用除法原理將近似的商乘以 10,程式碼中的實作對應到 `(((q << 2) + q) << 1)`,即 $(q*4+q = 5q) *2 = 10q$,最後 `tmp` 扣除 `q` 即可得到餘數。 這裡商的誤差因為前面證明,可以確定在 0 ~ 19 的範圍內都不會影響最終結果。 而原來題目中說明要額外處理被捨棄的位元,即 ```c d0 = q & 0b1; d1 = q & 0b11; d2 = q & 0b111; q = ((((n >> 3) + (n >> 1) + n) << 3) + d0 + d1 + d2) >> 7; ``` 先假設原說明沒有寫錯字 `q` 改為 `n` ,這裏處理的方法很奇怪,若是在題目指定範圍內 0 ~ 19 則完全不需要處理,因為 `n >> 3` 在最大值 19 得出的誤差為 $(0.011)_2$ , `n >> 1` 的誤差為 $(0.1)_2$ 兩者相加等於 $(0.111)_2$ 左移三位不會影響到最後右移 7 位的結果。 ```c= #include <stdint.h> void divmod_10(uint32_t in, uint32_t *div, uint32_t *mod) { uint32_t x = (in | 1) - (in >> 2); /* div = in/10 ==> div = 0.75*in/8 */ uint32_t q = (x >> 4) + x; x = q; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; *div = (q >> 3); *mod = in - ((q & ~0x7) + (*div << 1)); } ``` 測驗題目改用其他算法,先算出 `x` 等於 $in-\frac{1}{4}in=\frac{3}{4}in$ ,再利用 `x` 得出 $\frac{3}{4}in*\frac{1}{16}+\frac{3}{4}in=\frac{102}{128}$ ,其中 $\frac{102}{128} \approx 0.7968 \approx \frac{8}{10}$ ,第 12 行可以將 $x*\frac{8}{10}*\frac{1}{8}$ 也就是向右位移三位得到近似的商,餘數則由 $in-10*q$ 得到。程式碼中 6 ~ 10 行是處理 4 5 行右移帶來的誤差,但我看了很久還是不知道怎麼解釋。 程式碼第 4 行左邊括號內 `(in | 1)` 我一樣無法看出理由,利用測試程式,我發現在輸入等於20時,有加入 `| 1` 的結果是正確的,而沒有加入的則回傳 `q:1 r:10` 的結果。 #### 對 `q` 校正 因為原本算出的 `q` 是 `in` 乘上 $\frac{102}{128} \approx 0.7968$ 和 $0.8$ 有些微偏差,實作都是使用向下位移因此可以當成無條件捨去,我們可以藉此算出有效範圍 $$ 0.8x - \frac{102}{128}x \leq 1 $$ $$ x \leq 319.999... $$ 實際使用 python 測試大於 319.99 的最小正整數: ``` >>> 320*0.8 256.0 >>> 320*102/128 255.0 ``` 兩者的結果會相差 1,而這個偏差會隨數字增加越來越大 ``` >>> (2**32-1)*0.8 3435973836.0 >>> (2**32-1)*102//128 3422552063 >>> 3435973836 - 3422552063 13421773 ``` 到 `UINT32_MAX` 偏差會大到 `13421773`,如果輸入值大於 319 ,則需要額外對 `q` 進行修正。 《[Hacker's Delight](http://web.archive.org/web/20180517023231/http://www.hackersdelight.org/divcMore.pdf)》中因為餘數不會大於除數太多,所以使用 `r / q` 或是同等運算的位移和相加來校正 `q`。這裡最大偏差量很大,且要避免使用除法操作,所以改用其他方法。 `q = (q >> 8) + x;` 重複做 4 次,每次都將 `x` 也是未校正前的 `q` 值加入運算。其中 `q >> 8` 等效於 $\frac{q}{256}$ ,為什麼是 256 還沒看出為什麼,先將等效的算式整理可以得到: $$ \frac{q}{256} + q \tag{1} $$ $$ \frac{\frac{q}{256} + q}{256} + q = \frac{q}{256^2} + (1+\frac{1}{256})q \tag{2} $$ 依序做下去會得到 $$ \frac{q}{256^3} + (1 + \frac{1}{256} + \frac{1}{256^2})q \tag{3} $$ $$ \frac{q}{256^4} + (1 + \frac{1}{256} + \frac{1}{256^2} + \frac{1}{256^3})q \tag{4} $$ 最後整理得到 $$ (1 + \frac{1}{256} + \frac{1}{256^2} + \frac{1}{256^3} + \frac{1}{256^4})q $$ 可以看出是將原本的 `q` 加上修正量 $(\frac{1}{256} + \frac{1}{256^2} + \frac{1}{256^3} + \frac{1}{256^4})q$ ,為什麼是 256 我看了很久,忘記我們的目標是要逼近 `0.8` ,若把這幾個數字列出來看便可以知曉: $$ 0.8 = (0.CCCCCCCCCC...)_{16} $$ $$ \frac{102}{128} = 0.796875 = (0.CC)_{16} $$ $$ \frac{102}{128} * (1 + \frac{1}{256}) = 0.79998779296875 = (0.CCCC)_{16} $$ $$ \frac{102}{128} * (1 + \frac{1}{256} + \frac{1}{256^2}) = 0.7999999523162842 = (0.CCCCCC00...)_{16} $$ $$ \frac{102}{128} * (1 + \frac{1}{256} + \frac{1}{256^2} + \frac{1}{256^3}) = 0.7999999998137355 = (0.CCCCCCCC00...)_{16} $$ $$ \frac{102}{128} * (1 + \frac{1}{256} + \frac{1}{256^2} + \frac{1}{256^3} + \frac{1}{256^4}) = 0.7999999999992724 = (0.CCCCCCCCCBFF...)_{16} $$ 到這裡我才知道 8 是為了讓 `q` 更接近 0.8 的最小位移量,若選用 7 會讓得到的 `q` 比 0.8 還要大。至於為什麼是做 4 次,我還沒想到一個合理的證明或解釋。 觀察上面做 3 次校正後的十六進位數字,可以看到已經有小數點後8位的準確度,一開始我想能不能把第四次校正省去,但實際使用 python 測試,在某些數字答案會有錯誤。原本的操作是 `q = (q >> 8) + x;` 在右移後會有部分位元被丟棄,造成經度損失,若丟棄的值累積大於 1 ,原本 $0x0.CCCCCCCC00$ 又剛好只會算到小數點後 8 位,造成 `q` 不精準的情況,所以一定得坐第四次避免這種事發生。 #### 比較 CPU 週期數量 選用的兩個程式碼版本一個為題目提供,另一個如下: ```c void divmod_10_noopt(uint32_t in, uint32_t *div, uint32_t *mod) { uint32_t tmp = in / 10; *div = tmp; *mod = in - tmp * 10; } ``` 編譯命令為 `gcc -O0 mod.c`,並用 `objdump` 反組譯得到 ``` 0000000000001169 <divmod_10_noopt>: 1169: f3 0f 1e fa endbr64 116d: 55 push %rbp 116e: 48 89 e5 mov %rsp,%rbp 1171: 89 7d ec mov %edi,-0x14(%rbp) 1174: 48 89 75 e0 mov %rsi,-0x20(%rbp) 1178: 48 89 55 d8 mov %rdx,-0x28(%rbp) 117c: 8b 45 ec mov -0x14(%rbp),%eax 117f: 89 c2 mov %eax,%edx 1181: b8 cd cc cc cc mov $0xcccccccd,%eax 1186: 48 0f af c2 imul %rdx,%rax 118a: 48 c1 e8 20 shr $0x20,%rax 118e: c1 e8 03 shr $0x3,%eax 1191: 89 45 fc mov %eax,-0x4(%rbp) 1194: 48 8b 45 e0 mov -0x20(%rbp),%rax 1198: 8b 55 fc mov -0x4(%rbp),%edx 119b: 89 10 mov %edx,(%rax) 119d: 8b 55 fc mov -0x4(%rbp),%edx 11a0: 89 d0 mov %edx,%eax 11a2: c1 e0 02 shl $0x2,%eax 11a5: 01 d0 add %edx,%eax 11a7: 01 c0 add %eax,%eax 11a9: 89 c1 mov %eax,%ecx 11ab: 8b 45 ec mov -0x14(%rbp),%eax 11ae: 29 c8 sub %ecx,%eax 11b0: 89 c2 mov %eax,%edx 11b2: 48 8b 45 d8 mov -0x28(%rbp),%rax 11b6: 89 10 mov %edx,(%rax) 11b8: 90 nop 11b9: 5d pop %rbp 11ba: c3 ret ``` 從組合語言可以發現就算已經將最佳化關閉 `-O0` 還是沒有使用 `div` 指令,因此我改用手計算操作數量並對照指令得出序列。去除掉兩個函式皆有的 prologue 和 epilogue 以及記憶體存取指令,僅比較數值操作指令,沒有最佳化的版本分析: | Code | Inst | Latency | Accum | | ------------- | ---- | ------- | ----- | | `in/10` | div | 10-13 | 11.5 (10-13) | | `tmp*10` | mul | 3 | 14.5 (13-16) | | `in - tmp*10` | sub | 1 | 15.5 (14-17) | 最終結果為 15.5 個週期 | Code | Inst | Latency | Accum | | --------------------------- | ---- | ------- | ----- | | `in \| 1` | or | 1 | 1 | | `in >> 2` | shr | 1 | 2 | | `(in\|1) - (in>>2)` | sub | 1 | 3 | | `x >> 4` | shr | 1 | 4 | | `(x>>4) + x` | add | 1 | 5 | | `(q>>8)` | shr | 1 *4 | 9 | | `(q>>8) + x` | add | 1 *4 | 13 | | `q >> 3` | shr | 1 | 14 | | `*div << 1` | shl | 1 | 15 | | `q & ~0x7` | and | 1 | 16 | | ` (q & ~0x7) + (*div << 1)` | add | 1 | 17 | | `in - prev` | sub | 1 | 18 | 總共 18個週期,這是非常粗略的估算,因為觀察到每個操作都是基於前面指令的結果,所以沒有使用到 recipricol throughput 也就是一個週期 issue 一個指令,沒有多個指令同時在 EXE 階段的情況。另外 x86 中 div 指令,翻閱 x86-64 instruction set reference `DIV` 指令說明 > DIV r/m32: Unsigned divide EDX:EAX by r/m32, with result stored in EAX := Quotient, EDX := Remainder 可以得知已經將餘數存在 EDX ,因此完全不需要再重新計算 `in - q*10`,直接存取 %edx 即可。 ### 第三題 ```c static size_t ilog2(size_t i) { size_t result = 0; while (i >= 65536) { result += 16; i >>= 16; } while (i >= 256) { result += 8; i >>= 8; } while (i >= 16) { result += 4; i >>= 4; } while (i >= 2) { result += 1; i >>= 1; } return result; } ``` 題目使用二分搜尋法尋找目前 MSB 的位置來判斷是否增加 `ilog2` 的結果,第一個 `while` 檢查 MSB 是否在 31:16 位元,若為真,則代表 `ilog2` 結果至少大於 16 ,因此做相對應的操作 `result += 16` 並把 `i` 向下位移 16 格以方便後續程式碼一致操作。第二個 `while` 為檢查 是否在 15:8,第三個 `while` 檢查 7:4,最後一個 `while` 則是一個一個檢查直到 `i < 2`。 第二小題 ```c int ilog32(uint32_t v) { return (31 - __builtin_clz(v | 1)); } ``` 因為 `__builtin_clz()` 再傳入 0 時未定義,因此需要 `| 1` 確保一定大於 0 ,而因為這裡是 count leading zero,因此不會影響正常輸入的結果。 ### 第四題 從下方 `ewma_init()` 我們可以得知輸入的 `weight` 和 `factor` 都必須是 2 的冪,這是因為後方將兩者都取 `ilog2()`,之後再計算新加入的值時便可以利用位移取代原本公式的乘法。 $$ x \ll y = x * 2^y$$ $$\begin{equation} S_{t} = \begin{cases} Y_0 & t = 0\\ \alpha Y_0 + (1-\alpha)S_{t-1} & t > 0\\ \end{cases} \end{equation}$$ ```c= struct ewma *ewma_add(struct ewma *avg, unsigned long val) { avg->internal = avg->internal ? (((avg->internal << avg->weight) - avg->internal) + (val << avg->factor)) >> avg->weight : (val << avg->factor); return avg; } ``` 原來 EWMA 公式是計算 $(1-\alpha)S_{t-1}$, 題目中第四行做了不一樣的操作,若直接向左位移在減掉自己,可能會有精度損失,因此先向左位移再作相同操作得到以下數學公式: $$(\frac{1}{\alpha}*S_{t-1}-S_{t-1})*\alpha = (1-\alpha)*S_{t-1}$$ 而第 6 行,因為這裡的 EWMA 都有一個隱藏的 `factor`,所以 `val` 需要乘上 `factor`。 ### 第五題 ```c int ceil_ilog2(uint32_t x) { uint32_t r, shift; x--; r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; shift = (x > 0x3) << 1; x >>= shift; return (r | shift | x > 1) + 1; } ``` 這題實作原理和測驗三相似,不過作法稍有不同,一樣都是使用二分搜尋法,這裏不用加法 `r += 16` 改用 `r = 1 << 4` 代替,效果相同。依序從 [31:16] [15:8] [7:4] [3:2] 檢查,最後再回傳值 `| x > 1` 是檢查 [1] 的位元是否為一, 因為是算 `ceil` 所以將結果加一。這題可以使用 `|` 來計算 `r` 是因為每次加入的值位數不互相影響不會有進位的問題。 #### 改寫使 `x = 0` 回傳有意義的值 上方實作因為在計算前做了 `x--` 所以當 `x` 為零時會變成 `UINT32_MAX = 0xFFFFFFFF` 得到結果為 32。如果把 `x--` 拿掉,當輸入值是 2 的冪,例如 $2^2=4$,結果會等於 $2 + 1 = 3$,並非 `log2ceil` 的正確結果。簡單的做法就是檢查輸入是否為零,但這會破壞無分支的特性,因此需要使用其他方法改寫。 若把 `log2ceil(x)` 想成需要至少需要多少個位元才能表示 `x` 的數值,則 `log2ceil(0)` 會等於 1,使用此定義將上方程式碼 `x--` 的部分改寫為 `x -= !!x` ,利用兩次 `not` 操作將非零的數值變為一,零還是零,就可以得到上方定義的結果,且程式碼仍舊無分支。 ## 第四週 ### 測驗一 從 Leetcode 題目敘述中得知: > The Hamming distance between two integers is the number of positions at which the corresponding bits are different. 而 XOR 運算剛好符合上述要求,可以透過真值表觀察: | XOR | 1 | 0 | | --- | --- | --- | | 1 | 0 | 1 | | 0 | 1 | 0 | 若輸入想同 {0,0} 和 {1,1} 則值為零,若不同則為 1,剛好就是檢查兩個位元是否相同。將兩數字進行 XOR 後,利用 `popcount` 算出數字內有幾個位元為一就可以知道有幾個位元不同。最後需要將累加結果向右位移 1 的原因是原來的寫法會將 {i,j} 和 {j,i} 都計算一次,因此需要除以二得到正確結果。而 {i,i} 的結果因為 XOR 為零不須特別處理。 #### 改進程式碼 參考題目敘述中歸納的方法,只要算出相同位數的 1 數量,就可以得到該位數貢獻給 Hamming Distance 的值。改進實作不再存取輸入陣列 $n^2$ 次,而是只存取 $32n$ 次,依序對 32 個位元算出他的貢獻 ```c int totalHammingDistance_it(int *nums, int numsSize) { int total = 0; for (int i = 0; i < 32; i++) { int accum = 0; for (int j = 0; j < numsSize; j++) { accum += (nums[j] >> i) & 1; } total += accum * (numsSize - accum); } return total; } ``` 老師上課提過考慮到執行速度太快用 `gettime()` 可能無法反應真實速度,我使用 `__rdtsc()` 存取 CPU cycle count register 進行前後比較。另外,為了確保輸入資料 `arr` 已經在快取記憶體中,先進行一次計算再開始測量。 ```c unsigned long long test_func(int *arr, int size, int (*func)(int *, int)) { // Warmup func(arr, size); unsigned long long start = __rdtsc(); func(arr, size); unsigned long long end = __rdtsc(); return end - start; } ``` 測試結果如下: ``` $ $ ./a.out pop 621651614 it 1015892 ``` 測試輸入大小為 10000 時,改進的方法快原來實作的 600 倍,新的實作只有在輸入資料小於 32 時才會比原來的慢。 ![image](https://hackmd.io/_uploads/ryp9CJ3b0.png) #### 考慮快取記憶體 稍微檢視我原本的實作尋求改進空間時,我想到若輸入資料太多,每次存取整個陣列都會造成 cache miss,若交換迴圈順序並使用 `int[32]` 陣列儲存每個位元的數量,則可以避免這個情況發生: ```c int totalHammingDistance_it_mem(int *nums, int numsSize) { int ones[32] = {0}; int total = 0; for (int i = 0; i < numsSize; i++) { for (int j = 0; j < 32; j++) { ones[j] += (nums[i] >> j) & 1; } } for (int j = 0; j < 32; j++) { total += ones[j] * (numsSize - ones[j]); } return total; } ``` 利用原本的測試方法因為輸入資料數量不足以置換快取記憶體內容,所以無法充分反應修改的優勢,改用更大的輸入資料比較。測試電腦為 r7-7700 L1 Cache : 512 KB,所以只需要 $\frac{512*10^3}{4} = 128*10^3$ 個 `int` 即可填滿快取記憶體。 我選用一個遠大於這個數量的 $1*10^8$ 的輸入大小做實驗: ``` $ ./a.out it 10028001754 itmem 11300800924 ``` 得到的測試結果不如預期,我想的到的解釋是,因為記憶體存取的順序都是固定且很好預測的,而現在 CPU 內的 data prefetcher 都能夠有效看出這種模式並預先將需要的記憶體填入 L1 快取中,為了驗證這個猜測,使用先前 lab0-c 找到的 `perferator` 觀察 cache 使用情況。 ``` +-----------------------+--------------------------------+ | Event | Count | | | (totalHammingDistance_it) | +-----------------------+--------------------------------+ | cpu-cycles | 14130703721 | | instructions | 51221528813 | | l1d-read-accesses | 12809871246 | | l1d-read-misses | 200176481 | | l1d-prefetch-accesses | 199597907 | | cache-references | 404837777 | | cache-misses | 210397 | | time-elapsed | 2.662301565s | +-----------------------+--------------------------------+ +-----------------------+--------------------------------+ | Event | Count | | | (totalHammingDistance_it_mem) | +-----------------------+--------------------------------+ | cpu-cycles | 15551051005 | | instructions | 68121105091 | | l1d-read-accesses | 15651167893 | | l1d-read-misses | 6384108 | | l1d-prefetch-accesses | 6220027 | | cache-references | 12824036 | | cache-misses | 9650 | | time-elapsed | 3.013514068s | +-----------------------+--------------------------------+ ``` 從 profiling 結果來看, cache miss 確實降低很多,但是執行時間、 CPU 週期比較多,另外也發現 `l1d-read-accesses` 數量比較多。回去更改寫法,將 `nums[j]` 先存入 `int temp` 中再降低記憶體存取次數,得到測驗結果: ``` +-----------------------+--------------------------------+ | Event | Count | | | (totalHammingDistance_it) | +-----------------------+--------------------------------+ | cpu-cycles | 14114296595 | | instructions | 51103214472 | | l1d-read-accesses | 12792397124 | | l1d-read-misses | 200284012 | | l1d-prefetch-accesses | 199776937 | | cache-references | 405361558 | | cache-misses | 203017 | | time-elapsed | 2.672375406s | +-----------------------+--------------------------------+ +-----------------------+--------------------------------+ | Event | Count | | | (totalHammingDistance_it_mem) | +-----------------------+--------------------------------+ | cpu-cycles | 15353325068 | | instructions | 52687294360 | | l1d-read-accesses | 9595242080 | | l1d-read-misses | 6314365 | | l1d-prefetch-accesses | 6230386 | | cache-references | 12752291 | | cache-misses | 10016 | | time-elapsed | 2.943651296s | +-----------------------+--------------------------------+ ``` 其中 `l1d-read-accesses` 真的降低很多,但執行時間仍然較高,有其他因素沒有被我考量到再影響結果。 #### 觀察 asm 用 `$ gcc hamming.c -O0 -c` 產生 `.o` 檔 只看軟體 C 程式碼,兩者做的操作數量是一樣的,但是由 `objdump` 觀看組合語言發現第一種作法產生 30 行的 CPU 指令,第二種考慮快取記憶體的做法產生 84 行程式碼,所以就算快取記憶體存取次數較少,第二種做法執行的 CPU 指令數量多了 2 倍以上。 ### 測驗二 #### tic-tac-toe 遊戲 #### mod 7