contributed by < 93i7xo2
>
Source: 2021q1 第 2 週測驗題
1
container_of
首先在你所不知道的C語言:技巧篇提到__extension__
是用來防止 gcc 產生警告的修飾字,在這裡用來防止使用到非 ANSI C 標準語句 ({})
(braced-group within expression)時出現警告。
({})
是 gcc extension 的一項,視多個 statement 組成的 compound statement 為 expression,同時最後一個 expression 的值會視為整體的值。作用是讓 marco 更加安全,例如將會造成 double evaluation 的 max()
改寫成
再來參考 How to understand "((size_t) &((TYPE *)0)->MEMBER)"? 的描述,第 3 行可分解為以下步驟
0
上有一型態為 type
的結構memeber
member
的型態__pmember
是指向 member
的 const pointer
,其值為 ptr
。可以看到 0
並沒有任何作用,可帶入任意值。第4行 (type *) ((char *) __pmember - offsetof(type, member));
用意在取得包覆 __pmember
的 struct
起始位置,因此需要 offsetof(type, member)
來取得 __pmember
與該 struct
的相對偏移量。
接著仿效 A simple example with gcc and objdump 透過 objdump
與 gcc -g
觀察編譯後產生的 object file
26~2a 行:將 nptr
複製到 [rbp-0x10]
2e~32 行:使 [rbp-0x10]
與 0x10
相減,對應 (type *) ((char *) __pmember - offsetof(type, member));
,而 0x10
恰好是 4 byte integer + 8 byte pointer + 4 byte padding
的大小。
其中有許多冗餘片段,試著開啟最佳化 -O1
再編譯
list.h
精簡實作Linux 鏈結串列 struct list_head 研究提到一般應用程式使用 linux/list.h
的方法是將要用到的函式複製至code中,而例題與 linux/list.h
顯然有些相異之處。
list_add_tail
, list_del
讓新加入的節點在串列末端,從串列前端移除節點,捨棄 linux/list.h
包裝 __list_add
、__list_del
的作法,意味整條串列操作起來類似 Queue。
list_empty
, list_is_singular
用以判斷串列是否只有1~2個節點,從兩個函式猜測最前面的節點應是作為維護整條串列使用,本身不具有任何資料。
list_splice_tail
插入非空串列 list
底下所有 entry 至 head
串列的最後方。
list_cut_position
目的是將從 head_from->next
直到 node
的部份串列移到未初始化的空串列 head_to
之後,成為新的串列,空串列定義為使用 list_empty
判斷返回 true。如果與 linux/list.h
的 list_cut_position
進行比較,86~92行實際上是將原先用來包裝的 __list_cut_position
寫進 list_cut_position
但缺少下面這段
上方意思是不對空佇列或元素個數只有為1的情況進行處理。出自 commit 00e8a
TODO: 對照閱讀 Linux 核心的 git log/blame,查閱程式碼註解和相關的編修紀錄。程式碼給你,就是希望來日你可以做出貢獻。
git blame include/linux/lish.h
list_cut_position
和 __list_cut_position
被新增進來,之後沒有任何編輯紀錄。
get_middle
先將巨集 list_for_each (slow, list)
展開,看到這段巨集幫我們以 list->(next)
初始化 slow
:
接下來只要定義迴圈的終止條件,返回 slow
的容器即可。
list_merge_sort
搭配 queue_t
一起看
考慮到 list_cut_position
最後一個參數接受指向 list_head
的指標,必須先取得get_middle
返回容器的 list
成員再傳入,而所有新宣告的 list_head
都透過 INIT_LIST_HEAD
進行初始化。
list_merge
延伸問題:
2
假設 N
為 0x8000
,上述程式碼的作用在於將 MSB 以倍數的方式複製至後面每一個位元,至於 MSB 後面是否有非 0 bit 並不重要;若 N
的首個非 0 bit 不在 MSB 位置上,可能提早完成傳遞。作用同註解所說:將右邊所有位元設 1。
toastcheng 發現
uint16_t
因為 integer promotion 的關係實際 Bitwise shift 運算元是擴展成int
處理,遇到uint16_t
最大值 65535 能正確處理,但遇到uint32_t
則有 overflow 的風險,同時提出以__builtin_clz
實作出相容於uint32_t
的版本。
延伸問題:
is_power_of_2
,查閱 Linux 核心原始程式碼,舉例說明其作用和考量,特別是 round up/down power of 2
__roundup_pow_of_two
和 __rounddown_pow_of_two
的實作機制3
以從 read_rhs
起始位置讀取 1 位元組的資料為例
bitsize=8
read_lhs=2
read_rhs=6
write_lhs=5
write_rhs=3
(Round 1) Read
(Round 1) Mask data
(Round 1) Write
(Round 2) Read
考慮變數帶有以下後綴的意義
_lhs
:表示寫入/讀取位元從起始位置,從 MSB 開始_rhs
:表示從 _lhs
開始最多能寫入/讀取的位元數。例如 _lhs=2, _rhs=6
表示最多能寫入/讀取 6 bits。設計上,bitcpy()
儘可能的以1位元組作為寫入/讀取單位以節省操作次數
而在讀取/寫入指定的 count 位元數前,每一次分為讀取與寫入兩步驟,先看寫入:
我們取得 0xBEEF0000
類型向左對齊的資料後再看到寫入,分做兩種情況:
bitsize <= write_rhs
: 不需跨兩位元組寫入bitsize > write_rhs
: 需跨兩位元組寫入第 1 種情況,不需跨兩位元組寫入,只需在一位元組內將舊資料 original
與新資料 data
結合
mask
是中間挖空的遮罩,預期組成為
考慮 bitsize+write_lhs
的各種情況:
8-bitsize-write_lhs | bitsize+write_lhs | expected mask |
---|---|---|
8 | 0 | 0b11111111 (invalid, bitsize 不能為0) |
7 | 1 | 0b01111111 |
6 | 2 | 0b00111111 |
5 | 3 | 0b00011111 |
4 | 4 | 0b00001111 |
3 | 5 | 0b00000111 |
2 | 6 | 0b00000011 |
1 | 7 | 0b00000001 |
0 | 8 | 0b00000000 |
遮罩 mask 前面的部份由 read_mask[write_lhs] ,後者恰好由 write_mask[write_lhs + bitsize] 所對應,因此無跨位元寫入程式碼為: |
第2種情況,跨位元組寫入,情況稍微複雜,但判斷機制與讀取一樣:
以圖示說明兩次寫入的 BYTE 1, BYTE 2 的預期遮罩 mask1
, mask2
同樣考慮 bitsize-writh_rhs
的各種情況,恰好對應到表 write_mask
:
8-(bitsize-writh_rhs) | bitsize-write_rhs | expected mask |
---|---|---|
8 | 0 | 0b11111111 (invalid, ) |
7 | 1 | 0b01111111 |
6 | 2 | 0b00111111 |
5 | 3 | 0b00011111 |
4 | 4 | 0b00001111 |
3 | 5 | 0b00000111 |
2 | 6 | 0b00000011 |
1 | 7 | 0b00000001 |
0 | 8 | 0b00000000 (invalid, ) |
因此
mask1 = read_mask[write_lhs]
mask2 = write_mask[bitsize - write_rhs]
跨位元寫入程式碼為所對應的程式碼為:
總結 read_mask
與 write_mask
的意義:
read_mask
:用來過濾真實的資料。在寫入也會使用到,因為寫入時也是需要將資料讀出來進行合併。write_mask
:在指定位元組上謄出空間給資料寫入。雖然能描述上述程式碼,但不認為能在短時間寫出。
bitcpy
使用 perf -F max
以最大取樣頻率分析函式內部熱點,而為了採樣更多的數據分析,放大原本的input, output, i, j, k
為32倍,此時 perf.data
來到100M。Intel 和 AT&T 語法尚有些微差異,perf report
時使用 -M intel
更好閱讀。
發現函式內部熱點大多集中在 mov
,而 mov
的 register operand 最大可為 64bit,既然如此,實作 64-bit 的 bitcpy
對效能應有所改善。
以下為 64-bit 版的 bitcpy
上面程式碼假定使用者會在呼叫函式前判斷寫入/讀取處理是否越界。
64位元版本的 bitcpy
與原先版本差在 buffer 從 8-bit 變化至 64-bit,這裡用 32-bit buffer 進行說明,於原先的記憶體配置下得到的資料會是:
原先 <<, >>, |, &
之類的操作因為 Little Endian byte ordering 的關係無法使用,因此讀取需要用 reverse_byte
將位元順序翻轉再進行操作,寫入時再翻轉回來,有點像是線性轉換/反轉換。
+0
的技巧是為了去掉 const qualifier。
一開始擔心 &
在一邊是 unsigned
另一邊是 signed
型態轉換上會有問題,在規格書看到會先做 Usual arithmetic conversions,往下找到對應解釋
Otherwise, if the operand that has unsigned integer type has rank greater or equal to the rank of the type of the other operand, then the operand with signed integer type is converted to the type of the operand with unsigned integer type.
放心使用
幫你補充,根據 C99 standard 6.5.7.5
The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type
or if E1 has a signed type and a nonnegative value, the value of the result is the integral
part of the quotient of E1 / 2E2. If E1 has a signed type and a negative value, the
resulting value is implementation-defined.
因此 gcc documentation 有明確定義:
Bitwise operators act on the representation of the value including both the sign and value bits, where the sign bit is considered immediately above the highest-value value bit. Signed ‘>>’ acts on negative numbers by sign extension.
當然不能說你錯,但建議可以改寫避免此用法。
BakudYao
參考 bakudr18 共筆改寫為可讀性較高的用法:
然而 READ_MASK(0)
有 undefined behavior 不得已加上分支
count<64
使用原先版本進行複製,地址、位移需要重新計算,借用到 kernel.h 的 ALIGN_DOWN
觀察複製 0~4kB 的效能差距,每次測試讀取/寫入偏移量涵蓋 0~63,64-bit 版約省下一半以上的時間:
code
看起來很有機會加入減少 branch 的作法獲得不錯的效能提升!也許你可以試試,細節可參考 bakudr18 共筆。
BakudYao
一開始有想過減少分支數量,但 Mispredicted branch 比重太低,而且編譯器 -O3
開下去有時效果更好,會感覺又在做白工。
bitcpy64_likely
偏好跨 8 位元組
雖然如此,還是仿照 bakudr18/quiz2 實作消除 branch-prediction 的版本
測量 0~16 kbits 執行100次的效能
-O0
-O3
發現消弭分支帶來的效益不大。其實以 -O0
重現 bakudr18/quiz2 的實驗也與原本實驗結果有落差,branch misses
並效能差距的主要原因。
延伸問題:
4
延伸問題:
cstr
資料結構__cstr_data
cstr
: 字串。type
: 表示字串 cstr
所處位置,有 HEAP 與 STACK (CSTR_ONSTACK
)。hash_size
: 紀錄字串長度,之後的 hash table 使用此值計算 hash,應改用 length
為名較為合適。hash_size = 0
為特殊狀況(??)。ref
: 字串位於 heap 時,用來紀錄 refcount,從非 0 轉變成 0 時需要要釋放記憶體。__cstr_buffer
cstr_buffer var
經由 CSTR_BUFFER(var)
創立後,透過對以下操作對 __cstr_buffer
內的 str
進行修改
或是透過 cstr_cat()
。由於轉型成指標的關係,函式內部透過 sb->str
存取 str
。
__cstr_buffer
也能創立在 heap。
__cstr_buffer
建立在 stack、cstring str
使用struct __cstr_buffer
包住的動機不明。
cstr_cat
切入以上功能為
s
指向的 __cstr_data
位在 stack 上(由CSTR_BUFFER
創建的結果),儘可能附加 str
到 stack 上的 cstr
後,含 \0
最多 CSTR_STACK_SIZE
字元。若不足 CSTR_STACK_SIZE
則返回 cstr
。__cstr_data
是在 heap 或 stack,透過 cstr_cat2
結合新舊 cstr
取得/生成新的 __cstr_data
,釋放舊的 __cstr_data
物件。往下看 cstr_cat2
cstr_cat2()
最終需返回指向新 __cstr_data
的 cstring
。
a
、b
總長低於 CSTR_INTERNING_SIZE
,返回的是指向 interning table 內元素(__cstr_data
)的 cstring
。
hash_blob(tmp, sa + sb)
作為 hash valuecstr
附加在 __cstr_data
後
type = 0
hash_size = 0
ref = 1
cstr_interning()
作為整個程式核心,負責維護 interned string table (struct __cstr_node **hash
)。
__cstr_ctx
有幾項作用
expand()
倍增容量__cstr_node
的 pool
pool
內元素大小的 index
依據 ISO/IEC 9899:1999 (E) 6.7.8,__cstr_ctx
的成員被初始化為 0 或 null pointer。
If an object that has automatic storage duration is not initialized explicitly, its value is indeterminate. If an object that has static storage duration is not initialized explicitly, then:
— if it has pointer type, it is initialized to a null pointer;
— if it has arithmetic type, it is initialized to (positive or unsigned) zero;
— if it is an aggregate, every member is initialized (recursively) according to these rules;
— if it is a union, the first named member is initialized (recursively) according to these
rules.
interned string table 是一張使用 Separate chaining 處理碰撞的 hash table,
由於 __cstr_data
自身無法連結其他資料,須使用 __cstr_node
包裝。
在 table 內新增 entry 只能透過 interning()
,以傳遞進的 hash
作為 index,__cstr_interning.hash
上儲存的無疑是指向一連串連續 __cstr_node*
起始位置的指標。
__cstr_data
pool
新建 __cstr_node
,這樣做的好處是這些新建的 __cstr_node
都使用相鄰的記憶體位置,甚至連 __cstr_node.str.cstr
的空間也不分配,直接指向 __cstr_node.buffer[]
。
示意圖如下
CSTR_LITERAL
用法
產生的 __cstr_data
可能具有最後一種不會被 cstr_release
影響到的型態:CSTR_PERMANENT
,具體行為如下:
cstr_clone()
嘗試將 "Hello string" 放到 string interned table 中,或仿照 cstr_cat2
複製到 heap 上
無法想像到
__sync_bool_compare_and_swap
失敗的情境
cstr_release
將只有位在 heap 的 __cstr_data
且型態非
CSTR_PERMANENT
: 不於 table 又在 heap 上,由使用者自行維護CSTR_INTERNING
: table 內物件進行 refcnt - 1
並釋放,atomic operation 確保一個 refcnt
不會同時進行操作。
cstr_grab
整份程式也只有呼叫過一次,不難看出為了在離開函式也能使用 __cstr_data
,stack 上的物件必須複製一份 (行 7)。之後符合 s->ref == 0
的情況 (行 8),不可能發生。
cstr_equal
值得注意的是行 12~16 的邏輯,比完 hash value 才去比較字串 cstr
,拿去計算 hash value 的都是用 cstr
及 strlen(cstr)
,像 CSTR_INTERNING
、CSTR_PERMANENT
這種型態內 hash_size
無實質意義,就得用 strlen()
重新取得長度。
比對兩 cstring
是否相等,得依其順序進行比較
型態和 hash_size
之間的關係:
type | memory location | ref | hash_size |
---|---|---|---|
CSTR_ONSTACK |
stack | 0 | string length |
CSTR_INTERNING |
heap | 0 | hash_blob(cstr, strlen(cstr)) |
CSTR_PERMANENT |
heap | 0 | 0 |
0 (default) |
heap | >=1 | 0 |
array-to-pointer decay 或是 array decay 指的是陣列型態退化成指標,失去原先陣列所帶有的資訊,例如陣列大小。出現在以下情境:
關於第 1 點在 6.7.5.3 Function declarators 能找到相關敘述,或是參照 Daniel Chen 筆記。第 2 點常發生在 assignment,而 assignment 左式必須為 modifiable lvalue,ISO/IEC 1999:9899 - 6.3.2.1 這樣定義:
When an object is said to have a particular type, the type is specified by the lvalue used to designate the object. A modifiable lvalue is an lvalue that does not have array type, does not have an incomplete type, does not have a const-qualified type, and if it is a structure or union, does not have any member (including, recursively, any member or element of all contained aggregates or unions) with a const-qualified type.
這樣的限制,使下方程式無法編譯成功
隨後條目指出
Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.
因此 assignment 右方具有陣列型態的表達式將被轉型
關於 lvalue 的定義一般認知是 locator value,但規格書對 lvalue 的定義看起來很廣:
An lvalue is an expression with an object type or an incomplete type other than void; if an lvalue does not designate an object when it is evaluated, the behavior is undefined.
type
Objects, functions, and expressions have a property called type, which determines the interpretation of the binary value stored in an object or evaluated by the expression.
chriso/intern
比較為了讓測試進行下去,先套用 iLeafy11 提到的 __cstr_ctx.total
誤植問題
另一個問題是 void *ptr = (void *) (p + 1);
使用 gcc -Wpedantic -Wall -std=c99
編譯會出現警告。嚴格說,在 void*
上進行算術運算是非法操作,但 gcc 實作允許。
Reference
用法
仿造 chriso/intern 的 benchmark
量測插入 1
~5000000
共 5M 個字串到 interned table 平均所需要的額外開銷
strings_allocated_bytes()
量測 __cstr_interning
佔用空間
cstr
額外開銷足足有 chriso/intern 的兩倍,而 pool size 佔了大部份。為了減少開銷,在不採用 LRU 等手段減少 pool size 的情況下,能借鑑的只有 chriso/intern 本身的資料結構。chriso/intern
資料結構