Jacob Lin
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 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

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully