contributed by <ire33164
>
Linux Kernel
1
& 解釋程式碼由 union
的特性可知
xs
的結構體所占的空間會由 union
需要最大空間的成員所決定
因此比較三個成員 :
char data[16]
: 需要 16 個 sizeof(char)
的空間 -> 16 bytessizeof(uint8_t)
(15 bytes) + 4 bits (space_bit) + 4 個 1 bit -> 16 byteschar *
(8 bytes) + 54 bits + 6 bits -> 16 bytes - 4 bits因此 xs
可使用 16 bytes
的空間
利用 GDB 追蹤記憶體使用空間如下 :
data
, filler
, ptr
位在相同的記憶體位置space_left
, is_ptr
, flag1
, flag2
, flag3
共用 8 個 bitsize
+ capacity
共用 60 bit 的話,剩下四個 bit 為 is_ptr
, flag1
, flag2
, flag3
該設計中
char data[16]
: 限制 stack
中最多只能放入 15 bytes 的字串terminator
其為 null
也是 0
uint8_t filler[15]
: 前 15 bytes 是用來存取字串予以保留space_left
: 因 stack 上最多只可放 15 () 個 char
,因此僅需要用 4 個 bit 來儲存目前 stack 上有多少 free byte (還可以存放幾個字元),運算方式為 15 - (目前 stack 上的字元數量)is_ptr
: 若字串長度 > 15,即需要使用到 heap 時,設為1
其餘都為0
,又字串長度 <= 15 時,最後一個 byte 本就會被設為 null
也就是 0
,因此也巧妙的設定 is_ptr
為 0
flag1
, flag2
, flag3
: 未使用char *ptr
: 若使用到 heap 即 is_ptr
為 1
時,用來儲存字串的開始位址size_t size : 54
: 提供 54 bit 空間儲存字串 size,因此字串最大長度為 size_t capacity : 6
: 記錄實際分配到的記憶體空間,使用 6 bit 紀錄是為了要可以裝下 size
的 54 bit,其最多可分配到 = 63 的空間,也可以裝下 size
的 54,其中實際分配時是以 下去分配,也就是會分配到 1 << capacity
大小的空間is_ptr
, flag1
, flag2
, flag3
xs *xs_new
與 AAA上述程式碼是根據儲存的字串長度去初始化一個新的型別為xs
的物件
x
的 space_left
為 15因為
strlen
僅回傳字元\0
前的個數,因此長度必須再加上 1,給予\0
空間
len
若大於 16 就代表需配置到 heap 的空間len =
strlen(p)
+ 1
AAA 為 16
capacity
儲存size
儲存字串長度is_ptr
設為 1
,因配置空間為 heapmemcpy
將指定的字串複製到 ptr 指向的記憶體中xs *xs_grow
用來將 x
的空間增長到一定的 size
x
目前配置的位置做不同的處理realloc()
The realloc() function changes the size of the memory block pointed to
by ptr to size bytes. The contents will be unchanged in the range from
the start of the region up to the minimum of the old and new sizes. If
the new size is larger than the old size, the added memory will not be
initialized.
即使用 realloc 可以在不修改到內容的情況下,重新設定需要的空間大小
buf[16]
中malloc
配置需要的記憶體memcpy
複製回新 allocate 的空間xs *xs_concat
將串接三個字串到 string
中,其順序為 prefix
+ string
+ suffix
string
配置的空間是否足夠提供串接後所需的空間string
配置的空間足夠提供串接後所需的空間sufs + 1
是因為除了複製字串外還有複製 \0
space_left
memmove
The memmove() function copies n bytes from memory area src to memory
area dest. The memory areas may overlap: copying takes place as though
the bytes in src are first copied into a temporary array that does not
overlap src or dest, and the bytes are then copied from the temporary
array to dest.
memcpy
The memcpy() function copies n bytes from memory area src to memory
area dest. The memory areas must not overlap. Use memmove(3) if the
memory areas do overlap.
兩者的差別在於 memmove 會先將欲複製的空間存到 temporary array
再將 temporary array 的值複製到 dest 中
但 memcpy 是直接複製過去
memcpy 的方式在 dest 和 src 位址有交疊 (overlap) 時會產生錯誤
string
配置的空間並不足以提供串接後所需的空間xs
string
的記憶體空間xs *xs_trim
& BBB & CCC去除字串中 trimset
的字元
trimset
check_bit
和 set_bit
中 mask[(uint8) byte / 8]
相當於 mask[(uint8) byte >> 3]
,其實也就是實際上只用到 8 - 3 = 5
bitBBB
最多只會用到 的空間,由此推論 BBB
為 32BBB 為 32
trimset
的字元 8 bit 拆成兩部份
mask[index]
的值\
的 ascii code
為 0101 1100
,那麼就將 mask[0000 1011] 的從右邊數來第 4 個 bit 設為 1
,代表 \
是 trimset
的成員,需要被裁剪掉為何是 5 3 分?
猜測是因 char
為 1 byte 的型別,因此 mask 的 type 為 uint8_t
接著因為每個 mask 都有 8 個 bit 可以使用
也就可以用來標示 8 種狀態 (0 ~ 7) -> (0 ~ )
所以就將 char
的後 3 bit 作為 1 shift 的依據
剩下的 8 - 3 = 5
則當作 mask
的 index
就可以完整表示 8 bit 的 char
trimset
的成員\
是否為 trimset
的成員,就看 mask[0000 1011] 的從右邊數來第 4 個bit 是否為 1
,因此 CCC 使用 &
較為合適,將 (指定的 bit & 1) 可以直接得到原本的值,如 1 & 1 == 1
, 0 & 1 == 0
CCC 為 &
trimset
的成員紀錄到 mask
中check_it
從頭逐個檢查字串中的字元是否為 trimset
的成員trimset
成員的 offset 紀錄到 i
中i
代表字串中第幾個字元開始不為 trimset
的成員check_it
從尾巴開始逐個檢查字串中的字元是否為 trimset
的成員slen
的值,直到碰到字串中最後一個不為 trimset
的成員的字元slen
代表字串中從頭開始數到最後一個不為 trimset
的成員的字元的長度trimset
的成員的位置trimset
成員的長度trimset
成員的字串複製回 orig
中 => 成功去頭去尾xs xs *cpy
(考慮 CoW)參考 folly::fbstring 中記憶體配置的技巧設計實現 reference count
size_t RefCount_
: 用來紀錄 reference countchar data_[0]
: 利用宣告長度為 1 的陣列,可以直接紀錄下 struct Ref_Count
真正需要的記憶體空間的尾端,而這個 address 同時也是存放字串開頭的位址data_
中,並讓 xs 的 ptr
指向 data_
,因此只要將 ptr
的位址減去下方的 rc_getDataOffset ()
即可取得 Ref_Count 並對其操作data_
取得 struct Ref_Count
的 address 時會使用到offsetof
The macro offsetof() returns the offset of the field member from the
start of the structure type.
offsetof 會回傳 struct 開頭與其 member 間隔了多少 bytes
struct Ref_Count
的 address 以利後續對 Ref_Count 的操作data_
的 address,往前扣掉 data_
與 struct 開頭的 offset 即為 struct Ref_Count
的 addressstruct Ref_Count
struct Ref_Count
中的技巧,因此這邊 malloc
的空間為 offset
+ 字串需要的空間處理完麻煩的 Reference count 後
接下來就剩應用了
xs
時傳入的字串長度超過 16 ,則 create Ref_Count 並將 ptr 指向 Ref_Count 中的 data_
測試 xs_new 和 xs_cpy
輸出 :
refcount 為 2 符合預期
x
已經在 xs_concat()
中處理過 cow 的問題,因此若 x
在 heap
上可以直接使用 realloc
重新分配記憶體stack
上,則改成 allocate 到 heap 上並比較長字串辦理,建立 Ref_Countheap
上,且該字串有被複製或是複製別人而來,就必須真的複製字串 (因為要修改了)
malloc
的記憶體空間heap
上,就將 RefCount 減一stack
上,直接把 data
改成空字串測試 xs_concat & xs_grow & xs_free
預期輸出 :
實際輸出 :
Memory leak 問題
跑跑看 valgrind 來偵測是否有 memory leak 的問題
輸出 :
在 function rc_create 中存在 memory leak 的問題
但目前找不到問題所在
決定先把其他項作業要求完成
後來發現原來是
main
裡的xs
忘記 free 阿,真的很白痴 = =
heap
上且此字串確定要修改malloc
的記憶體空間orig
指向新的 ptr
測試 xs_trim & xs_cpy
預期輸出 :
實際輸出 :
TODO :
考慮到 race condition 的問題
要將 Ref_Count 中的 RefCount 改成利用 atomic 實做
strtok
功能實作char *strtok(char *str, const char *delim)
str
: 欲被分割的字串,若為 NULL
則延續前一次的 str
delim
: 用來分割的字串return
: str
中第一個被分割下來的字串xs_trim()
都有使用到字串位元的比較,因此沿用 check_bit
和 set_bit
strtok()
中傳入 NULL
後可以從上次切的字串符開始切x
為 NULL
,則沿用先前的 remain_str
start_token_idx
: 紀錄欲回傳的字串開頭的 idxend_token_idx
: 紀錄欲回傳的字串結尾的 idx + 1delimit
或字串只包含 delimit
就回傳 NULL
malloc
需要的空間並利用 memcpy
複製指定位置的 stringremain_str
指向取下字串的下一個字元這樣的方式在呼叫 xs_strtok() 回傳的字串在使用完後是需要利用 free() 來釋放記憶體的
但其實有另外的方式可以不用特別 free() 那就是要直接去改動原 xs
中的字串
直接將字串用 '\0' 切開
但這會牽涉到 CoW 的使用,根據我的寫法也是須另外 malloc() 出空間
因此想說既然都要 malloc() 那就先實做不牽涉到 CoW 也就是維持原字串值
而需要 free() 的版本
測試 xs_strtok
輸出 :
測試 memory leak :
結果 : 沒問題!!
觀測有使用 copy-on-write 與沒使用 copy-on-write 兩種情況的記憶體使用情況與效能差距
實做一個不使用 copy-on-write,直接複製字串的 function 作為實驗的對照組
分別使用 xs_cpy() 和 xs_cpy_without_cow() 做 1000000
次的字串複製
使用 Valgrind 觀察 :
根據這次使用 valgrind massif 分析的結果可以明顯看到使用 copy-on-write 的記憶體使用量遠小於直接複製的結果
使用 perf 觀察 :
根據這次 perf stat 的結果可以明顯發現使用 Copy-on-Write 後
cache-miss
: 從 1062942
降到 7993
cache-reference
: 從 1,212,514
降到 51,188