Exponential Golomb coding

Posted by blueskyson on January 8, 2021

Exponential Golomb coding

摘錄自維基百科,Exponential-Golomb coding (或稱 Exp-Golomb code) 是一種 Universal code,也就是 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 + 1 前面補上等同其位數減 $k$ 的數量的 0
x order-0 order-1 order-2 order-3 order-4
0 1 10 100 1000 10000
1 010 11 101 1001 10001
2 011 0100 110 1010 10010
3 00100 0101 111 1011 10011
4 00101 0110 01000 1100 10100
5 00110 0111 01001 1101 10101
6 00111 001000 01010 1110 10110
7 0001000 001001 01011 1111 10111
8 0001001 001010 01100 010000 11000
9 0001010 001011 01101 010001 11001
10 0001011 001100 01110 010010 11010
11 0001100 001101 01111 010011 11011
12 0001101 001110 0010000 010100 11100

C 語言實作

github 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

encodedecode 的參數格式如下,以下將 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 寫入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#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 的陣列中,如下圖示:

這裡所使用的編碼方法跟維基百科提到的一樣:

  1. 以二進位寫下 $x+2^k$
  2. 數 $x+2^k$ 的二進位數字總共有多少位數,並在 $x+2^k$ 前面補上等同其位數減 $(k+1)$ 的數量的 0

當轉換完所有資料後,在檔案尾加上一個 (uint32_t) 1 作為結束檔案的記號。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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] ,所以在以下程式碼的第 58 行寫了一個判斷去規避平移 32 bit 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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;
}

使用 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 能有效偵測效能,我使用 74-0.txt (湯姆歷險記) 進行測試,但是仍然無法測出 branch-misses 不知道是系統權限沒設定好或是要讓程式執行更久才能統計出來。

我對 order-0 的 encode 和 decode 測試 5 次,得到上面兩份數據,很明顯發現我的 cache-misses 非常高,平均高達 32% ,代表我程式寫法對變數的 locality 處理非常差,我認為這是目前值得我去改進的部份。我初步猜測可能原因是類似 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 達成壓縮的效果