# 重做 quiz3 測驗二的延伸問題,實作資料壓縮器並提出改進 x-compressor 的方案 contributed by < `blueskyson` > ###### tags: `linux2020` ## [quiz3](https://hackmd.io/@sysprog/2020-quiz3) 測驗二的延伸問題 在 [C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics) 中,我們介紹過 power of 2 (漢字可寫作「二的冪」)。 [GCC extension](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 其中兩個是 ctz 和 clz: >Built-in Function: `int __builtin_ctz (unsigned int x)` >- Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined. >Built-in Function: `int __builtin_clz (unsigned int x)` >- Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined. 我們可用 `__builtin_ctz` 來實作 [LeetCode 342. Power of Four](https://leetcode.com/problems/power-of-four/) ,假設 int 為 32 位元寬度。實作程式碼如下: ```cpp= bool isPowerOfFour(int num) { return num > 0 && (num & (num - 1))==0 && !(__builtin_ctz(num) OPQ); } ``` ### 1. 解釋上述程式運作原理 首先第一個判斷條件是 `num > 0` 不用多作解釋。 因為 2 的冪次有以下特性: - 2 = `00010`, (2 - 1) = `00001`, 2 & (2 - 1) = `0` - 4 = `00100`, (4 - 1) = `00011`, 4 & (4 - 1) = `0` - 8 = `01000`, (8 - 1) = `00111`, 8 & (8 - 1) = `0` - 16 = `10000`, (16 - 1) = `01111`, 16 & (16 - 1) = `0` 所以 `(num & (num - 1))==0` 可用來判定 2 的冪次 最後,我們需要從所有 2 的冪次中挑出 4 的冪次。觀察上面 2 的冪次的規律,可以發現只要是 4 的冪次,其 trailing 0-bits 的個數都為偶數,如: 4 的 trailing 0-bits 有 2 個、 16 的 trailing 0-bits 有 4 個。 如果 `num` 的 trailing 0-bits 為偶數,則 `!(__builtin_ctz(num) OPQ)` 回傳 `TRUE` ,很明顯的 ==OPQ== 為 `& 0x1` 。 ### 2. 改寫上述程式碼,提供等價功能的不同實作,儘量降低 branch 的數量 首先使用 perf 分析本題解法的 branch 數量,我用以下程式碼做測試 ```c= #include <stdbool.h> #include <stdlib.h> bool isPowerOfFour(int num) { return num > 0 && (num & (num - 1))==0 && !(__builtin_ctz(num) & 0x1); } int main(int argc, char *argv[]) { int n = atoi(argv[1]); isPowerOfFour(n); return 0; } ``` 編譯,並將執行檔命名為 `pow4` 後使用以下腳本計算 branch 數量: ```non $ cat test.sh for ((i = 0; i < 32; i++)) do ((num = 1<<i)) perf stat -r 10 -e branches -o "perf/$num" ./pow4 $num grep branches "perf/$num" >> branch.txt done ``` 執行完這份 shell script ,會得到一份 branch.txt ,紀錄 $2^{0}$, $2^{1}$,..., $2^{31}$ 所用的 branch 數量,可以觀察出原版程式所產生的 branch 數量介於 135000 至 138000 之間: ``` $ tr -d ' ,' < branch.txt 137565branches(+-0.53%) 137251branches(+-0.41%) 135862branches(+-0.53%) 136473branches(+-0.76%) 136823branches(+-0.50%) 136616branches(+-0.55%) 136150branches(+-0.56%) 137170branches(+-0.53%) 136848branches(+-0.34%) 137641branches(+-0.50%) 136450branches(+-0.56%) 135561branches(+-0.38%) 135748branches(+-0.40%) 136889branches(+-0.36%) 137397branches(+-0.45%) 138100branches(+-0.64%) 137186branches(+-0.50%) 135619branches(+-0.51%) 136723branches(+-0.46%) 137738branches(+-0.37%) 135568branches(+-0.61%) 136818branches(+-0.37%) 136434branches(+-0.51%) 135632branches(+-0.45%) 136480branches(+-0.56%) 137798branches(+-0.48%) 136101branches(+-0.35%) 136059branches(+-0.43%) 136315branches(+-0.57%) 136510branches(+-0.64%) 135684branches(+-0.42%) ``` 接下來我寫了一個等價功能的實作: 首先檢查 `num` 中為 1 的 bit 的數量是否為 1 ,然後檢查 trailing zero 的數量是否為偶數,如果兩個條件都符合就回傳 `true`。 ```c= #include <stdbool.h> #include <stdlib.h> bool isPowerOfFour(int num) { return __builtin_popcount(num) == 1 && num & 0b01010101010101010101010101010101; } int main(int argc, char *argv[]) { int n = atoi(argv[1]); isPowerOfFour(n); return 0; } ``` 一樣編譯成 `pow4` 後透過 shell script 執行,印出檔案,平均 branch 數量介於 135700 至 138000 之間,沒有把 branch 數量壓得更低: ``` 136751branches(+-0.50%) 136739branches(+-0.52%) 137065branches(+-0.54%) 138113branches(+-0.45%) 136818branches(+-0.44%) 137006branches(+-0.38%) 136894branches(+-0.51%) 137206branches(+-0.51%) 135915branches(+-0.38%) 137785branches(+-0.53%) 137061branches(+-0.42%) 135992branches(+-0.49%) 136021branches(+-0.39%) 137311branches(+-0.53%) 136040branches(+-0.35%) 137076branches(+-0.47%) 137580branches(+-0.37%) 136060branches(+-0.39%) 136346branches(+-0.44%) 137884branches(+-0.53%) 137547branches(+-0.39%) 136204branches(+-0.40%) 136971branches(+-0.44%) 136752branches(+-0.55%) 137632branches(+-0.52%) 137220branches(+-0.51%) 137152branches(+-0.60%) 136830branches(+-0.47%) 136015branches(+-0.44%) 136820branches(+-0.27%) 135756branches(+-0.45%) ``` ## 練習 [LeetCode 1009. Complement of Base 10 Integer](https://leetcode.com/problems/complement-of-base-10-integer/) ### 題目描述 Every non-negative integer `N` has a binary representation. For example, `5` can be represented as `"101"` in binary, `11` as `"1011"` in binary, and so on. Note that except for `N = 0`, there are no leading zeroes in any binary representation. The complement of a binary representation is the number in binary you get when changing every `1` to a `0` and `0` to a `1`. For example, the complement of `"101"` in binary is `"010"` in binary. For a given number `N` in base-10, return the complement of it's binary representation as a base-10 integer. **Example 1:** **Input:** `5` **Output:** `2` **Explanation:** `5` is `"101"` in binary, with complement `"010"` in binary, which is `2` in base-10. **Example 2:** **Input:** `7` **Output:** `0` **Explanation:** `7` is `"111"` in binary, with complement `"000"` in binary, which is `0` in base-10. **Example 3:** **Input:** `10` **Output:** `5` **Explanation:** `10` is `"1010"` in binary, with complement `"0101"` in binary, which is `5` in base-10. ### 解法 ```c= int bitwiseComplement(int N){ if (N == 0) return 1; unsigned int mask = 0xffffffff << (32 - __builtin_clz(N)); return ~(N | mask); } ``` ### 思路 以 `N = 5` 為例,其二進制表示為 `"101"` ,我想要求得 `5` 的補數,也就是 `"010"` 然而在 32 位元的 `int` 中, `5` 會表示成 `"00...00101"` ,直接對其取補數會得到 `"11...11010"` ,其二補數表示為 `-6` 不是我們想要的結果 所以我要做的事為: 將 N 的 leading zero 全部變成 `1` ,再進行取補數,也就是將 `"00...00101"` 變換為 `"11...11101"` 然後反轉為 `"010"` 即為正確答案 逐步解釋程式碼的意義: ``` Suppose N = 5 then: 1. s = 32 - __builtin_clz(N) = 3 2. mask = 0xffffffff << s = "11111111111111111111111111111000" 3. N | mask = "11111111111111111111111111111101" 4. ~(N | mask) = "00000000000000000000000000000010" #solution ``` ### 提交 ![](https://i.imgur.com/1TNf2Vq.png) ## 練習 [LeetCode 41. First Missing Positive](https://leetcode.com/problems/first-missing-positive/) ### 題目描述 Given an unsorted integer array `nums`, find the smallest missing positive integer. **Follow up:** Could you implement an algorithm that runs in `O(n)` time and uses constant extra space? **Example 1:** **Input:** `nums = [1,2,0]` **Output:** `3` **Example 2:** **Input:** `nums = [3,4,-1,1]` **Output:** `2` **Example 3:** **Input:** `nums = [7,8,9,11,12]` **Output:** `1` ### 解法 ```c= void sort(int arr[], int front, int end) { if(front<end) { int i, index=front; for(i=front; i<end; i++) { if(arr[i]<arr[end]) { int tmp=arr[i]; //swap arr[i]=arr[index]; // arr[index]=tmp; // index++; } } int tmp=arr[i]; //swap pivot arr[i]=arr[index]; // arr[index]=tmp; // sort(arr,front,index-1); sort(arr,index+1,end); } } int firstMissingPositive(int* nums, int numsSize){ sort(nums, 0, numsSize - 1); int i = 0, min = 1; while(i < numsSize) { if (nums[i] == min) min++; else if (nums[i] > min) break; i++; } return min; } ``` ### 思路 先對 `nums` 做 quick sort ,然後將 `min` 設為 `1` ,從排序好的 `nums` 中依序檢查由 `1` 開始的連續正整數,每當檢查到一個連續正整數便將 `min` 加 1 ,否則跳出迴圈並回傳 `min` ### 提交 ![](https://i.imgur.com/zBuVfJP.png) ### 另一種解法 使用 bit array ,還沒實作 ## 研讀 [2017 年修課學生報告](https://hackmd.io/@3xOSPTI6QMGdj6jgMMe08w/Bk-uxCYxz) ,理解 `clz` 的實作方式,並舉出 [Exponential Golomb coding](https://en.wikipedia.org/wiki/Exponential-Golomb_coding) 的案例說明 >[x-compressor](https://github.com/jserv/x-compressor) 是個以 [Golomb-Rice coding](https://en.wikipedia.org/wiki/Golomb_coding) 為基礎的資料壓縮器,實作中也用到 clz/ctz 指令,可參見 [Selecting the Golomb Parameter in Rice Coding](https://ipnpr.jpl.nasa.gov/progress_report/42-159/159E.pdf)。 ### `clz` 效能測試 [2017 年修課學生報告](https://hackmd.io/@3xOSPTI6QMGdj6jgMMe08w/Bk-uxCYxz) 列出了 8 種 `clz` 的實做,分別為: - binary search (iterative) - binary search (bit mask) - binary search (byte shfit) - binary search (recursive) - De Bruijn method - Harley’s algorithm - Harley’s algorithm inline assembly - binary search (recursive) inline assembly 因為不知道[2017 年修課學生報告](https://hackmd.io/@3xOSPTI6QMGdj6jgMMe08w/Bk-uxCYxz)效能分析的原始碼,而且也不確定我的 CPU 的行為會不會符合他們效能分析的結果,所以我先用自己的方法測試一次,測試環境: - CPU: Intel i7-10750H - OS: Ubuntu 20.04 LTS 我使用 `rdtsc` 取得 clz 運算前後的 time stamp ,再將其相減取得 cycle 數,即下方程式碼的 `end - begin` 。測試時,將參數 `argv[1]` 作為 2 的冪次傳入並將其轉成 `int` 並存入 `shift` ,我針對每個 $2^{shift}$ 作為 `clz` 的 input 測試。 以 binary search (iterative) 為例,其測試程式碼如下: ```c= #include <stdio.h> #include <stdlib.h> #include <stdint.h> #pragma optimize( "", off ) int clz(uint32_t x) { // l := left, r := right uint32_t n = 32, c = 16, l, r = x; do { l = r >> c; if (l) { n -= c; r = l; } c >>= 1; } while (c); return (n - r); } int main(int argc, char* argv[]) { int shift = atoi(argv[1]); /* input */ uint32_t x = 1 << shift; uint64_t begin, end; uint32_t begin_lo, begin_hi, end_lo, end_hi; __asm__ __volatile__ ("rdtsc" : "=a" (begin_lo), "=d" (begin_hi)); /* clz implementation */ clz(x); __asm__ __volatile__ ("rdtsc" : "=a" (end_lo), "=d" (end_hi)); begin = ((uint64_t)begin_hi << 32) | begin_lo; end = ((uint64_t)end_hi << 32) | end_lo; printf("%d %lu\n", shift, end - begin); return 0; } ``` 將 binary search (iterative) 的測試程式碼命名為 `clz1.c` ,編譯為 `clz1` 並藉由以下 shell script 對 `clz1` 至 `clz8` 進行效能測試,測試方式為對 $2^0$ 至 $2^{31}$ 分別執行 100 次 `clz`: ```non rm clz*.txt for (( i = 0; i < 32; i++ )) do echo "testing for 1 << $i" for (( j = 0; j < 100; j++)) do ./clz1 "$i" >> clz1.txt ./clz2 "$i" >> clz2.txt ./clz3 "$i" >> clz3.txt ./clz4 "$i" >> clz4.txt ./clz5 "$i" >> clz5.txt ./clz6 "$i" >> clz6.txt ./clz7 "$i" >> clz7.txt ./clz8 "$i" >> clz8.txt done done ``` 再來用 gnuplot 畫出 2 的冪次對 cpu cycle 的 scatter ,以下是 gnuplot 指令: :::spoiler ```non set size 0.5, 0.5 set term png size 900, 900 set output "figure1.png" set multiplot layout 2, 2 rowsfirst title "Binary Search CLZ" font ",18" set xlabel "power of 2" font ",10" set ylabel "rdtsc cycles" font ",10" set xtics font ",10" set ytics font ",10" # unset key set title "iterative" font ",14" plot [-1:32][:500] 'clz1.txt' with points pt 7 ps 1 lt rgb "red" # unset key set title "bit mask" font ",14" plot [-1:32][:500] 'clz2.txt' with points pt 7 ps 1 lt rgb "orange" # unset key set title "byte shfit" font ",14" plot [-1:32][:500] 'clz3.txt' with points pt 7 ps 1 lt rgb "blue" # unset key set title "recursive" font ",14" plot [-1:32][:500] 'clz4.txt' with points pt 7 ps 1 lt rgb "violet" unset multiplot ``` ::: ![](https://i.imgur.com/GuowsEi.png) 上圖 Binary Search 表示的結果大致和 [2017 年修課學生報告](https://hackmd.io/@3xOSPTI6QMGdj6jgMMe08w/Bk-uxCYxz) 測試的結果差不多,效能由優至劣排序依序是: $$\text{bit mask} \approx \text{byte shift} > \text{iteration} > \text{recursive}$$ 接下來再看另外 4 種 `clz` 的時間分佈圖: :::spoiler ```non set size 0.5, 0.5 set term png size 900, 900 set output "figure2.png" set multiplot layout 2, 2 rowsfirst title "Other CLZ" font ",18" set xlabel "power of 2" font ",10" set ylabel "rdtsc cycles" font ",10" set xtics font ",10" set ytics font ",10" # unset key set title "De Bruijn method" font ",14" plot [-1:32][:500] 'clz5.txt' with points pt 7 ps 1 lt rgb "red" # unset key set title "Harley’s algorithm" font ",14" plot [-1:32][:500] 'clz6.txt' with points pt 7 ps 1 lt rgb "orange" # unset key set title "Harley’s algorithm asm" font ",14" plot [-1:32][:500] 'clz7.txt' with points pt 7 ps 1 lt rgb "blue" # unset key set title "bnary search (recursive) asm" font ",14" plot [-1:32][:500] 'clz8.txt' with points pt 7 ps 1 lt rgb "violet" unset multiplot ``` ::: ![](https://i.imgur.com/R46iIK4.png) 可以發現 De Bruijn method 是 8 種方法中速度最慢的,我推測其原因為函式內使用了一次乘法拖慢了速度,此外,將 Harley’s algorithm 和 binary search (recursive) 用組合語言實做的確有改善效能,不過具體改善多少需要比較他們的平均。 以上實驗是以 2 的冪次作為 input ,會有盲點,未來可能需要加一些隨機性較高的輸入值來觀察這些 `clz` 的實做是否會表現其他特性。 我順手也測試了 `__builtin_clz` 所耗的時間,發現速度比上述實做都要快得多 ![](https://i.imgur.com/KPSneGW.png) 我將 `__builtin_clz` 包在函式 `clz` 中,並且用 gcc -S 編譯,查看其 x86 的實做: ```c // c implementation int clz(int x) { return __builtin_clz(x); } ``` ```asm ; x86 assembly implementation clz: .LFB6: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -4(%rbp) movl -4(%rbp), %eax ; load input value bsrl %eax, %eax ; get the position of most significant set bit xorl $31, %eax ; get complement of eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc ``` 由上面 x86 實做可以看出和 `__builtin_clz` 有關的指令只有 3 行,所以會比 2017 報告中實做的快,至於 `bsrl` 是否有獨立的電路或是有被硬體優化則需要再探討。 最後我畫出 `clz` 消耗的 cycle 的 10% trimed mean 比較圖: ![](https://i.imgur.com/tywCIqy.png) ### Exponential Golomb coding 摘錄自維基百科,[Exponential-Golomb coding](https://en.wikipedia.org/wiki/Exponential-Golomb_coding) (或稱 Exp-Golomb code) 是一種 [Universal code](https://en.wikipedia.org/wiki/Universal_code_(data_compression)),也就是 Exp-Golomb code 能夠映射到所有正整數。 假設輸入為 `x` ,其編碼步驟為: 1. 以二進位寫下 `x + 1` 2. 數 `x + 1` 的二進位數字總共有多少位數,並在 `x + 1` 前面補上等同其位數減 1 的數量的 `0` **Example 1:** ``` x = 5 1. Write down 5 + 1 = 6 as binary "110". 2. "110" has 3 digits, we write 2 "0"s preceding "110". The Exp-Golomb code of 5 is "00110". ``` **Example 2:** ``` x = 24 1. Write down 24 + 1 = 25 as binary "11001". 2. "11001" has 5 digits, we write 4 "0"s preceding "11001". The Exp-Golomb code of 24 is "000011001". ``` 按照上述規則計算 0 ~ 8 的 Exp-Golomb code: ``` x step 1 step 2 0 ⇒ 1 ⇒ 1 1 ⇒ 10 ⇒ 010 2 ⇒ 11 ⇒ 011 3 ⇒ 100 ⇒ 00100 4 ⇒ 101 ⇒ 00101 5 ⇒ 110 ⇒ 00110 6 ⇒ 111 ⇒ 00111 7 ⇒ 1000 ⇒ 0001000 8 ⇒ 1001 ⇒ 0001001 ``` 如果要讓 Exp-Golomb code 編碼**有號**整數,只需要讓有號數一一對應無號數即能達到目的: ``` x map step 1 step 2 0 ⇒ 0 ⇒ 1 ⇒ 1 1 ⇒ 1 ⇒ 10 ⇒ 010 −1 ⇒ 2 ⇒ 11 ⇒ 011 2 ⇒ 3 ⇒ 100 ⇒ 00100 −2 ⇒ 4 ⇒ 101 ⇒ 00101 3 ⇒ 5 ⇒ 110 ⇒ 00110 −3 ⇒ 6 ⇒ 111 ⇒ 00111 4 ⇒ 7 ⇒ 1000 ⇒ 0001000 −4 ⇒ 8 ⇒ 1001 ⇒ 0001001 ``` 我們可以按照順序,每 $2^i$ 個 Exp-Golomb code 分為一組。對於每個編碼,前面有 $i$ 個 `0` 就代表該編碼屬於 $S_i$ ,每個編碼正中央紅色的 `1` 作為分界點,後綴的編碼以二進位表示為第 $S_i$ 組的第 $j$ 個元素,記為 $G_{i,j}$ : $\ S_0 \quad \quad \quad S_1 \quad \quad \quad \quad \quad \quad \quad \quad \ \ S_2 \\ \{\color{red}1\},\ \{0\color{red}10,\ 0\color{red}11\},\ \{00\color{red}100,\ 00\color{red}101,\ 00\color{red}110,\ 00\color{red}111\},\ ... \\ G_{0,0} \ \ \ G_{1,0} \ \ \ G_{1,1} \quad \quad G_{2,0} \quad \ \ G_{2,1} \quad \ \ G_{2,2} \quad \ G_{2,3}$ ### Order-k Exponential Golomb coding 接下來,對 Exp-Golomb code 的編碼方式一般化,前綴的 `0` 的數量一樣代表該編碼在第幾組,但是每一組的元素數量變為 $2^{i+k}$ 個,後綴的編碼也加長 $k$ 位,我們將這種編碼方式稱為 $k$ 階 Exp-Golomb code。以下為範例: **order-0** $\ S_0 \quad \quad \quad S_1 \quad \quad \quad \quad \quad \quad \quad \quad \ \ S_2 \\ \{\color{red}1\},\ \{0\color{red}10,\ 0\color{red}11\},\ \{00\color{red}100,\ 00\color{red}101,\ 00\color{red}110,\ 00\color{red}111\},\ ... \\ G_{0,0} \ \ \ G_{1,0} \ \ \ G_{1,1} \quad \quad G_{2,0} \quad \ \ G_{2,1} \quad \ \ G_{2,2} \quad \ G_{2,3}$ **order-1** $\ \quad S_0 \quad \quad \quad \quad \quad \quad \quad S_1 \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad S_2 \\ \{\color{red}10,\ \color{red}11\},\ \{0\color{red}100,\ 0\color{red}101, \ 0\color{red}110,\ 0\color{red}111\},\ \{00\color{red}1000,\ 00\color{red}1001,\ 00\color{red}1010,\ ... \\ G_{0,0} \ \ G_{0,1} \quad \ G_{1,0} \quad G_{1,1} \quad G_{1,2} \quad G_{1,3} \quad \quad G_{3,0} \quad \quad \ G_{3,1} \quad \quad G_{3,2}$ **order-2** $\ \quad S_0 \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad S_1 \\ \{\color{red}100,\ \color{red}101,\ \color{red}110,\ \color{red}111\},\ \{0\color{red}1000,\ 0\color{red}1001, \ 0\color{red}1010,\ 0\color{red}1011,\ 0\color{red}1100,\ 0\color{red}1101,\ 0\color{red}1110,\ 0\color{red}1111\},\ ...\\ G_{0,0} \ \ \ G_{0,1} \ \ G_{0,2} \ \ G_{0,3} \quad \ \ \ G_{1,0} \quad \ \ G_{1,1} \quad \ \ G_{1,2} \quad \ \ G_{1,3} \quad \ \ G_{1, 4} \quad \ \ G_{1, 5} \quad \ \ G_{1, 6} \quad \ G_{1, 7}$ 產生 $k$ 階 Exp-Golomb code 的步驟為: 1. 以二進位寫下 $x+2^k$ 2. 數 $x+2^k$ 的二進位數字總共有多少位數,並在 $x+2^k$ 前面補上等同其位數減 $(k+1)$ 的數量的 `0` 以下是 order-0 到 order-4 的前 13 個編碼: |x |order-0 |order-1 |order-2 |order-3 |order-4 | |--|-----------|-----------|-----------|-----------|-----------| |0 |**1** |**1**0 |**1**00 |**1**000 |**1**0000 | |1 |0**1**0 |**1**1 |**1**01 |**1**001 |**1**0001 | |2 |0**1**1 |0**1**00 |**1**10 |**1**010 |**1**0010 | |3 |00**1**00 |0**1**01 |**1**11 |**1**011 |**1**0011 | |4 |00**1**01 |0**1**10 |0**1**000 |**1**100 |**1**0100 | |5 |00**1**10 |0**1**11 |0**1**001 |**1**101 |**1**0101 | |6 |00**1**11 |00**1**000 |0**1**010 |**1**110 |**1**0110 | |7 |000**1**000|00**1**001 |0**1**011 |**1**111 |**1**0111 | |8 |000**1**001|00**1**010 |0**1**100 |0**1**0000 |**1**1000 | |9 |000**1**010|00**1**011 |0**1**101 |0**1**0001 |**1**1001 | |10|000**1**011|00**1**100 |0**1**110 |0**1**0010 |**1**1010 | |11|000**1**100|00**1**101 |0**1**111 |0**1**0011 |**1**1011 | |12|000**1**101|00**1**110 |00**1**0000|0**1**0100 |**1**1100 | ### 實做 Exponential Golomb coding **repo:** https://github.com/blueskyson/Exponential-Golomb-coding **如何使用:** 首先將 repo 中的 `exp-golomb.c` 編譯成 `encode` ,再將 `decode` 連結至 `encode` 即完成編譯: ``` $ gcc exp-golomb.c -o encode $ ln -s encode decode ``` `encode` 和 `decode` 的參數格式如下,以下將 `sample_text.txt` 轉成 order-4 exp-golomb code 的方法: ``` # Usage encode [input file] [output file] [order-k] decode [input file] [output file] [order-k] # Example ./encode sample_text.txt text.encode 4 ./decode text.encode text.decode 4 ``` **主程式:** 這個小程式能將資料以 `uint8_t` 為一個單位編碼成 exp-golomb code ,在程式第 7 行,先設定 `MAX_ORDER` ,也就是程式能容許的 order-k 的限度,此程式最大的限度為 order-31 ,因為在轉換的過程是以 1 到 2 個 `uint32_t` 作為中繼的容器,如果 MAX_ORDER 大於等於 32 會造成編碼後的資料跨越 3 個 `uint32_t` ,我的沒有設計相應機制處理。 接下來設定 `BUFFER_SIZE` ,這個 macro 決定暫存陣列的長度。在 `encode` 函式中,暫存陣列為 `uint32_t buffer[BUFFER_SIZE]` ;在 `decode` 函式中,暫存陣列為 `uint8_t buffer[BUFFER_SIZE]`。 當 buffer 的元素達到 `WRITE_SIZE` 時便會將 buffer 的內容寫入 output file 並的清空 buffer 。 接下來我將 input file 以 file descriptor 的形式開啟,之後使用 `mmap` 讀檔、 output file 以 FILE* 的形式開啟,以 `fwrite` 寫入。 ```c= #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <libgen.h> //basename #include <fcntl.h> //open #include <unistd.h> //close #include <sys/mman.h> //mmap #include <sys/stat.h> #define MAX_ORDER 7 // must be smaller than 32 #define BUFFER_SIZE 100 #define WRITE_SIZE (BUFFER_SIZE - 2) /* function status */ #define FAIL 0 #define SUCCESS 1 int encode(int, FILE*, int); int decode(int, FILE*, int); int main(int argc, char *argv[]) { if (argc < 3) { puts("Usage:\n" "encode [input file] [output file] [order-k]\n" "decode [input file] [output file] [order-k]"); return 0; } /* open argv[1] as input file */ int in_fd = open(argv[1], O_RDONLY); if (in_fd < 0) { puts("cannot open input file"); return 0; } /* open argv[2] as output file */ FILE *out_file = fopen(argv[2], "wb"); if (!out_file) { puts("cannot open output file"); return 0; } /* use argv[3] as order */ long int order = 0; if (argc >= 4) { char* endptr; order = strtol(argv[3], &endptr, 10); if (endptr == argv[3]) { puts("order is not a decimal number"); return 0; } if (order > MAX_ORDER) { puts("order larger than max order"); return 0; } } int status; if (strcmp(basename(argv[0]), "encode") == 0) { status = encode(in_fd, out_file, order); } else { status = decode(in_fd, out_file, order); } fclose(out_file); close(in_fd); if (status == FAIL) { remove(argv[2]); } return 0; } ``` **Encode** 以 `uint8_t*` 型態打開 input file 的 `mmap` ,轉換後的 exp-golomb code 會緊密排列在 uint32_t 的陣列中,如下圖示: ![](https://i.imgur.com/NJm4IPD.png) 這裡所使用的編碼方法跟維基百科提到的一樣: 1. 以二進位寫下 $x+2^k$ 2. 數 $x+2^k$ 的二進位數字總共有多少位數,並在 $x+2^k$ 前面補上等同其位數減 $(k+1)$ 的數量的 `0` 當轉換完所有資料後,在檔案尾加上一個 (uint32_t) 1 作為結束檔案的記號。 ```c=73 int encode(int in_fd, FILE* out_file, int order) { struct stat s; if (fstat(in_fd, &s) < 0) { puts("cannot get status of iniput file"); return FAIL; } uint8_t *map = (uint8_t*) mmap(0, s.st_size, PROT_READ, MAP_PRIVATE, in_fd, 0); if (map == MAP_FAILED) { puts("cannot open mmap"); return FAIL; } uint32_t buffer[BUFFER_SIZE]; memset(buffer, 0, sizeof(buffer)); int buffer_index = 0; int cursor = 32; uint32_t offset = 1 << order; /* start to encode */ for (long int i = 0; i < s.st_size; i++) { uint32_t current = (uint32_t) map[i]; current += offset; int clz = __builtin_clz(current); int unary_width = 31 - clz - order; int binary_width = 32 - clz; /* write unary */ cursor -= unary_width; if (cursor <= 0) { cursor += 32; buffer_index++; } /* write binary */ if (cursor < binary_width) { // truncate in uint32_t buffer[buffer_index++] |= current >> (binary_width - cursor); buffer[buffer_index] |= current << (32 - (binary_width - cursor)); cursor = cursor + 32 - binary_width; } else { buffer[buffer_index] |= current << (cursor - binary_width); cursor -= binary_width; } /* write buffer */ if (buffer_index >= WRITE_SIZE) { fwrite(buffer, 4, WRITE_SIZE, out_file); uint32_t tail = buffer[buffer_index]; memset(buffer, 0, sizeof(buffer)); buffer[0] = tail; buffer_index = 0; } } /* finalize */ buffer[buffer_index + 1] = (uint32_t) 1; //end signal fwrite(buffer, 4, buffer_index + 2, out_file); return SUCCESS; } ``` **Decode** 這裡只要將 encode 的步驟反著做就好了,以 `uint32_t*` 型態打開 input file 的 `mmap` ,轉換後的資料會轉型成 `uint8_t` 。唯一需要注意的是,讓 `uint32_t` 左移或右移 32 bit 時,會發生 [-Wshift-count-overflow] ,所以在 191 行寫了一個判斷去規避平移 32 bit 。 ```c=134 int decode(int in_fd, FILE* out_file, int order) { struct stat s; if (fstat(in_fd, &s) < 0) { puts("cannot get status of iniput file"); return FAIL; } uint32_t *map = (uint32_t*) mmap(0, s.st_size, PROT_READ, MAP_PRIVATE, in_fd, 0); if (map == MAP_FAILED) { puts("cannot open mmap"); return FAIL; } uint8_t buffer[BUFFER_SIZE]; memset(buffer, 0, sizeof(buffer)); int buffer_index = 0; int cursor = 32; uint32_t offset = 1 << order; int map_index = 0; uint32_t current = map[0]; while (1) { /* read unary */ int unary_width = 0; if (current == 0) { // truncate in unary field current = map[++map_index]; int clz = __builtin_clz(current); unary_width = cursor + clz; cursor = 32 - clz; } else { int clz = __builtin_clz(current); unary_width = clz - (32 - cursor); cursor = 32 - clz; } /* end of file is (uint32_t) 1 , * the leading zero of end of file is 31 */ if (unary_width >= 31) { break; } /* read binary */ int binary_width = unary_width + 1 + order; uint32_t tmp = 0; if (binary_width > cursor) { // truncate in binary field tmp = current << (binary_width - cursor); current = map[++map_index]; binary_width -= cursor; cursor = 32; } tmp |= current >> (cursor - binary_width); tmp -= offset; buffer[buffer_index++] = (uint8_t) tmp; /* be careful for left shift 32 bits */ cursor -= binary_width; if (cursor == 0) { current = 0; } else { int shift = 32 - cursor; current = (current << shift) >> shift; } if (buffer_index == BUFFER_SIZE) { fwrite(buffer, 1, BUFFER_SIZE, out_file); buffer_index = 0; } } /* finalize */ if (buffer_index != 0) { fwrite(buffer, 1, buffer_index, out_file); } return SUCCESS; } ``` ### 使用 valgrind 檢查程式 因為整個程式完全沒使用 malloc ,所以很難發生 memory leak ,使用 `valgrind -q --leak-check=full` 編解碼都沒有被偵測到 memory leak。 下面為 encode 和 decode 的記憶體使用狀況: ![](https://i.imgur.com/QhkG4Pi.png) ![](https://i.imgur.com/znfd4SS.png) ### 使用 perf 觀察 **Encode** ``` # perf stat --repeat 5 -e cache-misses,cache-references,instructions,cycles,context-switches,branches,branch-misses ./encode 74-0.txt 74-0.encode Performance counter stats for './encode 74-0.txt 74-0.encode' (5 runs): 5,4589 cache-misses # 32.056 % of all cache refs ( +- 20.69% ) (45.64%) 17,0295 cache-references ( +- 4.66% ) (97.83%) 2515,6815 instructions # 1.46 insn per cycle ( +- 0.56% ) 1718,9863 cycles ( +- 0.80% ) 1 context-switches ( +- 40.82% ) 267,6514 branches ( +- 3.97% ) (54.36%) <not counted> branch-misses ( +-100.00% ) (2.17%) 0.006105 +- 0.000246 seconds time elapsed ( +- 4.02% ) ``` **Decode** ``` # perf stat --repeat 5 -e cache-misses,cache-references,instructions,cycles,context-switches,branches,branch-misses ./decode 74-0.encode 74-0.decode Performance counter stats for './decode 74-0.encode 74-0.decode' (5 runs): 7,6270 cache-misses # 32.452 % of all cache refs ( +- 20.52% ) (34.36%) 23,5021 cache-references ( +- 12.40% ) (77.71%) 2955,5866 instructions # 1.32 insn per cycle ( +- 2.98% ) (96.42%) 2245,0086 cycles ( +- 5.05% ) 1 context-switches ( +- 25.00% ) 298,9814 branches ( +- 2.96% ) (69.22%) <not counted> branch-misses ( +- 28.11% ) (22.29%) 0.00845 +- 0.00136 seconds time elapsed ( +- 16.08% ) ``` 為了讓程式執行的夠久,讓 perf 能有效偵測效能,我使用 repo 中的 74-0.txt 進行測試,但是仍然無法測出 branch-mises 不知道是系統權限沒設定好或是要讓程式執行更久才能統計出來。 我對 order-0 的 encode 和 decode 測試 5 次,得到上面兩份數據,很明顯發現我的 cache-misses 非常高,平均高達 32% ,代表我程式寫法對變數的 locality 處理非常差,我認為這是目前值得我去改進的部份。 我初步猜測可能原因是類似 `dict` 作業 copy 和 reference 機制的問題,也就是我常常存取的陣列沒有配置在較近的 heap 中。 ### Exponential Golomb coding 效能分析 下圖是 8-bit 數值經過 order 0 到 4 的編碼後所佔據的位元長度,可以看到除了 order-4 之外,其他 exp-golomb code 都在原數值大於 50 之前達到 8 bit 的長度。 ASCII code 中常用的數字和英文字母介於 48 的 `'0'` 到 122 的 `'z'` 之間,幾乎都是會讓 exp-golomb code 超過 8 bit 的區間,若沒有修改 exp-golomb 的機制,根本無法壓縮。 x-compressor 以不斷更新模型,盡量讓出現頻率最高的數字使用位元長度較少的 golomb-rice code 達成壓縮的效果 ![](https://i.imgur.com/SjRCT5l.png) ## 閱讀論文 [Selecting the Golomb Parameter in Rice Coding](https://ipnpr.jpl.nasa.gov/progress_report/42-159/159E.pdf) 這篇論文的目標是針對一個資源,找出 golomb-power-of-2 (GPO2) 最佳的參數 $k$ ,使得此資源的 GPO2 的 bit rate 期望值 (編碼的平均長度) 最小。找出參數 $k$ 的過程必須快、節省計算資源。 ### Analysis **A. Minimizing Code Rate** 首先,我們把**非負整數**設定為目標資源,用 GPO2 編碼非負整數的隨機變數 $δ$ 時,前綴的一進位值需要 $\left\lfloor\dfrac{j}{2^k}\right\rfloor + 1$ 個 bit ,後綴二進值則固定為 $k$ 個 bit ,此時的編碼率 (對所有非負整數編碼的長度) 為: $$ R_k = \sum_{j=0}^\infty(\left\lfloor\dfrac{j}{2^k}\right\rfloor+1+k) \quad \text{Prob}[δ=j]$$ 觀察上面的函數,在固定 $j$ 的情況下 $\left\lfloor\dfrac{j}{2^k}\right\rfloor+1+k$ 為一凸函數,又 $R_k$ 為這些凸函數的總和,所以 $R_k$ 也是凸函數,因此 $R_k$ 的區域最小值亦為全域最小值。任何非負整數 $k^*$ 滿足 $$R_{k^*-1} \leq R_{k^*} \leq R_{k^*+1} \quad \quad (1)$$ 即為 GPO2 編碼長度最小的解。為了方便計算,將 $R_{-1}$ 視為 $\infty$ 。 接下來,我們考慮已知 $µ=E[δ]$ 的情況下,如何依據先前的編碼估算參數 $k$ 的值。給定 $µ$ , GPO2 的最佳參數 $k$ 一定會在區間 $[k^*_{min}(µ), \ k^*_{max}(µ)]$ ,其中 $$\begin{align*} &k^*_{min}(µ) = \text{max}\left\lbrace 0,\left\lfloor \text{log}_2\left(\dfrac{2}{3}(µ+1)\right) \right\rfloor \right\rbrace \\ &k^*_{max}(µ) = \text{max}\lbrace 0, \lceil \text{log}_2 µ \rceil\rbrace \end{align*} \quad \quad (2)$$ 使得 $R_{k^*}$ 接近最低點 (編碼率最佳) ,下圖顯示了 $k^*_{min}(µ)$ 和 $k^*_{max}(µ)$ 的成長趨勢 ![](https://i.imgur.com/knA8lsX.png) 再來由 (2) 可以推得 $k^*_{max}(µ)-k^*_{min}(µ) \leq 2 \ \text{for all} \ µ$ ,其中的 $µ$ 是樣本的平均值。以 $µ$ 計算編碼率 $R_k$ 和 $R_{k-1}$ 的差值 $$\begin{align*} R_k - R_{k-1} &= 1 + \sum_j(\left\lfloor \dfrac{j}{2^k} \right\rfloor - \left\lfloor \dfrac{j}{2^{k-1}} \right\rfloor) \\ &= 1 + \sum_j \left\lfloor \dfrac{1}{2} - \dfrac{j+1}{2^k} \right\rfloor \quad \text{Prob}[δ=j] \end{align*}$$ 其中 $$-\dfrac{1}{2}-\dfrac{j}{2^k} ≤ \left\lfloor \dfrac{1}{2} - \dfrac{j+1}{2^k} \right\rfloor ≤ \dfrac{1}{2}-\dfrac{j+1}{2^k}$$ 所以我們得到 $$\dfrac{1}{2} - \dfrac{µ}{2^k} ≤ R_k - R_{k-1} ≤ \dfrac{3}{2} - \dfrac{µ+1}{2^k} \quad \quad (3)$$ 我們可以將編碼率表示為 $$\begin{align*} R_k &= k+1+E\left[ \left\lfloor\dfrac{δ}{2^k}\right\rfloor \right]\\ &= k+1+\dfrac{1}{2^k}\left(E[δ]-E\left[ δ-2^k \left\lfloor\dfrac{δ}{2^k} \right\rfloor \right]\right)\\ &=k+1+\dfrac{1}{2^k}(µ-\bar{r}_k) \quad \quad (4) \end{align*}$$ 其中 $\bar{r}_k$ 的值等於 $$\bar{r}_k=δ-2^k\left\lfloor \dfrac{δ}{2^k} \right\rfloor \quad \quad (5)$$ 其值剛好等於 $δ \ \text{mod} \ 2^k$ 。由 (4) 可以進一步推出 $R_k^{\text{up}}$ 和 $R_k^{\text{low}}$ 代表 $R_k$ 的上下界 $$\begin{align*} &R_k ≤ R_k^{\text{up}}(µ) \triangleq k+1+\dfrac{µ}{2^k}\\ &R_k ≥ R_k^{\text{low}}(µ) \triangleq k+\text{max} \left\lbrace 1, \dfrac{µ+1}{2^k}\right\rbrace \end{align*}$$ 我們由 (4) 推導出的算式進一步推論在滿足 (1) 的 $R_k ≤ R_{k-1}$ 時, $µ$ 的範圍為 (6) $$\begin{align*} &k+1+\dfrac{1}{2^k}(µ-\bar{r}_k) ≤ (k-1)+1+\dfrac{1}{2^{k-1}}(µ-\bar{r}_{k-1})\\ \Rightarrow \quad &µ≥2^k+2\bar{r}_{k-1}-\bar{r}_{k} \quad \quad (6) \end{align*}$$ 最後,針對 (6) 我們討論邊界問題: 因為 $\bar{r}_0 = 0$ ,當 $µ < 2^1+2\bar{r}_0-\bar{r}_1$ (即無法滿足 (6)) 時,就將參數 $k$ 設為 0 ;否則,選擇滿足 (6) 的最大的 $k$ 值。如此一來,便可以得到最佳的 $k$ 值,我們可以預先計算每個 $µ$ 在 (6) 中應該要對應哪個 $k$ 值,並做一個表格記錄下來。 **B. Dynamic Range Limit and Uncoded Data** 有兩個因素影響了 GPO2 在現實的應用: 1. 儘管 Golomb code 允許任意大小的輸入,但在實際應用中,樣本通常有最大值,縮小了需要考慮的編碼範圍。 2. 許多 entropy 編碼器可以選擇不要編碼樣本,參數選擇 ( $k$ ) 的過程中應該要包含不予編碼的狀況。 再來,當樣本有 $N$ bit 時, $k ≥ N − 1$ 會使得編碼率大於等於原始樣本,這樣就沒有壓縮的效果,所以我們預先施加約束: $k ≤ N − 2$ ### Coding a Geometric Source **A. Optimum Code Selection** 將 $δ$ 以幾何分布建模如下,評估 $\bar{r}_k$ $$\text{Prob}[δ=j]=(1-α)α^j \quad \quad α ∈ (0,1)$$ ## 改進 x-compressor 的方案 ### 解析 x-compressor 原始碼 **compress** 首先 input file 藉由 `stdin` 進入程序,在 `x.c` 的 main function 中,宣告 `FILE* istream` 指向 `stdin` 的資料流;`FILE* ostream` 指向 `stdout` 的資料流。 接下來,將資料分成兩個 `layer` ,將 `stdin` 的資料載入 heap 中,並用 `layer[0].data` 指向它,之後 `layer[1].data` 會被分配到一塊夠大的空間儲存轉換後的資料 。透過 `x_init()` 初始化 `table` ,也就是字元分佈的模型,首先按照每個字元的大小順序排好。 然後進入到 `x_compress` 函式,使用 `initiate` 初始化 `io` , `io` 負責將轉換好的編碼緊密的排在 `layer[1].data` 中, `io.ptr` 指向 `layer[1].data` 中即將被寫入的位址,功能類似我前面的程式的 `buffer_index` 、 `io.c` 指向即將被寫入的 bit ,功能類似我前面的程式的 `cursor` **todo:**