Yiwei 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
      • Invitee
    • 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
    • Engagement control
    • 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 Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control 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
Invitee
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
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
--- tags: 進階電腦系統理論與實作 --- # 2020q3 Homework3 (quiz3) contributed by < `RinHizakura` > >[2020q3 第 3 週測驗題](https://hackmd.io/@sysprog/2020-quiz3?fbclid=IwAR2VKM07QHTneLdK1VBa9XgZ9ca10sZsEKUgVXTXumv0PBhKUGibXLKxEe0) > [GitHub](https://github.com/RinHizakura/NCKU-System-Software-Quiz/tree/master/quiz3) ## 測驗 `1` 根據 C 語言規格書,對**有號型別的負數**算術右移(Arithmetic right shift) 在 C 語言中是 [unspecified behavior](https://en.wikipedia.org/wiki/Unspecified_behavior)。 > The result of E1 >> E2 is E1 right-shifted E2 bit positions. > ... > If E1 has a signed type and a negative value, the resulting value is implementation-defined. 意即其行為結果雖在語言規範中並無規定,但可以根據平台或者編譯器的參考文件得知。而算術右移無非就是 2 進位的最左位元補 0 或者補 1 兩種。由題意描述可知,期待的結果是如果原本數字為負數的情形下,右移後最左邊補 1。 因此,如果 `(m >> n)` 的結果已經如同預期(左邊補 1),則 `(fix ^ (fix >> n));` 可以為 0,也就是 `fix` 可以為 0。由此可以反推 `logical` 是在判斷右移的結果為何,因此 `OP1` = `(b) >>1` : * 如果 -1 算術右移最左邊補 0,(-1) >> 1 是正數,> 0 為 true,則 logical = 1 * 如果 -1 算術右移最左邊補 1,(-1) >> 1 是負數,> 0 為 false,則 logical = 0。此情況下 `fix` 必為 0,符合我們預想的 `m >> n` 的結果就如預期,不需額外更動 為求圖解精簡,圖例以 8 bits 為例,推廣到 32 bits 則同理。 即使負數右移後左邊是補 0,但在 `m` 為正數的情況下,規格書明確的說 `m >> n` 的結果會等同是最左位元補 0。 > The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of E1 / $2^{E2}$. 所以 `fix` 仍可為 0,由此可以反推 `OP2` = `(c) m < 0`。只有在滿足 1. 負數的右移結果為最左位元補 0 2. m 為負數 的情況下,`fixu` 會有真正的內容,且此時 `fixu` 會是 0x11111111,轉到 int 表示的 `fix` 就是 -1,則 `| (fix ^ (fix >> n))` 就會把右移的位元都補成 1,讓結果符合期待。 :::warning 問題: ```cpp unsigned int fixu = -(logical & (m < 0)); int fix = *(int *) &fixu; ``` 和 ```cpp int fix = -(logical & (m < 0)); ``` 兩個寫法的差異性? > 前者可避免編譯器進行過度 (aggressive) 最佳化 > :notes: jserv ::: ### 驗證正確性 因為在我的電腦上對負數算術右移是左邊補 1,為了確認程式在左邊補 0 的狀況下同樣正確,所以我就先實作了一個很在我的電腦中可以保證左邊補 0 的 right shift,再透過 `asr_i` 去比較和左邊補 1 的 right shift 結果是否有差異,檢驗 `asr_i` 是否是可以適用於跨平台的實作。 :::warning :warning: 顯然這個方法很暴力,而且也欠缺對 data model 的考量,不過因為重點只是確保 `asr_i` 的正確性所以就無傷大雅。 ::: ```c= int zero_rs(int a, int n) { for (int i = 0; i < n; i++) { a = (a >> 1) & 0x7FFFFFFF; } return a; } int asr_i(signed int m, unsigned int n) { const int logical = zero_rs(((int) -1), 1) > 0; unsigned int fixu = -(logical & (m < 0)); int fix = *(int *) &fixu; return zero_rs(m, n) | (fix ^ zero_rs(fix, n)); } ``` ### 通用的 ASR 實作可以針對不同資料寬度的 ASR。 ```c= #define asr_i(m, n) \ _Generic((m), \ int8_t: asr_i8, \ int16_t: asr_i16, \ int32_t: asr_i32, \ int64_t: asr_i64 \ )(m,n) #define asr(type) \ const type logical = (((type) -1) >> 1) > 0; \ u##type fixu = -(logical & (m < 0)); \ type fix = *(type *) &fixu; \ return (m >> n) | (fix ^ (fix >> n)) int8_t asr_i8(int8_t m, unsigned int n) { asr(int8_t); } int16_t asr_i16(int16_t m, unsigned int n) { asr(int16_t); } int32_t asr_i32(int32_t m, unsigned int n) { asr(int32_t); } int64_t asr_i64(int64_t m, unsigned int n) { asr(int64_t); } ``` 根據幾點考量而如此實作: * 根據 [data model](https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models) 的不同,`int`、`long` 等等型別的長度可能有所差異,我認為根據 bits 實際的長度選擇正確寬度的 ASR 是比較好的作法 * 透過 `u##type`([Concatenation](https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html)) 技巧連接字串 * 透過 [_Generic](https://hackmd.io/@sysprog/c-preprocessor#_Generic-C11) 達到類似 C++ template 的效果 :::warning 延伸問題: 乍想之下,無論型別都直接轉型成 64 bits 的版本處理,計算出結果後再轉回原本的型別,似乎正確性上是沒有問題的?這樣想是否完全正確呢?排除正確性的話,這樣方式的實作可能有甚麼缺點? > 不是每款 C 語言編譯器都能正確處理 64 位元,且 data model 會涉及 ABI 議題 > :notes: jserv ::: ## 測驗 `2` * `num > 0`: 因為 $4^n$ 必定 >= 1 * `(num & (num - 1))`: 可以判斷非負數的 `num` 是否為 $2^n$(n 為整數),因為如果 `num` 不是 $2^n$ 就必不為 $4^n$ * 在[你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics#%E9%81%8B%E7%94%A8-bit-wise-operator)有提及相關內容,或者可以參考[這個筆記](https://hackmd.io/9tGfpJtLTwyyOzDvlyJS2w?view#x-amp-x---1--0) * 如果是 $4^n$: | Column 1 | Column 2 | | -------- |:--------- | | $4^0$ | 0b1 | | 4 | 0b100 | | $4^2$ | 0b10000 | | $4^3$ | 0b1000000 | 在滿足前兩個條件的前提下,從右往左數到第 1 個 1 之前的 0 總數(也就是`ctz`)為偶數個,因此 `OPQ` = `(f) & 0x1` ### 不同的實作 要判斷是否是 4 的倍數,可以有許多不同的實作方式。 ```c= bool isPowerOfFour_new(int num) { return !(__builtin_popcount(num) ^ 1) && __builtin_ffs(num) & 1; } ``` 一種思路是可以擅用 popcount 和 find first set,因為要判斷是否是 $4^n$ 只要滿足: 1. 2 進位表示中僅有一個 1 2. 且此 1 的位置為奇數(注意是從 1 開始數,所以 `__builtin_ffs(1) == 1`。事實上,`ctz` 計算後其實就可以達到同等 `ffs` 的效果) 或者也可以改寫成: (注意 `&&` 左右的 statement 是不能交換的,因為這裡的小技巧是通過 `&&` 左邊如果為 0 的狀況就不會去執行右邊的特性,避免 num >> 32 這樣的非法操作。) ```c= bool isPowerOfFour_new(int num) { int ffs = __builtin_ffs(num); return (ffs & 1) && !(num >> ffs); } ``` ### [1009. Complement of Base 10 Integer](https://leetcode.com/problems/complement-of-base-10-integer/) 題解為: ```c= int bitwiseComplement(int N){ if(N == 0) return 1; int mask = (1 << (31-__builtin_clz(N)))- 1; return ~N & mask; } ``` 根據題意,目標是將足以表示該數字的 2 進位表示做 complement(例如 5 可以表示成 0b101,所以有效部份為 3 個 bit)。因此,我們只需找到這個位元數 `n`,取直接對數字做 complement 的後 `n`,其他則維持為 0,就可以得到正確答案。 我們可以透過 `clz` 來計算,透過 `(1 << (31-__builtin_clz(N)))- 1` 可以得到需要的位元位置為 1,其他為 0 的 mask(舉例來說,5 = 0b000...00101,則 mask 就會是 0b000...00111),對 5 取補數後再和 `mask` 做 and 運算,就可以得到正確的結果。 附上在 leetcode 中執行正確且通過的證明: ![](https://i.imgur.com/9ijqTI8.png) ### [41. First Missing Positive](https://leetcode.com/problems/first-missing-positive/) 題解為: ```c= #define XORSWAP_UNSAFE(a, b) \ ((a) ^= (b), (b) ^= (a), \ (a) ^= (b)) int firstMissingPositive(int* nums, int numsSize){ for(int i=0;i<numsSize;i++){ if(nums[i] > 0 && nums[i] < numsSize){ int pos = nums[i] - 1; if(pos != i && nums[i] != nums[pos]){ XORSWAP_UNSAFE(nums[i],nums[pos]); i--; } } } for(int i = 0;i < numsSize;i++){ if(nums[i] != i + 1) return i + 1; } return numsSize + 1; } ``` 此題的解題思路是:因為目標是找到最小不在陣列中的正整數解,因此我們可以經歷整個陣列,如果找到 `1` 就放在陣列的 `0` 、找到 `2`,就放在陣列的 `1`,依照此規則擺放。則再從頭檢查陣列時,如果陣列 `0` 位置不是 `1`,就表示原始的陣列中沒有 `1`,為正確答案,依照同樣的規則,就知道原始的陣列是不是有 `2`、`3`...,依此類推。如果陣列中的內容都照規則擺放,則最小的正整數解就是 `numSize + 1`。 附上在 leetcode 中執行正確且通過的證明: ![](https://i.imgur.com/ngJIqr6.png) :::warning 想不到這題用甚麼解法會需要 `clz` > 最後一個迴圈內的結束條件,可用到 `clz` > :notes: jserv ::: ## Golomb coding [Golomb coding](https://en.wikipedia.org/wiki/Golomb_coding) 是一種無損的資料壓縮方法,屬於 [Prefix code](https://en.wikipedia.org/wiki/Prefix_code) 的分組編碼系統,可以對非負整數進行編碼。其演算法流程為: 1. 根據一個定義的參數 M,對於編碼目標 N,計算出 N / M 的商 q 以及餘數 r 2. 最終的編碼會是 `<對 q 的編碼><對 r 的編碼>` 。對於 q 的編碼,使用 [unary coding](https://en.wikipedia.org/wiki/Unary_coding),對於 r 則使用 [Truncated binary encoding](https://en.wikipedia.org/wiki/Truncated_binary_encoding) 3. 因此對 q 的編碼為: q 個 1 加上 1 個 0(或者 q 個 0 加上 1 個 1) 4. 對 r 的編碼,根據情形會有不同的截斷量, 令 b = $\lfloor log_{2}M \rfloor$,則: * 如果 r < $2^{b+1}$ - M,則用 b 個 bits 表示 r 作為編碼 * 否則,用 b + 1 個 bits 表示 r + $2^{b+1}$ - M 作為編碼 因此,每一個商 q 最多可以對應 M 種結果。詳情參照 [Wikipedia 範例](https://en.wikipedia.org/wiki/Golomb_coding#Example)可以更容易理解。 ### Golomb-Rice coding Golomb-Rice coding 是 Golomb coding 的特例,規定前面提到的參數 M 必須是 $2^N$ (N 為整數),這可以帶來幾個好處: * 可以透過 bitwise operation 計算餘數(`r = N & (M - 1)`) * 對於 r 的編碼,一致取 r 二進位表示的低 $log_{2}m$ 位元來表示 ### Exponential-Golomb coding [Exponential-Golomb coding](https://en.wikipedia.org/wiki/Exponential-Golomb_coding)(order-k exp-Golomb code) 是另一種對 Golomb coding 的延伸。其演算法流程為: 1. 對於一個數字 A,去掉最低的 k 個位元後加一,得到數字 B 2. 在數字 B 的前面,加上等同 B 的位元數量減 1 個零 3. 將前面去掉的 k 個位元補回尾端 與前面的方法最大的不同點是,Exponential-Golomb coding 的每一個 group 成員數量不同,會成指數型的成長。 Reference: [Golomb 及指数哥伦布编码原理介绍及实现 ](https://www.cnblogs.com/wangguchangqing/p/6297792.html) ### x-compressor [x-compressor](https://github.com/jserv/x-compressor) 是以 Golomb-Rice coding 為基礎的資料壓縮器。這裡我們先特別關注其中 Golomb coding 相關的實作。 #### `generic_io_t` ```cpp typedef struct { uint32_t *ptr; /* pointer to memory */ void *end; uint32_t b; /* bit buffer */ size_t c; /* bit counter */ } generic_io_t; ``` 為了可以更好的理解程式碼,大致敘述 `generic_io_t` 的用途: * 編碼使用的位元數目不固定,但整數型別有固定的長寬 * 因此解法為:每次 append 新的 bit 時,對於那些不足以一個 32 位元的表示的 bit 就放在 `b` 中,並用 `c` 紀錄多少部份是那些 append 的 bit * 每當 append 滿 32 bits,就把 `b` 轉過去 `ptr` 中,並清空 b 和 c,以供新的 bit 置放(透過 `flush_buffer` 函式來完成,因為非此章重點就暫不詳細說明) #### `write_golomb` 在壓縮時,會透過 write_golomb 來填入對應的 bit ```cpp static void write_golomb(generic_io_t *io, size_t k, uint32_t N) { assert(k < 32); write_unary(io, N >> k); write_bits(io, N, k); } ``` * 對於 N / $2^k$ 的商部份,使用 unary code 編碼 * 對於 N % $2^k$ ,也就是 N / $2^k$ 餘數部份,計算相應結果後寫入 #### `write_unary` ```cpp static void write_unary(generic_io_t *io, uint32_t N) { for (; N > 32; N -= 32) write_zero_bits(io, 32); write_zero_bits(io, N); put_nonzero_bit(io); } ``` * `write_unary` 會根據商的結果寫入 q 個 0 加上一個 1 * `write_zero_bits` 一次最多允許寫 32 個 bits,因此如果要寫的總數超過,要透過 for 迴圈依次進行 * `put_nonzero_bit` 寫入一個 1 #### `write_bits` ```cpp static void write_bits(generic_io_t *io, uint32_t b, size_t n) { assert(n <= 32); for (int i = 0; i < 2; ++i) { assert(io->c < 32); size_t m = MIN(32 - io->c, n); io->b |= (b & (((uint32_t) 1 << m) - 1)) << io->c; io->c += m; if (io->c == 32) flush_buffer(io); b >>= m; n -= m; if (n == 0) return; } } ``` * 把 b 的後 n 個 bits 寫入,事實上就等同在取 `b % n` 的結果做編碼,但需考慮到餘數編碼可能會橫跨兩個 byte 儲存,需要兩個迴圈去做對應的處理 #### `read_golom` 反之將資料解壓縮 ```cpp static uint32_t read_golom(generic_io_t *io, size_t k) { uint32_t N = read_unary(io) << k; N |= read_bits(io, k); return N; } ``` * 根據 `read_unary`,從 leading zero 到第一個 1 的 prefix 可以得出原本數字的商 * 結合 read_bits 得到的餘數部份,則可以解壓縮出原本的數字 `N` 下面來檢驗一下我們對程式的理解是否有錯誤: * 假設壓縮一個文字檔,其中內容只有 `A` (ASCll = 65) 和一個換行符號 (ASCll = 10) * 對於 65 / 8 = 8 ... 1 因此對於 `A` 的編碼為 <font color="#00f">001</font><font color="#f00">100000000</font>(注意要從右往左看) * 對於 10 / 8 = 1 ... 2 因此對於換行符號的編碼為<font color="#00f">010</font><font color="#f00">10</font> * 因此整合兩個結果會是 <font color="#00f">0 1010</font> <font color="#f00">0011 0000 0000</font>,因此前 4 個 byte 會是 a300,這一致於 hexdump 輸出的結果 ```shell $ hexdump output 0000000 a300 0000 0000 0002 0000008 ``` * 最後還會對 EOF(ASCll = 256) 做編碼,這裡就不繼續說明 ### 改寫 x-compressor 為 Exponential-Golomb coding 原本的壓縮是使用 Golomb-Rice coding,這裡我們基於原始的實作,根據維基百科上對演算法的敘述,抽換壓縮技巧為 Exponential-Golomb coding。 #### `write_expgolomb` ```cpp static uint32_t reverse(uint32_t x) { x = (((x & 0xaaaaaaaa) >> 1) | ((x & 0x55555555) << 1)); x = (((x & 0xcccccccc) >> 2) | ((x & 0x33333333) << 2)); x = (((x & 0xf0f0f0f0) >> 4) | ((x & 0x0f0f0f0f) << 4)); x = (((x & 0xff00ff00) >> 8) | ((x & 0x00ff00ff) << 8)); return((x >> 16) | (x << 16)); } static void write_expgolomb(generic_io_t *io, size_t order, uint32_t N) { // 1. Encode ⌊x/2k⌋ using order-0 exp-Golomb code uint32_t B = (N >> order) + 1; assert(B != 0); int zero_write = 32 - __builtin_clz(B) - 1; // since zero_write won't larger then 32, so we can write zero bits directly write_zero_bits(io, zero_write); int total_bits = 32 - __builtin_clz(B); write_bits(io, reverse(B) >> (32 - total_bits), total_bits); // 2. Encode x mod 2k in binary uint32_t mod = N & ((1 << order) - 1); write_bits(io, mod, order); } ``` 將 `x_compress` 中兩處的 `write_golomb` 換成 `write_expgolomb`,其中有幾點需注意: * order 為多少才可以最好的壓縮我們的資料呢?之後會再根據文獻或者實驗去對參數的選擇做修改。 * 為使解碼正確,先將 `B` 反轉再編碼,如何反轉?編碼的部份如何有效的 bitwise 處理? 目前尚未周延考慮何種才是最佳的作法 下面來檢驗一下程式是否有錯誤(以 order 0 為例): * 假設壓縮一個文字檔,其中內容只有 `A`(ASCll = 65) 和一個換行符號(ASCll = 10) * 對於 65 + 1 = 0b1000010 因此對於 A 的編碼為 <font color="#00f">0100001</font><font color="#f00">000000</font>(注意要從右往左看) * 對於 10 + 1 = 0b1011 因此對於換行符號的編碼為<font color="#00f">1101</font><font color="#f00">000</font> * 因此整合兩個結果會是 <font color="#00f">1101 000</font><font color="#f00">0 1000 0100 0000</font>,因此前 5 個 byte 會是 d0840,這一致於 hexdump 輸出的結果 ```shell $ hexdump out 0000000 0840 000d 0000 0010 0000008 ``` #### `read_expgolomb` ```cpp static uint32_t read_expgolom(generic_io_t *io, size_t order) { uint32_t N = read_unary(io); uint32_t rb =read_bits(io, N); uint32_t num = reverse((rb << 1) | 1) >> (32 - N - 1); rb = read_bits(io, order); return (((num - 1) << order) | rb ) ; } ``` 解讀的部份則顯而易懂:首先藉由 0 的數量決定後續要讀出的數量,經過反轉和位移處理後則可以得到原本的數字。根據 order 的不同,後續也要做出相對應的處理。 詳細請參考 [RinHizakura/x-compressor](https://github.com/RinHizakura/x-compressor)(目前用 `make check` 測試 order = 0, 1, 2 都沒問題) :::warning TODO: 1. 依據資料特性,比較 Exponential-Golomb coding 及 Golomb-Rice coding 對資料壓縮的影響; 2. 參照 `git log`,原本 `x-compressor` 支援多個 layer,用以強化壓縮率 :notes: jserv ::: ## 測驗 `3` 思考一下各步驟在 bitwise 操作上的意義: * 如果是奇數:表示最後一個位元是 1,減 1 是把最後一個 bit 設為 0 * 如果是偶數:表示最後一個位元是 0,除 2 就是向右位移 把演算法的流程圖畫出來就像這樣: ```graphviz digraph g{ node [shape="box", fontcolor=black,fontsize=16,style=filled, fillcolor=aquamarine ] start[shape=ellipse, fontcolor=white,style=filled, fillcolor=tomato,label="start"] condition[label="last bit == 1?",shape="diamond", fontcolor=white, fillcolor=darkorange] terminate[label="total bit == 1?",shape="diamond", fontcolor=white, fillcolor=darkorange] C[label="total bit = ??",fillcolor=yellow] { rank = same A[label="right shift \n(step++, total bit -= 1)"] B[label="set last bit to 0 \n (step ++)"] end[shape=ellipse, fontcolor=white,style=filled, fillcolor=tomato] } start->C->terminate condition->B[label=" yes", fontcolor=blue,fontsize=24,color=green] B->terminate terminate:w->condition[label=" no", fontcolor=blue,fontsize=24] condition:w->A[label=" no", fontcolor=blue,fontsize=24,color=red] terminate->end[label=" yes", fontcolor=blue,fontsize=24] A->condition } ``` 終止條件是 `total bit == 1`(因為最後一步必然是 1 變成 0,表示最後一個位元不需要做 right shift),所以紅色的 edge 會走剛好整整 `total bit - 1` 次,而走過綠色 edge 的次數則是根據整個 `num` 中有幾個 1,也就是 `popcount` 在計算的數量。至於 `total bit` 實際上是多少呢?就是表達該數字需要的最少位元數量(比如說 8 = 0b1000,所以 total bit = 4),而這個數量可以根據 32 - `clz(num)` 就可以被計算出來。 所以此題的答案是 `__builtin_popcount(num) + 32 - __builtin_clz(num) - 1` = `__builtin_popcount(num) + 31 - __builtin_clz(num)` 。`AAA` = `(a) __builtin_popcount(num)`,`BBB` = `__builtin_clz(num)` ## 測驗 `4` 根據 [Greatest Common Divisor 特性和實作考量](https://hackmd.io/@sysprog/gcd-impl) 中的 [Binary GCD Algorithm](https://iq.opengenus.org/binary-gcd-algorithm/) 來閱讀程式。 * `for (shift = 0; !((u | v) & 1); shift++)` 迴圈在判斷是否 u 和 v 皆為偶數,如果 u 和 v 皆為偶數,則 gcd(u, v) = 2 * gcd(u/2, v/2) * 經過前面步驟的 u 和 v 的數值可能會改變(假設是 $u'$、$v'$),所以接下來要先解 gcd($u'$, $v'$),最後的結果再位移先前除 2 的次數 * `while (!(u & 1))` 在計算: 如果 $u'$ 為偶數,則 gcd($u'$, $v'$) = gcd($u'$/2, $v'$) * `do` 迴圈的第一個 iteration,`while (!(v & 1))` 在計算: 如果 $v'$ 為偶數,則 gcd($u'$, $v'$) = gcd($u', v'$/2) * 然後剩下的其實就是輾轉相除法(Euclidean algorithm),並且會確保進入下一個迴圈的 v' 是比較小的那個值,重複直到**進入這次輾轉相除時的 u 和 v 相同**,因此 `XXX` = `(b) v`(因為在判斷式時 v 已經是前面計算的 t) * 則這個相同的數字就是最大公因數,也就是 $u'$,再位移最開始除 2 的次數,所以 `YYY` = `(e) u << shift` ### 透過 `__builtin_ctz` 改寫 注意到下面的實驗中會將 quiz3 中題目的作法命名為 `fast gcd`,使用 `__builtin_ctz` 調整後的版本則稱為 `faster gcd` 以方便解釋。 前面的 `for (shift = 0; !((u | v) & 1); shift++)` 迴圈可以使用 `__builtin_ctz` 來改寫為如下: ```cpp #ifndef min #define min(x, y) ((x) < (y) ? (x) : (y)) #endif uint64_t faster_gcd64(uint64_t u, uint64_t v){ if (!u || !v) return u | v; int shift = min(__builtin_ctz(u), __builtin_ctz(v)); u >>= shift; v>>= shift; while (!(u & 1)) u >>= 1; do { while (!(v & 1)) v >>= 1; if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } } while (v); return u << shift; } ``` 則可以將 for 迴圈中的操作化簡,減少程式分支和對此操作的總指令數量。而為了比較兩者的效能區別而設計對應的實驗:對於相同 ctz 的 a 和 b,進行 50 次實驗並測量時間,注意一些細節: * 為了確保平等的比較兩個方法,原本的 fast gcd 中的 `/ 2` 都替換成 `<< 1` (雖然經過最佳化後可能沒有差別) * 這裡我使用了一些技巧來確保 a 和 b 可以有固定數量的 trailing zero,當然時間的比較會基於相同 a、b 來計算 * 此外,這個實驗中 a 和 b 會是同樣數量的 trailing zero,但是也該考慮 trailing zero 的不同排列組合來設計實驗(暫時還沒想到如何視覺化可以比較好的觀察結果) 實驗程式以最佳化參數 `-O2` 進行編譯,初步我嘗試用 3 維的圖形來觀察希望可以更好的視覺化實驗結果,不過似乎不是那麼理想QQ 但還是可以從圖中觀察觀察一些資訊 * 紅色的點(透過 `__builtin_ctz` 改寫的版本) 整體上會有較佳的執行時間 * 隨著數字的 ctz 越大,Euclidean algorithm 的操作次數就有機會變得更少,所以 y 軸是向 ctz of and b = 30 的地方傾斜的 ![](https://i.imgur.com/TKTNFBt.png) 重新設計實驗:對兩個方法進行 1000 次的實驗,排除標準差 2 外的 outlier 後繪制 boxplot 來視覺化實驗結果,則可以進一步看出其中差距 ![](https://i.imgur.com/wZgxS3Y.png) 可以看到藍色的 fasted_gcd 執行的效率較佳,同時也可以看出前面所說時間會往 ctz = 30 傾斜的狀況。 ## 測驗 `5` `bitmap` 中有 `bitmapsize` 個 64 位元的整數,因此可以用來表示一串 64 * `bitmapsize` 的數字。而 `naive` 程式就是在計算這些數字中哪些位置為 1,儲存在 `out` 中,而 `pos` 則是結果為 1 的總數。 在 `naive` 的解法中,我們會逐一檢查每一個 bit 是否為 1,去紀錄該位置。但事實上,如果我們把問題轉換成: **找出最右邊的 1**(顯然可以透過前面的 `ctz` 做到) + 將最右邊的 1 設成 0,則重複此步驟,我們就可以不必依序檢查每個 bits,得到相同的結果。 如何將最右邊的 1 設成 0 呢?我們知道如果 A & $\bar{A}$ = 0(每個 bit 都相異),$A$ & $(\bar{A} + 1)$ 則會得到和 A 最右邊的 1 相同位置為 1,其他為 0 的 mask `t`,與此 mask 做 XOR 運算就可以消除最右邊的 1 。因此此題答案為:`KKK` = `(e) bitset & -bitset`。

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