contributed by < WayneLin1992
>
linux2021
list_head
結構INIT_LIST_HEAD
get_middle
使用龜兔賽跑技巧, fast 前進兩位, slow 前進一位,等到 fast 繞一圈,因此 COND1 = fast->next = list
COND2 = fast->next->next == list
, slow 會在中間的位置,所以 TTT = slow
。
TODO: 探討偶數和奇數個節點的 list 在上述演算法的行為
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
當總節點為奇數(共五個節點)時:
初始位置
第二迴圈
第三迴圈:因為 fast->next->next
會指到 list 所以會 break,而此時 slow 指向第三節點。
當總節點為偶數(共四個節點)時
初始位置
第二迴圈
第三迴圈: 因為 fast->next = list
所以 break 此時由上圖知道 slow 指向第二節點的位置,
總結:由此可知 slow 是指向中間的位置。
merge_sort
查看 list_cut_position
可以看出來 MMM 的位置是 struct list_head *node
題目是要 merge sort 實作,需要的是 get_middle
中 slow 的位置,&get_middle(&q->list)
因為 get_middle
回傳 type 為 list_ele_t
,但需要的是 list_head
所以 &get_middle(&q->list)->list
assert
由 C99 規格書 7.2章,講解 assert.h
,允許寫狀態診斷訊息
assert(validate(head));
可以由 validate( )
看出此 function 為判斷,是否有從小到大依序排列,假如沒有就會出現下行的訊息。
改善空間,事實上 queue_t
完全是多餘的, list_ele_t
中 next
也可以捨去
其中最需要注意的就是 free 部分
要用 container_of 知道 struct 起始位置,才能 free 掉。
經過這幾次作業的經驗得知,涉及到 C 語言結構體的記憶體釋放,往往要在每個層面都相當熟悉結構體成員的組成,否則就會造成各式記憶體操作的錯誤。我曾經撰寫一個小程式 nalloc,在 C 語言標準函式庫的 malloc 和 free 之上建構一層包裝,達到 structure aware memory allocator 的作用。
舉例來說,你可以這樣使用 nalloc:
注意在 matrix_delete
函式中,你只需要呼叫一次 nfree
,即可將 matrix
結構體內部的 data 成員和結構體本身釋放。nalloc 並未增加太多成本,你可一併思考這樣的實作是如何達成。
還可以實做出 show 看到實際排序後的結果。
__attribute__((nonnull(2,3)))
代表2和3參數不能空。
@cmp_func: pointer to comparison function
與傳統不同的cmp
方式。
將 value 轉成 int 使它能讓 random 數值寫入,並且還需要將程式轉成 user level 所能執行達到 standalone 的目的。
將 quiz1 的 quick sort 及 non recursive quick sort 一起比較後,如下圖
非遞迴 優點:能降低 cache missing 的程度,遞迴 優點:程式好寫,由下表可以看出非遞迴比遞迴的 cache missing 低很多, quick sort 的時間複雜度,快的時候能到 O(nlogn) 慢的時候到 O(n2),為了降低 quick sort 的時間複雜不確定性,將 pivot 能夠盡量的在中間值,能夠複雜度可以降到 O(nlogn)。
但經過我重新思考 quiz1 的改寫,發現到我有修改到 pivot 的位置,導致非遞迴 quick sort 時間複雜度較差。
在測驗一次
將 linux list_sort standalone 來比較
開啟 perf 權限
linux kernel merge sort 和 merge sort 比較
linux kernel merge sort 在 cache reference 和 執行時間上 都較好的表現。
quick sort 和 quick sort non recursive 比較
quick sort non recursive 在 cache reference, CPI和執行時間 都有更好的表現。
linux kernel merge sort 和 quick sort non recursive 比較
linux kernel merge sort 在 CPI 及 執行時間好一點。
其中 quick sort non recursive 有比較好的 cache misses 表現,在 privot 及 non recursive 優化的效果是巨大的。
TODO:
func(21)
N = 101012 第一行 10101 | 01010 = 11111
回傳為 (N+1) >> 1
時 N = 100002 = 1610
is_power_of_2
輸入參數 type
為 unsigned long
代表不考慮有號的情況,將實際數值帶入,令 n = 1610 = 100002 代入後 10000 & 01111 = 00000
故得 n is power of 2。
將非 2 的次方,代入時,令 n = 1310 = 11012 代入後1101 & 1100 = 1100
得到非零解,所以 n is not power of 2。
roundup_pow_of_two
有 fls_long
觀察一下
fls64
及 fls
find last set 找最後一個有值 bit 的位置,sizeof(l)
判斷 unsign long
因為 long
有 4 bytes (Windows) 也有 8 bytes (Linux)。
TODO: 許多現代微處理器提供 Find first set 指令,找出 Linux 核心原始程式碼對應的硬體實作定義,如 x86_64 和 aarch64。
aarch64: clz
Count Leading Zeros
clz
的操作 e.g. clz(0001010100011011)
= 3,從MSB到最後1出現零的數量。
x86: TZCNT
Counts the number of trailing least significant zero bits e.g. tzcnt(0000 0000 0000 0000 1000 0000 0000 1000)
= 3 ,從 LSB 到1出現零的數量。 ffs = tzcnt + 1
Linux Armv5+ 為例
e.g. ffs(5403)
= fls(0001010100011011 & 1110101011100101)
= fls(0000000000000001)
= 1 從 LSB 開始計算到第一個 1 出現的位置。
e.g. fls(0001010100011011)
= 16 - 3 = 13 此為 16 bit 所以用 16 bit 相減。 從 LSB 開始計算到最後 1 的位置。
__clz
為比 clz 更有效率的實作 e.g. __clz(0x80000000)
= 0 。
延伸閱讀:
TODO: 探討 Linux 核心原始程式碼中,為何會有 clz
和 __clz
這樣貌似同名但實質不同用法的函式,從而分析 Linux 跨平台支援的手法
clz
在 arm 中的定義 CLZ{cond} Rd, Rm
其中 Rm operand register, Rd destination register。
__clz
__
在 linux 代表實際實作,而 clz
由個硬體進行提供。
clz
在 MIPS 中的定義 CLZ rd, rs
其中 rs source register, rd destination register。
參考資料:
MIPS
arm __clz complier
arm clz
1UL
為 unsigned long int 裡面值為 1 ,令 n = 1010 = 010102 代入得到 fls_long(n-1)
得到 4 也就是 1UL << 4
= 100002 = 16 達到 roundup the power of 2 無條件進位的目的。
其中fls_long(n-1)
的 n-1 使原本就是 2 的次方的數值,保持一致
由此可以推測出 rounddown 只需要修改成 fls_long(n)-1
就成無條件刪去
我們需要一種配置記憶體的方式,來管理比 page 還小的記憶體,這就是 slab allocator 配置的大小將會以 power of two 來配置 (如:要 10 他就會配置 16)。
slab 配置方式 kmalloc
: 負責配置比 page 還小的記憶體空間。(最小為 32 bytes 最大為 32MB)
/linux/slab.h
page 的大小並不會固定以 4KB 呈現,會因為不同的結構有所不同 aarch64 in linux 有 16KB, IA64 in linux 有 4KB, 8KB, 16KB, 64KB 不同。
參考資料:
Wikipedia: Page
slab 記憶體配置:
cache friendly and benchmark friendly
目的: page 記憶體配置由 page allocator,但很多時候 不需要 page 那麼大的空間,這時就可以使用 slab allocator 配置 page 以下的記憶體空間,而且 cache 對 slab 的配置是很敏感的,而且 slab 所存入將是 object ,如下圖所示
操作:
static struct kmem_cache
存指向下一個 kmem_cache 的指標, object 大小。
struct kmem_cache_node *node[MAN_NUMNODES]
其中將有 slab 的狀態,狀態可以分三種 full, partial, empty 代表 slab 的空間。
struct array_cache __percpu *cpu_cache
避免找空隙時需要 O(n) 所以會在 array 中放入空隙的 index 方便搜索空隙。
void *freelist
page 空隙的位置。
void *s_mem
指向 page 第一個 object 。
NUMA (Non-uniform memory access):
在 SMP (對稱多處理器)時,要使用單一 BUS 向記憶體提取資料,而處理器處理資料的速度高於記憶體給資料速度,處理器會產生 "starved for data" 的情況,所以 NUMA 就希望每個處理器,都能有個 BUS 連接記憶體,避免壅塞的情形,所以延伸出的做法,每個處理器都有屬於自己的本地記憶體 ( local memory),這裡講的本地記憶體,是只靠近處理器的(記憶體包括: cache 、 memory)
NUMA 對 slab 的影響:
在 NUMA 下除了本地記憶體還可以透過詢問其他處理器使遠方記憶體可以進行配置,使 slab 在尋找 slabs free 變得緩慢,所以在 kmem_cache_node
中增加 partial list, full list, empty list,使得 slab allocator 效率整體上升 5%
參考資料:
The Slab Allocator in the Linux kernel
NUMA aware slab allocator V4
由 if 的條件可以知道, read_lhs > 0
代表 _read & 7 > 0
意思是 _read
讀取的內容不足 8 bit , data = *source++
所以我們要 RRR = data = data << read_lhs
使 data 成為 8 bit , bitsize > write_rhs
判斷寫入位元 及 位數,上面註解有寫 write_lhs + bitsize is never >= 8
DDD = mask |= mask_rw[write_lhs + bitsize];
可以使用一個表格就好,但表格的參數要有改變,用一個 bit 一個 bit 為代表。
經測試後得到相同結果
程式目的: string interning 將 string 存入 hash 中,當想使用時只需要將指標指向 string 即可,不需要額外存取,來改善記憶體的使用效率。
查看 cstring 是否有對應的 interning。
cstr
: 字串最長能存到 CSTR_INTERNING_SIZE
= 32
。
hash_size
: hash table 的大小
type
: CSTR_ONSTACK
string
存入 stack 中, CSTR_INTERNING
存入 heap 中, CSTR_PERMANENT
存入 loop 中。
ref
reference counting:負責記錄空間是否有被使用過,有使用就會 count++
。
當 count = 0
時,代表空間沒在被使用過,就可以 free 掉。
用 hash table 將 cstring
包裝起來。
這 macro 表達創建一個空的 hash table , ## 為連接的意思 e.g. CSTR_BUFFER(king)
表示 king##_cstring
為 king_cstring
,起始 type = CSTR_ONSTACK
。
CSTR_INTERNING_SIZE
= 32
INTERNING_POOL_SIZE
= 1024
cstr_cat
function 目的:名稱 cat
可以看出,他是將 str
與 sb->str
連接的意思,連接後還需要把 cstring
中的資料更新, 因此要將本來 cstring->hash_size
+ strlen(str)
並更新, i < CSTR_STACK_SIZE
代表還能寫入,CSTR_STACK_SIZE = 128
代表寫入 cstring 中最長的字串長度 , i
代表已寫入字串的長度, CCC
= i = s->hash_size
依序將 str
中的字串,依序寫入 s->cstr[i]
中,當str == 0
return s
當 type = CSTR_PERMANENT
或 CSTR_INTERNING
才會執行 cstr_cat2
sa + sb < CSTR_INTERNING_SIZE
代表有空間寫入sa + sb > CSTR_INTERNING_SIZE
代表空間需要再額外空間才能寫入,所以呼叫 xalloc
存入 heap 當中。
lock
: 上鎖數量
index
: pool 的 已使用的數量
size
: hash table 的大小
total
: hash table 已使用的數量
buffer
: 會存入與 cstr
相同的 string
在寫入時,CSTR_LOCK()
會 lock 住,並在寫完後解鎖 CSTR_UNLOCK()
pool
: 將會是連續的依序存取 pool[index-1]
hash table
: 會 parameter 存入對應的 hash[hash]