contributed by < TSAICHUNGHAN >
jimmy01240397
- 已修改沒必要的 NULL ,我只需將我的迴圈改成判斷是否回到
head
即可,因此也不須將環狀鏈結串列斷開。
- 在
q_delete_dup
裡我確實寫的過於冗長,我已改進我的程式碼,下次 coding 會避免沒必要的分支以精簡程式碼。
修改後 commit:579a2ef
謝謝jimmy01240397
同學抽空 Review 。
jeremy
jujuegg
你的洞見呢?與其談「心法」,不如設想「如果我是你」,我會怎麼做,示範一次給同學看,而且檢討自己哪裡沒做好,從而呼應原本學員的不足處。清楚標註學員的不足處 (git commits 和描述)。
軟體工程師要學會說故事,本作業要求學員從良性詳盡的批評開始。繼續檢討!
:notes: jserv
對照 Dictionary.com 對 implement 一詞的解釋:
「實作」會是符合上述 "implement" 語意的譯詞,取「起」以「創作」的意思,而「製造」本質上是固定輸入並通常產生固定輸出的行為。
資訊科技詞彙翻譯
clang-format的使用範例
建立一個空佇列,初始化 struct list_head
,須先將 next
與 prev
都指向自身。
避免非必要的項目縮排 (即 *
和 -
),以清晰、明確,且流暢的漢語書寫。
設計流程:
用 malloc 分配記憶體空間給 head
若 head 分配空間無效,則回傳 NULL
使用 list.h
中的 INIT_LIST_HEAD()
來初始化 head
程式碼
使用作業規範的程式碼縮排風格。
避免非必要的項目縮排 (即 *
和 -
),以清晰、明確,且流暢的漢語書寫。
若 head 為無效,回傳
新增兩個 element 指標
使用 list_for_each_entry_safe
可以訪問每個鏈節,且儲存下個連結的鏈節 ???
使用 list_del
移除鏈節後,此時還未釋放節點的記憶體空間
使用 free() 使放鏈節記憶體空間
q_insert_head
, q_insert_tail
避免非必要的項目列表 (即 *
),使用清晰明確的漢語書寫。
false
false
strdup(s)
將 char *s
裡的字串複製給 node->value 指定的位置strdup
返回的值是否無效,若無效則釋放記憶體空間,並回傳 falselist.h
中的 list_add
插到鏈結頭部中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)
q_insert_head
程式碼
q_insert_tail
程式碼
q_remove_head
,q_remove_tail
NULL
list_first_entry
來取得 list 的第一個 elementsp==NULL && bufsize <= 0
則回傳 NULLstrncpy()
將 node->value
所指向的字串符複製到 sp
,最多複製bufsize個list_del
移除list中的節點q_remove_head
程式碼
q_remove_tail
程式碼
更動:
在字串的結尾補上 \0
表示結束
list_for_each_entry
從 head 開始訪問每個鏈節,每經過一個總數加一程式碼
更動:
此部份更動已寫在後續章節「使用 Valgrind 分析記憶體問題」中
false
list_entry
由慢指標得知結構體,再用 q_release_element
進行記憶體的釋放程式碼
更動 1:
當初在設計時忘記是個循環的雙向鏈結串列,因此將鏈結串列的循環給斷開,再最後才把循環接起來。
更動 2:
修改自 jimmy01240397
的建議,只須將迴圈條件設置成當 fast
指標指向 head
即可。
list_for_each_entry_safe
訪問每個節點,並紀錄下個節點跟當下的節點的字串進行比較flag
作記號來判斷是否刪除程式碼
修改自 jimmy01240397
的建議,減少沒必要的分支以精簡程式碼。
list_move
將 cur
的下個節點移至 newhead
的下個節點來進行 swap ,再將 newhead
設為 cur
來進行下組的 swap程式碼
更動:
參考 chiangkd,更改swap實做使程式更直觀。
list_for_each_safe
可以訪問每個節點的同時保留下個節點並進行 reverselist_for_each_safe
定義上的關係 node != head
,因此必須另外定義head的 next
及 prev
程式碼
待改進:
使用 list_move
使程式碼更簡潔易讀
中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)
與 q_swap
實作相似,利用 list_move
可以用更簡潔的方式表示。以 k 個節點為一組進行 reverse ,在將 newhead
變更為前一組的最後一個節點。
list_head
的指標 curr
及 newhead
指向 head
curr->next
每次會被移除到newhead
的下個節點,經過(k-1)次的循環後curr
會間接的變成尾巴的節點。curr
變為新的頭節點程式碼:
參考你所不知道的 C 語言: linked list 和非連續記憶體和學長 wanghanchi 中合併兩個鏈節串列並作排序的方法。
避免非必要的項目縮排 (即 *
和 -
),以清晰、明確,且流暢的漢語書寫。
mergesort
list_head
q_merge_two
q_merge_two
list1
與list2
字串大小list1
或 list2
走到盡頭,剩餘的一方將會接續排序在尾巴q_sort
head->next
的值帶入 mergesort
函式prev
已亂掉,因此重新定義,並把環狀結構接回更動:
在我執行trace-04-ops
時 sort 總是無法順利進行,輸出如下:
此時我才發現佇列並不會跟著鏈結串列一起訪問節點,因此把 list_entry
帶入迴圈跟著存取每個節點。
access 的翻譯是「存取」,而非「訪問」(visit)
題目說明 : 2487. Remove Nodes From Linked List
參考 wanghanchi 的解法,在單向鏈節串列中可以使用reverse
來更加簡潔的表達,在環狀雙向鏈節串列中使用prev
就能更輕易的解決。
head
為無效或為空的鏈節串列,回傳零list_head
的指標 curr
指向 head->prev
及 prev
指向 curr->prev
list_entry
得到佇列裡的資訊strcmp()
進行比較,若 curr_entry
的字串大於 prev_entry
,則刪除 prev
節點,反之使 curr
移動到 prev
所在節點程式碼
更動:
list_head
的指標使用,使程式碼更精簡list_entry
的位置,使每次迴圈都能訪問到佇列位置參考 chiacyu
避免非必要的項目縮排 (即 *
和 -
),以清晰、明確,且流暢的漢語書寫。
檢查 head 是否為空或無效
新增一個 list_head
名為 temp
使用 list_for_each_entry_safe
訪問每個佇列,
用 list_splice_init
將每個節點移動到 temp
的鏈結串列,並將佇列 curr->q
的頭指標初始化為空佇列
用 q_sort
進行排序
使用 list_splice_init
將排序後的 temp
鏈結串列合併到 head
鏈結串列的第一個隊列中
回傳合併後的節點數量
:::info
和 :::spoiler
,讓行文清晰且流暢。作業要求:論文〈Dude, is my code constant time?〉重點提示
論文相關筆記紀錄 : 筆記
LAB0-C/dudect
dudect/constant.c
measure
主要測試 insert
和 remove
功能是否正常。
其測試的方法為用佇列的大小判斷,下方程式碼從 case DUT(insert_head)
中擷取。
dudect/ttest.c
先看結構體
t_init
主要用作初始化 t_context_t
這個結構體
t_push
看似用來確認穩定狀態
t_compute
下方程式碼為 Welch's test 公式 :
dudect/fixture.c
is_##x##_const
函式由前置處理器 concatenation 處理,當中 DUT_FUNCS
包含 insert_head
、 insert_tail
、remove_head
、remove_tail
相關知識可以參考 你所不知道的 C 語言:前置處理器應用篇
differentiate()
主要用作計算執行時間
report()
用作判斷是否為 constant time
doit()
ret
接收 measure
測試 insert
、 remove
是否都正常的布林值。
update_statistics
接收來自 differentiate
的執行時間結果和 classes。
最後 ret
檢查是否為 constant time 。
在 traces/trace-17-complexity.cmd
中看到第一行為 option simulation 1
,可見 simulation 為檢驗時間複雜度的開關,因此先來了解 simulation 與 dudect 的關聯。
在 qtest.c
中看到這段
程式碼更新於 commit : bdcfae3
用 pos == POS_TAIL ? is_insert_tail_const()
可以用 pos
選擇要插入的位置。
從上述程式碼中看到似乎是用 is_insert_head(tail)_const
去回傳是否為 constant time 。
預設測試 TEST_TRIES
次,每次測試時先呼叫 init_once()
對 t_context_t
結構體進行初始化,接著進入 doit
,由 prepare_inputs
輸出提供的值 ,經由 measure
測試功能是否正常及 differentiate
得到執行時間,再將計算得到的執行時間、 classes 輸入給 update_statistics
,若函式都成功執行且為 constant time ( report()
回傳 true
) 則 ret
回傳 true
。
LAB0-C/dudect
達到時間常數在 LAB0-C/dudect
中相較原程式碼 dudect.h 少了 percentile()
功能,即少了裁切測量值。
因此將原始碼的 percentile 加入 LAB0-C/dudect
中並修改,最後終於讓我看到了卡比之星。
修改內容 commit d04d313
讀了一遍這篇論文,對於完全沒有統計基礎的人來說十分挫折,我並不能完全理解裡面分析及理論想表達的內容,反而是透過看同學們的筆記一點一點紀錄才大致了解實作原理,後續有時間再回來複習。
詳細內容可以參考我的 commit:d0493b2 和 commit:c091ea3
設計邏輯遵循 Fisher–Yates shuffle 裡的 The modern algorithm
下方表格為實作概念:
Times | Range | Roll | Scratch | Result |
---|---|---|---|---|
0 | 1 2 3 4 5 6 7 | |||
1 | 0~6 | 2 | 1 2 7 4 5 6 | 3 |
2 | 0~5 | 5 | 1 2 7 4 5 | 6 3 |
3 | 0~4 | 3 | 1 2 7 5 | 4 6 3 |
4 | 0~3 | 1 | 1 5 7 | 2 4 6 3 |
5 | 0~2 | 1 | 1 7 | 5 2 4 6 3 |
6 | 0~1 | 1 | 1 | 7 5 2 4 6 3 |
7 | 0~0 | 0 | 1 7 5 2 4 6 3 |
在 qtest
中輸出結果
接著導入 M01: lab0 所提供的驗證程式輸出
shuffle 的頻率是大致符合 Uniform distribution
由於我在 $make check
總是出現有 ERROR: Freed queue, but 2 blocks are still allocated
。
一開始看完以為問題出在 q_free
上,因此花費了大量時間反覆修改測試,直到我看了 以 Valgrind 分析記憶體問題 才開始使用到工具分析計體資訊,以下為輸出結果。
這時我才發現可能是我的 q_size
造成的問題,以下是我當時的 q_size
及更改後:
原來我這時在設計 q_size
時自作聰明的多幫 node
設置 malloc ,下次在設計時更應該思考配置記憶體的必要。
這問題現在看似很簡單,但在當時 $ make check
的情況看來,我只會認為一定是我的 q_free
在某個地方出錯,我更應該善用工具來分析問題。
感謝 RinHizakura, chiangkd, Risheng
不該只有致謝,你的洞見呢?
__attribute
可以設置函式屬性(Function Attribute)、變量屬性(Variable Attribute)和類型屬性(Type Attribute)。
其中函式屬性可以幫助開發者把一些特性添加到函數聲明中,從而可以使編譯器在錯誤檢查方面的功能更強大。
__attribute__
機制也很容易同非GNU應用程序> 做到兼容之功效。
為何不查閱 GCC 手冊呢?
其中 2、3 代表地二、三個引數不能為空
參考 __attribute__((nonnull)) function attribute
This function attribute specifies function parameters that are not supposed to be null pointers. This enables the compiler to generate a warning on encountering such a parameter
先定義函式第 2、3、4 不能為 NULL
在 linux/include/linux/list_sort.h 中找到 cmp 的 typedef
可以確定 cmp
是函數指標,且回傳 int
,參數分別為 void *
、兩個 struct list_head *
cmp
> 0 ,即為 a
在 b
之後cmp
<= 0 ,即為 a
在 a
之前因 list_sort 為 stable sort ,故不需特別探討小於和等於 0 的區別。
stable sort : Stable sorting algorithms maintain the relative order of records with equal keys (i.e. values). That is, a sorting algorithm is stable if whenever there are two records R and S with the same key and with R appearing before S in the original list, R will appear before S in the sorted list. See here for a more complete description.
ex :
排序前 : 3,5,19,1,3*,10
排序後 : 1,3,3*,5,10,19
兩個3、3*排序前後順序相同
與 merge
作法相似,差別在於最終連結 prev
回上一個節點,形成雙向環狀鏈結串列,在速度上比起每次 merge 後再接 prev
來的更快。
程式碼的註釋提到若合併是極度不平衡(像是輸入已經排序),則會經過多次的迭代,儘管這些迭代不需要。因此 cmp
可以週期性的呼叫 cond_resched()
。
查閱 Linux 核心原始程式碼裡頭的文件和程式碼註解,上述描述不精確。
接著討論 likely
與 unlikely
,在 linux/include/linux/compiler.h 可以找到
__built_expect()
是 gcc 的內建 function ,用來將 branch 的相關資訊提供給 compiler ,告訴 compiler 設計者期望的比較結果,以便對程式碼改變分支順序來進行優化。
!!(x)
用來確保值一定是 0 或 1!!(x)
就無法確保值一定是0或1x==ture
的對應執行程式碼接在判斷後面x==false
的對應執行程式碼接在判斷後面參考自 chiangkd 提供的參考資料 likely / unlikely macro introduction
在看程式碼之前先來看註解的說明 :
若 a
需被排序在 b
之後,則 @cmp > 0
(@a > @b
),反之若 a
需被排序在 b
之前,則 @cmp <= 0
。list_sort 是 stable sort 因此在這裡我們不需特別探討小於和等於 0 的區別。
從這裡開始說明 list_sort 的實作方法,兩個要被 merge 的 list 始終保持 2 : 1 的大小,這可以降低比較次數。
相較 fully-eager bottom-up mergesort 只要出現兩個 大小的 list 就會立刻被合併。而 list_sort 是在出現兩個 大小的 list 的時候不立即合併,而是等到下一個 的 list 被蒐集起來時,前者的兩個 linked list 才會被合併。
避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景。
只要這 2 : 1 的 list (即 的 list )可以完全被放到 cache 裡,就可以避免 cache thrashing。
count
用來計算在 pending lists 的 element
數量count
增加時,第 k 位的 bit 設為 1 ,並清除 k-1 位的 bit 為 0不懂就說不懂,沒有「不太明白」這回事。
後續的註解說明看不懂,因此直接參考 kdnvt 的確保 2 : 1 的實作方法中有非常清楚的說明
引述其的表格 :
count decimal | count binary | merge | before merge | after merge |
---|---|---|---|---|
0 | 0000 | No | NULL | X |
1 | 0001 | No | 1 | X |
2 | 0010 | Yes | 1-1 | 2 |
3 | 0011 | No | 1-2 | X |
4 | 0100 | Yes | 1-1-2 | 2-2 |
5 | 0101 | Yes | 1-2-2 | 1-4 |
6 | 0110 | Yes | 1-1-4 | 2-4 |
7 | 0111 | No | 1-2-4 | X |
8 | 1000 | Yes | 1-1-2-4 | 2-2-4 |
9 | 1001 | Yes | 1-2-2-4 | 1-4-4 |
10 | 1010 | Yes | 1-1-4-4 | 2-4-4 |
11 | 1011 | Yes | 1-2-4-4 | 1-2-8 |
12 | 1100 | Yes | 1-1-2-8 | 2-2-8 |
13 | 1101 | Yes | 1-2-2-8 | 1-4-8 |
14 | 1110 | Yes | 1-1-4-8 | 2-4-8 |
15 | 1111 | No | 1-2-4-8 | X |
16 | 10000 | Yes | 1-1-2-4-8 | 2-2-4-8 |
list_sort 實作 :
count = 0
初始狀態如下圖所示,這邊假設一共有 5 個節點,其中 node1
為 head
此時 bit
為 0 因此不進入 for()
迴圈也不進行 merge
count = 1
此時 bits
為 1 經過一次 for
迴圈 tail
指向 node2
的 prev
,不進行 merge
。
count = 2
此時 bits
為 ,不進入 for
迴圈,但進行 merge
此時 a(node3)
跟 b(node2)
已經合併完成,合併後用 node2,3
來表示
count = 3
count = 3 = 因此會經過 2 次 for
迴圈,此時 tail
指向 node2,3
的 prev
,因 bit
為 0 故此階段不進行合併。
count = 4
count
為 bits
不為 0 進行合併
此時 a(node5)
跟 b(node4)
已經合併完成,合併後用 node4,5 來表示
上述節點都已被加到 pending list ,接著將所有的 pending list 合併在一起
最後將 prev 重新接回變成雙向鏈結串列
list_sort
加入到專案中LAB0_C
專案中list_sort.c
中用到的巨集加入到 list_sort.h
中likely
與 unlikely
,在 list_sort.h
中加入以下程式碼,可以在linux/include/linux/compiler.h中找到list_sort.c
中的 u8
型別Makefile
中的 OBJS 中新增 list_sort.o
qtest.c
include "list_sort.h"
list_sort
cmp
此時參考到 Linux 核心的比較函式撰寫
得知 priv
是個測試用的指標
list_sort
加入到 help 中,可以透過 option list_sort [true/false]
來啟用 sort
或 list_sort
trace-sort.cmd
將其放入 trace 目錄中輸入
即可得到 list_sort
或 sort
的處理時間
參考 perf 安裝
輸入:
kernel.perf_event_paranoid 的會是 4
輸入:
-1
可以將權限全開
list_sort
與 q_sort
效率分析使用 Metric prefix,而非「萬」
資料數 | q_sort | list_sort |
---|---|---|
100k | 0.066 | 0.039 |
200k | 0.167 | 0.128 |
300k | 0.290 | 0.202 |
400k | 0.414 | 0.290 |
500k | 0.547 | 0.387 |
600k | 0.687 | 0.479 |
700k | 0.816 | 0.579 |
800k | 0.951 | 0.682 |
900k | 1.102 | 0.753 |
1000k | 1.255 | 0.856 |
使用 gnuplot 製圖工具畫成折線圖方便觀察
從圖看出當資料量越大 list_sort
的處理效率明顯更好
接著使用 perf
工具分析更詳細的內容
輸入
不要急著比較程式效能,你該思考:
謝謝老師的指教,我會在更進一步的分析
jeremy
重複 5 次該程序 ,並顯示每個 event 的變化區間,接著分析對一百萬的資料做 sort ,並分析 cache-misses
、 cache-references
、instructions
、 cycles
。
得到以下結果
q_sort
list_sort
測試內容 | q_sort | list_sort |
---|---|---|
cache-misses | 60,203,429 | 47,561,816 |
cache-references | 91,423,384 | 71,827,278 |
instructions | 4,732,222,486 | 4,693,241,118 |
cycles | 7,353,673,626 | 5,615,783,740 |
由以上資料分析得知:
list_sort
相比 q_sort
少了 21% 的 cache-misses
q_sort
快了約 31%中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)
留意各式細節,唯有重視小處並步步為營,方可挑戰原始程式碼規模超越三千七百萬行的 Linux 核心
收到
jeremy
目前只是原封不動的加入到 lab0
後續再將其修改與解讀
在 console.c
中加入
和 do_ttt
函式
更多合併過程可以參考我的
在合併的過程中遇到的問題:Cppcheck: Null pointer dereference
探討:
hlist_entry
hlist_entry
為 container_of
的包裝container_of
中有這段(type *)0
將 0 轉型成 null pointer
,是為了取得 member 的 type 。
因此修改 pre-commit.hook
,在 CPPCHECK_suppresses
中加入這段
在 jasperlin1996 的筆記中也有遇到相同問題,且探討的更深入。
新增電腦對決電腦的功能至 qtest 中
問題:
每次執行的結果都相同,應增加更多隨機性使其結果有更多變化
TODO:
在對弈的過程中,要在螢幕顯示當下的時間 (含秒數),並持續更新。
參考:Monte Carlo Tree Search Wiki / Monte Carlo Tree Search 解說
四步驟:
從根節點開始,由 UCB 判斷該往那的子節點進行移動,其中 UCB 將在最佳策略與探索新路徑做出平衡。
: 該 node 贏得的次數
: 該 node 總共進行次數
: Parent 的模擬次數
: 權重 (可以自行調整 c: )
總結:
=> exploitation (選擇已知最好策略,即勝率最高)
=> exploration (選擇探索已知路線)
此方程式不完全只走目前已知勝率最高的路線,而是在探索與最佳策略之間切換來達到平衡。
為何不建議在 Linux 核心裡使用浮點數運算 :
拖慢執行速度,還需要手動儲存/取回相關的 FPU 暫存器。
其中 FPU 造成之問題:Lazy FP state restore
來自 Linux 核心原始程式碼的 Load Average 處理機制 => linux/kernel/sched/loadavg.c
理解這定點數的乘冪花了我些時間了解,先從其註釋看起
寫成數學式:
例如 () :
下面程式碼的目的為決定是否要將 乘入,以 為例,在第一次迴圈中 n & 1
為 false
,故 不會乘入其中。
到進行下次迴圈時 變為 再下次為 以此類推
較大的縮放係數,可以使其數值範圍較大,但反之精度會較差,在這裡縮放係數表示為 。
在老師提供的 mcts
中 score
使用的是 double
,因此要將其改成定點數運算,使用到 stdint.h 中的 uint64_t
來取代 double
。
在 UCB 的計算中用到 log2 與 sqrt 的運算,需先將其改成使用定點數的運算。
log
參考< jujuegg >的實作使用泰勒級數展開來取得近似 的值 :
sqrt
接個引入第三週測驗中的開方根函式