# 2023q1 Homework1 (lab0) contributed by < `SPFishcool` > ## 開發環境 ```shell $ neofetch --stdout OS: Ubuntu 22.04.1 LTS x86_64 Host: GL552VW 1.0 Kernel: 5.15.0-60-generic Uptime: 13 hours, 20 mins Packages: 1877 (dpkg), 9 (snap) Shell: bash 5.1.16 Resolution: 1920x1080 DE: GNOME 42.5 WM: Mutter WM Theme: Adwaita Theme: Yaru [GTK2/3] Icons: Yaru [GTK2/3] Terminal: gnome-terminal CPU: Intel i5-6300HQ (4) @ 3.200GHz GPU: Intel HD Graphics 530 GPU: NVIDIA GeForce GTX 960M Memory: 3626MiB / 11868MiB $ gcc -v Thread model: posix Supported LTO compression algorithms: zlib zstd gcc version 11.3.0 (Ubuntu 11.3.0-1ubuntu1~22.04) ``` ## lab0 資料架構認識 ### list_head - list_head 負責作為鏈結串列將其 entry 串連在一起,其為 Circular Doubly-linked list 類型 :::warning 注意書寫: doubly-linked list,連字號的位置在 doubly 和 linked 之間。 :notes: jserv ::: ```graphviz digraph list { rankdir=LR; node[shape=record, style=bold]; subgraph cluster_0 { head [label="{<prev>prev|<next>next}"]; color=invis; label=list_head } } ![](https://i.imgur.com/8XRDf8u.png) ``` - list_head架構圖 :::warning 使用 Graphviz 製圖並嵌入到 HackMD 頁面。 :notes: jserv 收到!正在改進中! ::: - list_head的struct為: ```c struct list_head { struct list_head *prev; struct list_head *next; }; ``` ### element_t - `element_t` 為佇列儲存 value,內部有 `list_head` 架構的 list 能夠將所有 `element_t` 串接起來 ```graphviz digraph list { rankdir=LR; node[shape=record, style=bold]; subgraph cluster_0 { value [label="{*value}"]; list [label="list", shape=plaintext] head [label="{<prev>prev|<next>next}"]; label=element_t; } } ``` - element_t 架構圖 - element_t 的 struct 為: ```c typedef struct { char *value; struct list_head list; } element_t; ``` - 因此 lab0 實做的佇列結構會是 ![](https://i.imgur.com/2NomHmV.png) ### queue_chain_t - 在執行 [qtest.c](https://github.com/SPFishcool/lab0-c/blob/master/qtest.c) 時,會先 create 一個 `queue_chain_t` 結構的物件,其功用類似上述佇列結構的 **head**,目的是每當 `q_new` 一個新的佇列時會將新的佇列利用 list_add_tail 加入到 `queue_chain_t` 鏈結串列的最後面。 >![](https://i.imgur.com/96FnpoC.png) >queue_chain_t 架構圖,size 為佇列的數量 ```c typedef struct { struct list_head head; int size; } queue_chain_t; ``` ### queue_contex_t - `queue_contex_t` 在紀錄每一條佇列的資訊,`*q` 為 pointer of queue head,`size` 是佇列裡面 element_t 的數量,`chain` 為與其他 `queue_contex_t` 串連的 `list_head`。 >![](https://i.imgur.com/80aWxMq.png) >queue_contex_t架構圖 ```c typedef struct { struct list_head *q; struct list_head chain; int size; int id; } queue_contex_t; ``` - 再加入 `queue_chain_t` 其完整佇列架構就完成了! >![](https://i.imgur.com/NuEJyGr.png) >queue 為上圖的紅色虛線部份 ## lab0 開發過程 ### q_new `q_new()` 會先配置 `list_head` 大小的記憶體位置,接下來判斷是否配置成功,最後 `INIT_LIST_HEAD(new)` 會將這個 node 接回自己(成為 Doubly-linked list 型態)。 ![](https://i.imgur.com/Qvdzk6d.png) ```c struct list_head *q_new() { struct list_head *new = malloc(sizeof(struct list_head)); // check if space allowed if (!new) return NULL; INIT_LIST_HEAD(new); return new; } ``` ### q_free `q_free()` 從 `head->next` (第一個有entry的節點)開始釋放,直到回到 head 最後釋放 head 節點。 ```c void q_free(struct list_head *l) { if (!l) return; struct list_head *indirect = l->next; struct list_head *tmp = NULL; while (indirect != l) { tmp = indirect; indirect = indirect->next; q_release_element(list_entry(tmp, element_t, list)); } free(l); } ``` ### q_insert_head / q_insert_tail 兩者可以使用 [list.h](https://github.com/SPFishcool/lab0-c/blob/master/list.h) 中的 `list_add` 與 `list_add_tail` 將 `element_t` 內的list加入鏈結串列中。 ```c bool q_insert_head(struct list_head *head, char *s) { if (!(head && s)) return false; element_t *new_ele = malloc(sizeof(element_t)); if (!new_ele) return false; new_ele->value = (char *) malloc(strlen(s) * sizeof(char) + 1); if (!new_ele->value) { free(new_ele); return false; } strncpy(new_ele->value, s, strlen(s) + 1); // struct list_head *new_node = (new_ele -> list); list_add(&new_ele->list, head); return true; } ``` ### q_remove_head/q_remove_tail remove 只有將 element_t 的串列節點從佇列中移除並沒有釋放其空間,因此只需要將其 next 與 prev 連接起來即可移除。 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; struct list_head *indirect = head->next; //or head->prev (q_remove_tail) list_del(indirect); element_t *ele = list_entry(indirect, element_t, list); if (sp) { strncpy(sp, ele->value, bufsize-1); *(sp+bufsize-1) = '\0'; } return ele; } ``` ### q_size `q_size` 則從 `head->next` 開始計算這條佇列的 `element_t` 數量,並且回傳 `i` (count 後的結果) ```c int q_size(struct list_head *head) { if (!head) return -1; struct list_head *indirect = head->next; int i = 0; while (indirect != head) { i++; indirect = indirect->next; } return i; } ``` ### q_delete_mid 因為佇列是 doubly-linked list 結構,因此可以使用左右指標(最左和最右可以快速找到),其原理是每一迴圈 `left_node` 往右走一步,`right_node` 往左走一步,直到兩指標相遇(奇數 element)或相鄰(偶數 element)時 right_node 就是我們要找的節點。 優點:跟快慢指標相比,迴圈節省了一半的次數(僅限 Circular Doubly-linked list)。 ![](https://i.imgur.com/m7wqbwr.png) ```c bool q_delete_mid(struct list_head *head) { while(!head || list_empty(head)) return false; struct list_head *left_node = head->next; struct list_head *right_node = head->prev; while ((left_node->next != right_node) && (left_node != right_node)) { left_node = left_node->next; right_node = right_node->prev; } list_del(right_node); q_release_element(list_entry(right_node, element_t, list)); return true; } ``` ### q_delete_dup `q_delete_dup` 這個 question 我紀錄此次和前次的比較紀錄,而受到「判決」的節點為 `indirect->prev`(目前指到的節點的前一個節點) - `cmp`:`indirect` 與 `indirect->prev` 的比較結果 - `prev_cmp`:`indirect->prev` 與 `indirect->prev->prev` 的比較結果 ![](https://i.imgur.com/5lbM3Lc.png) 不管是 `cmp` 還是 `prev_cmp` 只要為 0 都代表著**中間這個節點的 value 與前後節點重複**,必刪除! ```c bool q_delete_dup(struct list_head *head) { if (!head || list_empty(head)) return false; struct list_head *indirect = (head->next)->next; element_t *prev_ele = NULL; element_t *current_ele = NULL; int cmp = -1, prev_cmp = -1; while (indirect != head) { prev_ele = list_entry(indirect->prev, element_t, list); current_ele = list_entry(indirect, element_t, list); cmp = strcmp(prev_ele->value, current_ele->value); if (cmp == 0 || prev_cmp == 0) { list_del(indirect->prev); q_release_element(prev_ele); } prev_cmp = cmp; indirect = indirect->next; } if (cmp == 0) { list_del(indirect->prev); q_release_element(current_ele); } return true; } ``` ### q_swap `q_swap` 只要將佇列內的 element 兩兩交換即可,因此只需要使用 [list.h](https://github.com/SPFishcool/lab0-c/blob/master/list.h) 中的 `list_move` 把自己搬到 `next` 後面即可 ```c void q_swap(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *indirect = head->next; while (indirect != head && indirect->next != head) { list_move(indirect, indirect->next); indirect = indirect->next; } } ``` ### q_reverse 這裡則是使用 pointer to pointer 紀錄第一個 element 節點內的 next 位址(`&head->next->next`),在每次迴圈中將 `next` 指到的節點利用 `list_move` 移到 `head` 前面直到 `next` 指向的位置為 `head` 為止。 ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head **indirect = &((head->next)->next); while (*indirect != head) list_move(*indirect, head); } ``` ### q_reverseK `q_reverseK` 其原理與 `q_reverse` 相似,但因為如果後面個數不足 k 個則不做 reverse,因此我的作法是先讓一個指標邊走邊數,數到第 k 個後,再將後面的節點一一搬到前面來,因此如果走完一圈沒有數到 k 個,剩下的節點就不會動到。 ```c void q_reverseK(struct list_head *head, int k) { if (!head || list_empty(head)) return; struct list_head **indirect = &(head->next); struct list_head *current = head->next; struct list_head *tmp = NULL; int i = 1; while (current != head) { if (i % k == 0) { tmp = current; while (*indirect != tmp) { list_move(tmp->prev, current); current = current->next; } indirect = &((current)->next); } current = current->next; i++; } } ``` ### q_sort 參考[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#Merge-Sort-%E7%9A%84%E5%AF%A6%E4%BD%9C)merge_sort寫法。 原先想法是要維持 doubly-linked list 的結構去設計,但當我要 `malloc` 新的 head 來接分割後的子串列時遇到`ERROR:Calls to malloc disallowed`,因此決定將 circular 斷開將最後一個 node 的`prev`指向`NULL`才進行`mergesort`並且在排序後將排序過後的串列重新接上 head。 ```c void q_sort(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *start = head->next; struct list_head *tail = head->prev; tail->next = NULL; struct list_head *sorted_list = merge_sort(start); head->next = sorted_list; struct list_head *cur = head; while (cur->next) { cur->next->prev = cur; cur = cur->next; } cur->next = head; head->prev = cur; } ``` ```c struct list_head *mergeList(struct list_head *l1, struct list_head *l2) { struct list_head *head = NULL; struct list_head **current = &head; while (l1 && l2) { if (strcmp(list_entry(l1, element_t, list)->value, list_entry(l2, element_t, list)->value) >= 0) { *current = l2; l2 = l2->next; } else { *current = l1; l1 = l1->next; } current = &((*current)->next); } *current = (struct list_head *) ((__intptr_t) l1 | (__intptr_t) l2); return head; } ``` ```c struct list_head *merge_sort(struct list_head *head) { if (!head || head->next == NULL) return head; struct list_head *slow = head; for (struct list_head *fast = head->next; fast && fast->next; fast = fast->next->next) { slow = slow->next; } struct list_head *list2 = slow->next; slow->next = NULL; return mergeList(merge_sort(head), merge_sort(list2)); } ``` ### q_descend `q_descend` 若是 `prev` 方向有比自己小的節點則這些小的節點都要 delete 掉,直到遇到下一個比自己大的節點,則移動到下一個節點(`prev`) ```c int q_descend(struct list_head *head) { if (!head || list_empty(head)) return 0; struct list_head *current = head->prev; struct list_head *cur_prev = current->prev; while (cur_prev != head) { if (strcmp(list_entry(current, element_t, list)->value, list_entry(cur_prev, element_t, list)->value) > 0) { list_del(cur_prev); q_release_element(list_entry(cur_prev, element_t, list)); } else { current = current->prev; } cur_prev = current->prev; } int len = q_size(head); return len; } ``` ### q_merge 在 [qtest.c](https://github.com/SPFishcool/lab0-c/blob/master/qtest.c) 裡執行`q_merge`其程式碼為 ```c len = q_merge(&chain.head); ``` 這個 chain 其實就是 `queue_chain_t` 結構,所以得知輸入的資料為 `queue_chain_t` 內的 `head` 元素因此我們可以很簡單的拜訪所有佇列再使用[list.h](https://github.com/SPFishcool/lab0-c/blob/master/list.h)中的 `list_splice_init` 從第二個佇列到最後一個一一串在 ~~`id=0`~~ 目前第一個 `queue_contex_t->q` 的串列內。 ```c int q_merge(struct list_head *head) { if(!head || list_empty(head)) return 0; else if(list_is_singular(head)) return list_entry(head->next, queue_contex_t, chain)->size; queue_contex_t *target = list_entry(head->next, queue_contex_t, chain); queue_contex_t *que = NULL; list_for_each_entry (que, head, chain) { if (que == target) continue; list_splice_init(que->q, target->q); target->size = target->size + que->size; que->size = 0; } q_sort(target->q); return target->size; } ``` ## 目前 queue.c 靜態與動態評分 ### make test - score: 95/100 - problem: q_insert 系列程式時間複雜度非 constant time ### make valgrind - score: 94/100 - problem: Test of malloc failure on new 測試未通過 - reason:在 `qtest` 執行結束未妥善釋放其程式所配置的記憶體所導致 - solved: 在研讀[Valgrind + 自動測試程式](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-b#%E4%BB%A5-Valgrind-%E5%88%86%E6%9E%90%E8%A8%98%E6%86%B6%E9%AB%94%E5%95%8F%E9%A1%8C)後發現大部分的test其實都在記憶體配置上出現問題,例如: ```bash ==104151== 128,000 bytes in 2,000 blocks are still reachable in loss record 4 of 6 ==104151== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==104151== by 0x10F110: test_malloc (harness.c:133) ==104151== by 0x10F645: q_insert_tail (queue.c:72) ==104151== by 0x10C62B: do_it (qtest.c:312) ==104151== by 0x10DDFA: interpret_cmda (console.c:181) ==104151== by 0x10E3AF: interpret_cmd (console.c:201) ==104151== by 0x10E7B0: cmd_select (console.c:610) ==104151== by 0x10F09C: run_console (console.c:705) ==104151== by 0x10D1EC: main (qtest.c:1223) ``` 而上述 problem 的錯誤為: ```bash ==104070== 160 bytes in 5 blocks are possibly lost in loss record 3 of 3 ==104070== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==104070== by 0x10CBCE: do_new (qtest.c:145) ==104070== by 0x10DDFA: interpret_cmda (console.c:181) ==104070== by 0x10E3AF: interpret_cmd (console.c:201) ==104070== by 0x10E7B0: cmd_select (console.c:610) ==104070== by 0x10F09C: run_console (console.c:705) ==104070== by 0x10D1EC: main (qtest.c:1223) ``` 在這情況下我決定先嘗試解決比較輕的問題,解決過程如下: - 檢查 queue.c 是否有哪一個功能沒有完全 free 空間 - 檢查 script 資料夾內的 cmd 檔案,發現沒有此問題的腳本都有執行 free。 - 開始檢查 [qtest.c](https://github.com/SPFishcool/lab0-c/blob/master/qtest.c) 的退出程式發現: ```c static bool q_quit(int argc, char *argv[]) { return true; report(3, "Freeing queue"); if (current && current->size > BIG_LIST_SIZE) set_cautious_mode(false); if (exception_setup(true)) { struct list_head *cur = chain.head.next; while (chain.size > 0) { queue_contex_t *qctx, *tmp; tmp = qctx = list_entry(cur, queue_contex_t, chain); cur = cur->next; q_free(qctx->q); free(tmp); chain.size--; } } ``` 發現 `do_quit` 第一行程式碼即 `return true` 表示下方程式完全沒有走過,註解掉後重新 `make valgrind` 所有問題都解決了。 ## 研讀 Linux 核心原始程式碼的 lib/list_sort.c 在 linux 核心原始程式碼內的 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 所作的 sort 為 **Merge Sort** ,但這裡面的 Merge Sort 跟我們以前認識的 Merge Sort 不同。 在 `list_sort` 內有 3 個 Function - `merge` - `merge_final` - `list_sort` 而走訪 linked list 時會使用 count 會計算目前走訪到的節點數量,一旦走訪數量走到 $2^k$ 數量時不執行 `merge`,反之就會執行 merge。其流程如下 1. 一開始 `list` 從 `head->next` 開始,而 `pending` 起始為 `NULL`,`count` 起始為 `0`,linked list 最後指向 `NULL`。 2. 迴圈開始,宣告 `**tail`指向 `pending` 的位址。 3. 會先計算 `count` 是否為 $2^k-1$ ,如果是,跳到 "7."。 4. 準備 `merge` ,將 `a = *tail`、`b = a->prev`,執行 `merge(priv, cmp, b, a);` 5. `merge` 會有一個 `**tail` 紀錄前一次比較最小的節點的 `next` 位址,每一次比較會把 `*tail` 指向較小的節點,**其實就是一般的merge,這裡就把 `next` 拿來做 merge 的主要 link。** 6. merge return 為 merge 後的第一個節點(最小)存入 `a` ,將目前的 `a->prev` 重指成 `b->prev`,藉由 `*tail` 把 `pending` 指向 `a`(merge 起始節點)。 7. 將 `list->prev` 指向 `pending`,`list` 往前一步, `panding = list->prev`, 把 `pending->next` 指向 `NULL`。 8. 若 `list` 還沒走完,回到 "2." 9. merge 剩下還沒 merge 起來的 pending list。 10. 最後 `merge_final` 把 merge 完的 list 將其 `prev` 連接起來。 看完 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 發現他並沒有特別做 divide 而是走訪過程一一將 `next` 斷開並且把 `next` 當成執行 merge 動作時走訪的 link,比較「[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#Merge-Sort-%E7%9A%84%E5%AF%A6%E4%BD%9C)」所提及的遞迴與非遞迴,此做法省去 divide 這一部份,也沒有使用 stack 有 Max Depth 的限制。 ## 引入 lib/list_sort.c 到 lab0-c 專案 - 在 [qtest.c](https://github.com/SPFishcool/lab0-c/blob/master/queue.c) 新增 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 三條 finction 和引入 [list_sort.h](https://github.com/torvalds/linux/blob/master/include/linux/list_sort.h) 內宣告設定 - 參考[lab 0:Valgrind + 自動測試程式](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-b)中的 qtest 直譯器實作,使用 `ADD_COMMAND` 新增剛才加入的 `list_sort`, 再依照檔案裡的 `do_sort` 新增 `do_list_sort` ```c ADD_COMMAND(list_sort, "Sort queue in ascending order by list_sort", ""); ``` - 製作分析準備 ### 利用 perf 分析 `list_sort` 與自己做的 `q_sort` 效能比較 參考 [linhoward0522 開發紀錄 (lab0)](https://hackmd.io/@linhoward/lab0-2023#2023q1-Homework1-lab0) 使用 perf 分析其效能,在 [traces/trace-15-perf.cmd](https://github.com/SPFishcool/lab0-c/blob/master/traces/trace-15-perf.cmd) 有三筆資料來測試排序,因此我們可以依照這個檔案自製用來測試的 cmd 檔。 ``` . ├── RAND-1000000-list.cmd ├── RAND-1000000-q.cmd ├── RAND-100000-list.cmd ├── RAND-100000-q.cmd ├── RAND-10000-list.cmd ├── RAND-10000-q.cmd ├── RAND-1000-list.cmd └── RAND-1000-q.cmd ``` `q` : `q_sort` `list` : `list_sort` 接下來是試做 `shell scripts` ```bash #!/bin/bash # q_sort for i in ./traces/perf_traces/RAND-*-q.cmd do perf stat --repeat 5 -o "${i%.cmd}"-report -e cache-misses,branches,instructions,context-switches ./qtest -v 0 -f "$i" done #list_sort for i in ./traces/perf_traces/RAND-*-list.cmd do perf stat --repeat 5 -o "${i%.cmd}"-report -e cache-misses,branches,instructions,context-switches ./qtest -v 0 -f "$i" done ``` - `q_sort` | #node | cache-misses | branches | instructions | context-switch | | ------- | ------------ | ----------- | ------------ | -------------- | | 1000 | 3469 | 77,9884 | 516,0401 | 0 | | 10000 | 3,3403 | 621,9374 | 4305,8950 | 0 | | 100000 | 543,0532 | 6326,2119 | 4,3295,3471 | 0 | | 1000000 | 7491,5547 | 4,6974,1917 | 31,6153,3055 | 10 | - `list_sort` | #node | cache-misses | branches | instructions | context-switch | |-------|--------------|----------|--------------|-----------------| |1000|2489|77,7798|519,9240|0| |10000|4,1257|614,2028|4329,3845|0| |100000|439,1922|6258,0696|4,3642,0292|3| |1000000|5824,6891|4,6028,3507|31,6579,3448|9| 可以看到節點數增多,效能差異越大,雖然 RAND 會導致偶爾會讓 `list_sort` 效能看起來跟 `q_sort` 差不多甚至更差,但時間上還是 `list_sort` 略勝一籌。 ## 在 [qtest.c](https://github.com/SPFishcool/lab0-c/blob/master/qtest.c) 新增 `shuffle` 命令 作業要求為使用「Fisher–Yates shuffle」完成 `shuffle`,其原理就是從最後一個節點開始,隨機選取前面的節點與其交換,交換完後再往前一格完成前面敘述的事情直到第二張牌為止。 ```c for (int i = len-1;i > 0;i--) { //random 1~i int count = rand() % i; struct list_head *node = head->next; for (int j=1; j < count;j++) node = node->next; swap(tail, node); tail = node->prev; } ``` 跟設定 `list_sort` 一樣,新增 `do_shuffle` 與 `ADD_COMMAND` ```c ADD_COMMAND(shuffle, "shuffle queue.", ""); ``` 接下來統計剛設計出來的 `shuffle` 亂度 參考 [L01: lab0 亂數+論文閱讀](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-d) 中的測試程式並載入 [Makefile](https://github.com/SPFishcool/lab0-c/blob/master/Makefile) 執行指令 ```makefile shuffle_check: qtest scripts/shuffle_test.py scripts/shuffle_test.py ``` 執行結果: ![](https://i.imgur.com/6nFK4rS.png) 依照 4 個數字的亂數來看,分佈差距大約在 $\pm1.2\%$ ,學生突然想到如果數字在多一點的話分佈會不會變化比較大,因此我將數字加到 6 個,而 6 個數字的排列組合比 4 個數字多出 30 倍,因此也將次數增加到 30000000 次。 執行結果: ![](https://i.imgur.com/MgJ8VzX.png) 在這張圖最少次為 41081,最多次為 42281,幅度有稍微增加,但沒有特別明顯。 ## 使用 entropy 觀察亂數 在認識 entropy 之前,對 entropy 的唯一認知就是機器學習中計算 loss 值的方式 `cross entropy`,發現其中的含意與 entropy 也有一些關係,但知道了「entropy 與亂數產生的資訊量成反比」還是覺得對 entropy 有點抽象,因此我打算從 qtest.c 裡的指令 `option entropy 1` 與 `ih RAND 10` 找出關於 entropy 的蛛絲馬跡,最後發現 [shannon_entropy.c](https://github.com/SPFishcool/lab0-c/blob/master/shannon_entropy.c) 裡的 `shannon_entropy` 實作了以下 entropy 公式。 $-\sum^{n}_{i=1} p(x_i)\log_2{p(x_i)}$ ```c for (uint32_t i = 0; i < BUCKET_SIZE; i++) { if (bucket[i]) { uint64_t p = bucket[i]; p *= LOG2_ARG_SHIFT / count; entropy_sum += -p * log2_lshift16(p); } } entropy_sum /= LOG2_ARG_SHIFT; return entropy_sum * 100.0 / entropy_max; ``` 這裡除了使用 bucket 計算每一個字元的熵值,而且使用 `LOG_ARG_SHIFT` 預計算的常數來避免使用浮點數增加計算時間。