contributed by < jj97181818
>
建立新的「空」佇列
在想這裡應該要宣吿指向 struct list_head
或是 element_t
的 pointer,來回改了幾次,在後面實作到 q_insert_head 的 function 時,看了 list.h 裡面的 list_add function 的實作,要新增在最前面的新節點是接在 head 後面的,所以其實 head 本身不需要是 element_t
,因為他的目的是要指向第一個節點,不需要存任何 value。
釋放佇列所佔用的記憶體
先用 list_for_each_safe
iterate 所有節點,因為該 function 允許刪除節點,然後用 list_entry
透過成員位址 node(struct list_head)存取到該節點(element_t)的開頭位址,並用 queue.c 中的 q_release_element
來 free 節點的 value 和節點本身。在所有節點的記憶體都釋放之後,再來 free head。
在佇列開頭 (head) 加入 (insert) 給定的新元素 (以 LIFO 準則)
先宣告一個指向 element_t 的指標 node,並配置記憶體給它。然後再配置記憶體給 node 的成員 value,大小為 s 本身的長度再加上 1('\0'),如果沒有成功就將已配置給該節點的記憶體釋放。複製 s 的值到 node 的成員 value 中,再呼叫 list_add
將新節點插入 list 的最前面。
自己原本 list_add(&(node->list), head);
這樣寫 commit 的時候會被 pre-commit hook 拒絕,但是改 list_add(&node->list, head);
就會順利過了,但兩個是會做一樣的事情。
TODO: 寫一個更小且可重現 (reproducible) 的測試案例,提交到 Cppcheck 專案的錯誤追蹤。
jservImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
在佇列尾端 (tail) 加入 (insert) 給定的新元素 (以 FIFO 準則)
和 q_insert_head
一樣,除了 list_add
改成呼叫 list_add_tail
。
在佇列開頭 (head) 移去 (remove) 給定的節點
檢查 head 是否為 NULL 或是 empty。list_entry
能取得第一個節點的起始位址,list_del
刪除第一個節點的連結。然後將要刪除節點的 value 複製給 sp,再將 sp 的最後一個字元改為 \0。
在佇列結尾 (tail) 移去 (remove) 給定的節點
和 q_remove_head
一樣,除了要刪掉的節點從 head->next
改成 head->prev
。
計算目前佇列的節點總量
移走佇列中間的節點
透過快慢指標,先將 fast 和 slow 的起始位置設定在第一個節點,然後 fast 一次移動兩步,slow 一次移動一步,當 fast 或 fast->next 等於 head 的位址時,slow 所在的位置即為中間節點。先用 list_del
移除中間節點的連結,再用 list_entry
取得該節點起始位置,呼叫q_release_element 將該節點的值和節點本身的記憶體釋放掉。
在已經排序的狀況,移走佇列中具備重複內容的節點
走訪整個 queue,當現在走到的節點與下一個節點的值相同時,就將現在的節點刪除,然後設定 duplicate = true
。當現在走到的節點與下一個節點不同,且 duplicate
的值是 true
時,也要將現在走到的節點刪除,這是為了要刪除連續重複節點的最後一個。
交換佇列中鄰近的節點
走訪整個 queue,先將節點刪除連結,再將它連結加到下一個節點的後面,然後 safe
要成為 node
節點的下一位,因為接下來 node
會存取 safe
的位置,需要讓 node
指向兩兩 swap 中的前面那一個。
以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點
走訪整個 queue,先將節點刪除連結,再將它連結加到原本 queue 的最後一個節點 last
後面。一直重複這個動作,直到走訪到 last
結束。
以遞增順序來排序鏈結串列的節點
參考了 你所不知道的 C 語言: linked list 和非連續記憶體 中, Merge Two Sorted Lists 的 indirect pointer 寫法、Linked List Sort 的 merge sort 寫法,以及 mm0809 的共筆 q_sort 的寫法。
next
指向 NULL,以 next
的方向來看,queue 變成不是 circular。mergeSortList
,找 queue 的中間節點,將 queue 分成兩半,在遞迴呼叫自己,將分開的兩半分別再繼續分成兩半,直到分成只有一個節點的時候(只有自己就是已排序的狀態),開始呼叫 merge
。merge
會將兩個 queue 依據值由小到大合併,一直呼叫 merge
直到整個 queue 都被 merge 完成。next
有連接到對的節點,所以最後需要將 prev
修正成對的節點。先將 lib/list_sort.c
和 include/linux/list_sort.h
放到 lab0 的專案下,然後在 Makefile
加入要編譯的 list_sort.c。
宣告一個 sortmode 變數,用來切換要執行的 sort 函式。
在 qtest.c
中加入 option 參數 sort,根據不同的 sortmode,來切換 sort。
原本執行 q_sort 的地方,根據 sortmode 來確定要執行原本自己寫的 sort 或 lib/list_sort.c
。
因為實作 compare function 時不會用到 list_sort 的第一個參數,所以第一個參數為 NULL,第二個參數和原本呼叫 q_sort 傳入的參數一樣,第三個是放 compare function 的 function pointer。
在 list_sort.h
有定義 compare function 回傳值必須為整數,參數有三個,第一個是 priv,但用不到,第二、第三個參數為指向 struct list_head 的指標,透過這兩個指標所在節點的值去比大小。
實作出的 compare function:(在 qtest.c
)
lib/list_sort.c
有用到在 linux/compiler.h 下的巨集:likely, unlikely,將兩個巨集直接搬到 list_sort.c
當中:
在 merge_final
中會用到型別 u8
,因為未定義 u8
所以會錯誤,將其改成 uint8_t
,並加上標頭檔 #include <stdint.h>
即可。
將原本 linux 相關的標頭檔刪除
然後移除:
cppcheck 認定變數 head 沒有指派任何值,但是 head 在後面是有被指派值的。
解決方法是在該變數宣告的上一行加上:
抑制 cppcheck 檢查該行 unassigned 變數。
分別測試自己寫的 sort 和引入到本專案的 lib/list_sort.c
各自 sort 字串,並測試 5 次做平均。
trace-original-sort.cmd
:trace-kernel-sort.cmd
:執行 ./qtest -f traces/trace-original-sort.cmd && ./qtest -f traces/trace-linux-sort.cmd
得到的結果:
sort 的字串數量 | original sort | linux sort |
---|---|---|
100000 | 0.029(s) | 0.024(s) |
1000000 | 0.637(s) | 0.489(s) |
kernel 的 sort 表現得比較好,分別快了 20.8% 和 30.2%。
合併方式是當有 個節點時,合併前兩個 變成 ,並留下一個 不動,維持著合併:不合併為 2 : 1 的比例,因為只要 可以放得進 L1 cache,可以避免 cache thrashing。
count
為 pending list 中節點的數量,在 count
變 count + 1
後,可以觀察到第 k
個 bit 會改為 1,0 ~ k - 1
個 bit 會變 0,此時會將 2 個 合併,並留下一個 。
每當 count + 1
,可能會有兩種情況:
count + 1
後為 ,就不合併(只有 leading 1 是 1,其餘都為 0)count
= 1()count + 1
= 2()count + 1
為 2 是 2 的倍數,所以此種情況不合併。count
= 2()count + 1
= 3()count + 1
為 3 不是 2 的倍數,所以此種情況要合併。count
變 count + 1
後,第 k 個 bit 會改為 1,0 ~ k - 1
個 bit 會變 0。而這裡的 k 為 0,所以會將 2 個 合併,並留下一個 ,也就是合併 2 個 1 為 2,留一個 1 不合併。以下是 count 從 0 一直加到 16 merge 的狀況:
(括號內是當次被合併的節點加起來的節點數量,用加號來分隔尚未合併的節點,黃色底則是告知此次合併的是 1 + 1, 2 + 2 或 4 + 4 等。)
count 變化 | count 二進位 | merge | pending 上的節點 |
---|---|---|---|
0 -> 1 | 0000 -> 0001 | no() | 1 |
1 -> 2 | 0001 -> 0010 | no() | 1 + 1 |
2 -> 3 | 0010 -> 0011 | yes | (2) + 1 |
3 -> 4 | 0011 -> 0100 | no() | 2 + 1 + 1 |
4 -> 5 | 0100 -> 0101 | yes | 2 + (2) + 1 |
5 -> 6 | 0101 -> 0110 | yes | (4) + 1 + 1 |
6 -> 7 | 0110 -> 0111 | yes | 4 + (2) + 1 |
7 -> 8 | 0111 -> 1000 | no() | 4 + 2 + 1 + 1 |
8 -> 9 | 1000 -> 1001 | yes | 4 + 2 + (2) + 1 |
9 -> 10 | 1001 -> 1010 | yes | 4 + (4) + 1 + 1 |
10 -> 11 | 1010 -> 1011 | yes | 4 + 4 + (2) + 1 |
11 -> 12 | 1011 -> 1100 | yes | (8) + 2 + 1 + 1 |
12 -> 13 | 1100 -> 1101 | yes | 8 + 2 + (2) + 1 |
13 -> 14 | 1101 -> 1110 | yes | 8 + (4) + 1 + 1 |
14 -> 15 | 1110 -> 1111 | yes | 8 + 4 + (2) + 1 |
15 -> 16 | 1111 -> 10000 | no() | 8 + 4 + 2 + 1 + 1 |
merge
函式可以看到 priv 會被當作 cmp 的參數傳入,在其他地方不會用到。q_sort
傳入的參數一樣list 指向 head 的第一個節點,pending 指向 NULL,先檢查是否沒有任何元素或只有一個元素,然後將 head 前一個節點指向的下一個位置指向 NULL,將雙向 linked list 變成單向的。
在 while 迴圈中,會先讓 tail 往前移動到待 merge 的節點,然後在 if 判斷是否需要 merge,最後移動 pending 和 list 的位置,直到 list 沒有節點。pending 從沒有節點,然後一次次將節點從 list 中搬到 pending,等到 list 沒有節點就代表現階段結束了。
當 cmp(priv, a, b) <= 0
表示 a 的值小於 b,因為由小排序到大,所以先接 a 再接 b,cmp(priv, a, b) > 0
表示 a 的值大於 b,則是先接 b 再接 a。
其中 head
會在排序過 list 的最前面,並回傳回去。
以 4, 3, 2, 1 的 list 為例做 list_sort 排序,隨著 count
增加,pending
內節點數量漸漸增加,而 list
內的節點數量漸漸減少:
list->prev = pending
因為 pending 為 NULL,所以 list->prev 也指向 NULL:pending->next = NULL
:在 qtest.c 中的 console_init()
加入:
然後另外宣告一個 do_shuffle()
:
在 queue.c 中宣告 shuffle()
:
利用 Fisher–Yates shuffle 演算法來實作洗牌(shuffle)
q_size
取得 queue 的大小 len
。random
,然後 old
將指向從前面數來第 random 個節點,new
會指向最後一個未被抽到的節點,將 old
和 new
指向的節點的值交換,再將 len - 1。Pearson's chi-squared test 能檢驗虛無假說(Null hypothesis),即某特定事件在樣本中觀察到的頻率分佈與特定理論分佈一致,事件必須是互斥且機率為 1。
如果要 shuffle 三個不同的數字,會出現六種可能的結果,彼此是互斥的,且發生機率加起來為 1。那假設 shuffle 是公平的(六種結果發生的機率相同),並遵守 Uniform distribution,那虛無假說為:
接下來透過 Pearson's chi-squared test 來驗證 shuffle 三個數字的頻率是符合 Uniform distribution:
在測試 shuffle 1000000 次後,六種結果各自的頻率如下表:
觀察到的頻率 | 期待的頻率 | ||
---|---|---|---|
[1, 2, 3] | 166391 | 166666 | 0.45375181500726003 |
[2, 1, 3] | 166829 | 166666 | 0.15941463765855063 |
[2, 3, 1] | 166807 | 166666 | 0.11928647714590858 |
[1, 3, 2] | 166790 | 166666 | 0.0922563690254761 |
[3, 1, 2] | 166862 | 166666 | 0.23049692198768795 |
[3, 2, 1] | 166321 | 166666 | 0.7141528566114265 |
Total | 1.76935907743631 |
= 1.76935907743631
對於 N 個隨機樣本而言,自由度為 N - 1。我們 shuffle 3 個數字會有六種結果,因為加起來機率為 1 ,所以實際上可以自由變換的變數只有五個,其中一個結果的機率為 1 減去另外五個結果發生的機率,所以自由度為 5。
對於要拒絕的虛無假說(),觀察到的結果必須具有統計顯著性,即觀察到的 P value 小於預先指定的顯著水準 α。
因為 P value(0.8~0.9)> alpha(0.05),統計檢定的結果不拒絕虛無假說()
也就是樣本資料不拒絕 shuffle 的結果遵循 Uniform distribution,因為沒有足夠證據推翻。
從圖表來看 shuffle 的頻率是大致符合 Uniform distribution 的。
看到在 scripts/driver.py
有用到 subprocess.call()
來產生新 process 執行指定的命令,然後等待命令完成後取得 return code,後續再檢查 return code 是否為 0 來得知測試是否成功。
因為這裡的測試我想取得 stdout,所以改用 subprocess.run()
,會執行參數中指定的命令,然後等待命令完成後,會回傳一個 CompletedProcess instance。
取得 CompletedProcess.stdout
再做編碼後,可以得到 stdout 的字串。
後續再將得到的輸出字串取出 shuffle 過後的結果,放到 nums
變數中。
產生直方圖的程式:
測試 [1, 2, 3, 4] 做 shuffle 1,000,000 次的直方圖:
藉由測試程式發現自己最初寫的 shuffle 程式寫錯了,在所有 shuffle 組合中,只有其中 2 ~ 3 種組合會大量的出現,猜想是因為我在每次 shuffle 時都重設亂數種子,而亂數種子是根據時間當作參數,測試時會在短時間跑很多次,因為時間很短,所以很多次的 shuffle 都會取得一樣的時間參數,導致跑出來的只有少少的幾種組合。
把重設亂數種子刪除後,跑出來的結果就正常了,較平均地出現每種組合。
有參考 PinLin 的 web 實作
先試跑 tiny-web-server 專案,可以在瀏覽器看到跑該專案的目錄下有什麼檔案
,預設的 port 為 9999。
先按照整合 tiny-web-server的引導,有三種方法,其中第三種方法最符合我們的需求。
linenoiseEdit
中加入以下程式碼:select()
要等一個 file descriptor 變成 ready 多少時間。如果 timeout 設定為 NULL,select()
將會無限期的等待一個 file descriptor 變成 ready。這個無限迴圈中,先宣告一個 fd_set 的 set
,然後先用 FD_ZERO
將該 set
中的 file descriptor 移除,也就是初始化這個 file descriptor set。再用 FD_SET
將 listenfd
和 stdin_fd
加到 set
中。
用 select
來監視 set
中的 FD_SET
將 listenfd
是否準備好被讀取了,回傳值 rv
如果是 -1 代表有錯誤發生,0 則代表超過設置的等待時間,成功時,則為三個集合中包含的 file descriptor 數量。
FD_ISSET
會檢查 file descriptor 是否在 set
中。
當 listenfd
在 set
中:
因為已經用 select()
檢查過 file descriptor 是否可讀,可讀代表有新的連線正在等待被 accept()
,accept()
會回傳一個新的 file descriptor connfd
作為後續新的連線,原本的還是會留著用 accept()
接受新的連線。
將 connfd
和 clientaddr
當作參數傳入 process()
,並回傳一個字串 p
,並用 strncpy()
複製 p
的字串內容到 buffer
,然後結束與 client 的連線,用 close()
來關閉。
當 listenfd
在 set
中:
讀取 stdin file descriptor 1 byte 的大小到 c 裡面,如果 read 回傳的數字小於等於 0,就回傳目前 edited line 的長度。
tiny.c
加入專案中tinyweb.c
,並編輯 Makefile
,將 tinyweb.o
也變成需要的目標檔:tinyweb.c
中的 process
函式,其中的 function_name
改成 filename
在 console.c
加入:
在 console.c
中的 init_cmd()
加入 web command:
console.c
的 run_console()
,依照 linenoise 開啟與否來選擇要使用 linenoise()
還是 cmd_select()
有兩種得到命令(command)的方式,一種是 standard input,從終端機輸入 command,另一種是讀檔,例如:執行 make check
的時候會執行一些範例測試,就會需要讀檔。
是標準輸入時,會看 noise 來決定要用 linenoise()
或是 cmd_select()
。
console.c
的 cmd_select()
,新增對應的處理如果 readfds 為 NULL,那就給它一個集合 local_readset
,然後將 readfds
初始化,再將 standard input 的 file descriptor infd
加到 readfds
集合裡。如果 web 還沒準備好 listen,就將 listen 的 file descriptor listenfd
也加到 readfds
。
在 console.c
的 push_file()
中,stdin 的 file descriptor 設定為 STDIN_FILENO
,所以當 infd
為 STDIN_FILENO
時,代表是 stdin 的情況,這裡先將 prompt 的內容:cmd>
輸出,再將緩衝區的內容都輸出。
然後 nfds
和系統呼叫 select()
一樣,要是 infd
和 listenfd
中數字比較大的再加一。使用 select()
去監視,如果在任何一個 file descriptor 準備好之前時間就到期了,會回傳 0,出現錯誤是回傳 -1。
infd
在 readfds
中,就是用 cmdline 取得 command。infd
從 readfds
中移除,然後讀取一行內容,將它放進 interpret_cmd()
中,interpret_cmd()
會將讀到字串解析成 command line,然後執行命令。listenfd
在 readfds
中,就是用 socket 的方式取得 command。listenfd
從 readfds
中移除,然後使用系統呼叫 accept()
取得一個新的 file descriptor connfd
做後續的連線。然後在 process()
中處理連線得到的 URL 字串,將 request 的 filename 轉成和 cmdline 的格式相同,然後一樣放進 interpret_cmd()
執行。將 tinyweb.c
的 main()
移除
避免專案中有兩個 main function 的定義
新增 tinyweb.c
的標頭檔:tinyweb.h
console.c
引入 tinyweb.h
tinyweb.c
中 process()
以下這行移除console.c
中更改 do_web 的部分修改到這裡,目前功能是在我們下 web
指令的時候,將 linenoise
關掉,改用 cmd_select()
。
而且還會再多輸出一次 cmd> web
console.c
的 cmd_select()
加入讓他不要多輸出一次 cmd> web
console.c
的 do_web()
用 curl 下 command 是正常運作。
因為 server 沒有回傳任何東西給 client 就結束連線了,所以會發生瀏覽器以為是網路問題,多次重送 request 給 server。所以要來寫給 client 的 response。
在 tiny-web-server 專案中的 tiny.c
,有個處理 request 的 function: handle_directory_request()
,參考它來實作要回傳給 client 的 response。
在 tinyweb.c
中加入的 send_response()
。
然後在 tinyweb.c
中的 process()
去呼叫 send_response()
:
就可以解決上述問題了。
從瀏覽器發 request 後,可以看到上圖 favicon.ico 的 status 是 404。因為部分瀏覽器會要求 request 中要提供網頁圖案的對應資訊,而我上面的 response 沒有提供。
在 I'm getting favicon.ico error 有提供做法,就是將下面這行加進去 head 裡面即可:
因此修改 send_response()
,將上面的程式加進去:
再次嘗試,就都是 200 OK 了!
更改 bench.c,傳送 HTTP request 給開著的 web server,並接收 HTTP response,查看是否與預期的結果相同。
在 testweb.c 中,傳送 new 的命令,但是回傳回來除了執行結果還多了換行和一個隨機的字元…
因為原本的 web 實作中,HTTP 的 body 只會回傳 ok,現在要將原本輸出到標準輸出的結果也當作 HTTP response 傳回給 client。
在 qtest.c
的 do_operation 系列的函式,常常呼叫 report.c 的 report
和 report_noreturn
,為了將執行結果輸出到標準輸出,其中兩者差別在是否有輸出換行,輸出後有需要換行就會呼叫 report
,不需要會呼叫 report_noreturn
。
在 report
和 report_noreturn
檢查是否有連線,如果有連線,就將結果一起寫進 response 中,如此一來即可回傳執行結果。
參考 AmyLin0210 的思路
執行 make valgrind
會出現(節錄部分輸出):
在輸出訊息中有看到 still reachable
,在 K01: lab0 的 Valgrind 使用案例 中有提到:
still reachable: 程式結束時有未釋放的記憶體,不過卻還有指標指著,通常會發生在 global 變數
查看發生錯誤訊息的路徑:
qtest.c
中第 972 行,呼叫到 linenoiseHistoryLoad
function:linenoise.c
中第 1325 行(在 linenoiseHistoryLoad
function),有呼叫到 linenoiseHistoryAdd
function:linenoise.c
中第 1224 和 1236 行(在 linenoiseHistoryAdd
function)會出現問題:在 history 為 NULL 時,配置記憶體給 history,然後將傳進來的 line 參數用 strdup 複製出新的字串,再將新字串放到 history 裡面。
從 manual 可以看到 strdup 是用 malloc 配置新字串的記憶體:
DESCRIPTION
The strdup() function returns a pointer to a new string which is a duplicate of the string s. Memory for the new string is obtained with malloc(3), and can be freed with free(3).
static char **history = NULL;
,history 是 static 靜態變數,存放在記憶體的固定位置,不會隨函數的結束而被回收。因此要自行將 history 回收,而 linenoise.c
中有 freeHistory
這個 function,只有被 linenoiseAtExit
呼叫到,因為只有在要離開、不需要用到了才會將 history 回收。
回來看 qtest.c
可以發現 main
函式會呼叫到 run_console
,但因為最初執行 make valgrind
就是要讀檔測試的,例如:traces/trace-01-ops.cmd
,而不是直接從 command line 輸入,所以在 run_console
中,我們會執行 21~25 行的部分,是用 cmd_select
而不會呼叫到 linenoise 的函式。
但是在 qtest.c
中我們無論是用讀檔的方式或是從 command line 輸入的方式,都會呼叫到 linenoise 相關的函式,所以應該改成在沒有檔案的時候,才去呼叫 linenoise 相關的函式,才能避免讀檔會發生記憶體洩漏的情況:在 qtest.c
的 main
呼叫了 linenoiseHistoryLoad
,卻又沒能在 run_console
中使用 linenoise 釋放記憶體。因此改成以下即可解決:
再來是測試複雜度的測試中,如果失敗,輸出訊息中也會看到 still reachable
的提示:
在 add_cmd()
中會透過 malloc_or_fail()
配置記憶體給新的 command,然後在 finish_cmd()
中呼叫 do_quit()
將記憶體釋放,所以可能是因為沒有順利呼叫到 finish_cmd()
導致記憶體沒有被順利釋放。
只要在 qtest.c
以下改成:
原本當 ok
為 false 時,就不會呼叫到 finish_cmd()
,因此改變 ok
和 finish_cmd()
的順序,就一定能順利呼叫到 finish_cmd()
。
用 Massif 來測量執行程式會需要用到多少 heap memory,並透過它來看記憶體洩漏的情況。
在終端機輸入:
valgrind -v --tool=massif ./qtest -f traces/trace-01-ops.cmd
會得到 massif.out.18935
的檔案,每次執行後面的五位數字都會不一樣
在終端機輸入:
massif-visualizer ./massif.out.18935
就可以透過 massif-visualizer
來看剛剛產生出來的檔案
可以看到原本動態記憶體是沒有被釋放完全的,沒有歸零。
在修改之後,動態記憶體最終歸零了。
dudect 中的 cpucycles.h
是使用 rdtsc 取得 timestamp:
在 dudect 中的 constant.c
有使用到 cpucycles.h
中定義的 cpucycles:
因為 rdtsc 只會給 timestamp,沒辦法告訴我們一個操作需要花多少時間,所以這裡是在 dut_insert_head(s, 1)
function 執行前,先取得 timestamp,在 function 離開後再取得一次 timestamp,即可得知相對的時間變化,粗略得知該 function 執行多少時間。
先將程式鎖定到一個 CPU 執行,降低 CPU 的干擾因素。
嘗試使用 clock_gettime
代替 rdtsc
,各自執行十次,幾乎都是非 constant time。
下表為按照 insert_head, insert_tail, remove_head, remove_tail 順序,測試出來是否為 constant time:(1: True, 0: False)
rdtsc | clock_gettime() | |
---|---|---|
1 | 1010 | 1010 |
2 | 1110 | 1010 |
3 | 1111 | 1100 |
4 | 1110 | 1010 |
5 | 1110 | 1110 |
6 | 1000 | 1010 |
7 | 1000 | 1010 |
8 | 1100 | 1011 |
9 | 1000 | 1110 |
10 | 1000 | 1110 |
因為不確定自己寫的 insert_head, insert_tail, remove_head, remove_tail function 是不是常數時間,後來有換成將這些 function 內容直接改成 return 0(因為一定會是常數時間),然後分別用 rdtsc 和 clock_gettime() 測試,但是跑出來的結果也幾乎都辨識為非常數時間
有在想 function 直接 return 0 是否速度會太快,會影響到測試之類的,有再將 insert_head, insert_tail, remove_head, remove_tail function 改成執行 10000 次的迴圈,但跑出來也是有非常數時間。
有想過不直接看最後結果是否為常數時間,直接拿 rdtsc 和 clock_gettime() 所執行的時間,各自做一些簡單的統計,像是平均、標準差(如果標準差較小,可能比較穩定(?)),然後將時間分佈做個直方圖來比較,但不確定這麼做是否有意義。
查看即時 CPU 的頻率:
watch -n.1 "grep \"^[c]pu MHz\" /proc/cpuinfo"
使用的 CPU 是 2.9 GHz,換算 rdtsc 的 cycle 變成 ns:
1 / 2.9G * (ns / cycle) 0.3448(ns / cycle)
test_tries
跑一次會測量特定 function 91 * 110(頭尾個去掉 20) = 10010 次,下圖是測量 insert_head 10010 次每次的執行時間,大部分都落在 100~200 ns。
但有時候會出現 outlier,像是開頭的執行時間 1991 ns。
紀錄 fix class 和 random class 兩者執行時間的分佈:
crop 的大小
=