contributed by < XDEv11
>
1
透過 __extension__
來明確表示要使用 GNU C extensions,並避免在某些情況下產生警告。
-pedantic and other options cause warnings for many GNU C extensions. You can prevent such warnings within one expression by writing extension before the expression. extension has no effect aside from this.
({})
有點類似於 ,
運算子,藉此達到類似於函式回傳值的效果。
The last thing in the compound statement should be an expression followed by a semicolon; the value of this subexpression serves as the value of the entire construct.
__typeof__()
可得到變數的型態。
If you are writing a header file that must work when included in ISO C programs, write
__typeof__
instead of typeof.
offsetof()
得到某個成員的位置偏移量。
doubly-linked list 的鏈結,納入新結構的成員中即可。
把 list
接到 node
的尾端。
把串列從 node
的地方切開,前半部分接到 head_to
,後半部分留在 head_from
。
假設自己定義的 struct S
,裡面有一個 list_head list
,可以透過 list_entry(node, S, list)
得到整個物件的指標 struct S *
。
運用上述程式碼實作 merge sort。
list_ele_t
中的 next
與 queue_t
中的 head
和 tail
都不需要。
list_ele_t
與 queue_t
中都有一個 struct list_head
即可形成 circular doubly-linked list。
這邊進一步改進,將 struct list_head
放在結構中的第一個成員,存取整個物件時,只需把 struct list_head *
轉型為 list_ele_t *
,而不需要使用 list_entry()
(container_of()
) 這個 macro,不只讓操作更便利以外,也節省運算指標位置的減法 (- offsetof(type, member))
)。
container_of
用到 offsetof
,後者是編譯時期的操作。在結構體中的第一個成員 struct list_head list;
需要特別標注其位置,也該考慮到 padding,例如 Typical alignment of C structs on x86 所及,第二個成員實際的記憶體位置可能會因 padding 調整
根據 C11 standard 6.7.2.1 Structure and union specifiers 中第 15 點,對於第一個成員的位置是有規範的。
Within a structure object, the non-bit-field members and the units in which bit-fields reside have addresses that increase in the order in which they are declared. A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.
程式碼精簡後,不難發現 queue_t
根本就是多餘的結構,應該一併調整。這是設計題目時,讓同學們得以進一步重構 (refactor) 的空間。
已經進行重構,把多餘的結構去除並改寫程式碼!
回傳值改為 list_head *
,會比較好進行串列的操作。需要時,也可透過 list_entry()
得到整個物件。
測試程式碼
這邊可以利用 list_add_tail
把他接到第一個元素 (q->next
) 的前面。
2
N |= N >> 1;
會讓第一個 1
的位元的下一個位元變成 1
,接著 N |= N >> 2;
會讓第一個 1
的位元開始的四個位元都變成 1
,以此類推。
最後得到一個第一個 1
的位元以下皆為 1
的數字,加上 1 後往左位移,即可得到第一個 1
的位元以下皆為 0
的數字。
程式碼最後一行應該改為 return (N >> 1) + 1;
,避免 msb 為 1
的數字會溢位。
特別注意的是 func(0)
會從 0
變為 1
,不過 小於或等於 0
的 power-of-2 本身就是未定義的,這邊就不管它了。
不過因為這邊型別是 16 位元的無號整數,利用 gdb 去看組合語言。
可以發現 return (N + 1) >> 1;
是用 32 位元的暫存器 (%eax
) 做運算,所以不會有整數溢位的問題。
如果把寫法改成,
就會得到,
可以發現程式碼會使用 16 位元的暫存器做運算,
或是改成 32 位元的無號整數版本,都會產生整數溢位的問題。
根據 C11 standard 6.3.1 Arithmetic operands 6.3.1.1 Boolean, characters, and integers 中第 2 點,
If an int can represent all values of the original type (as restricted by the width, for abit-field), the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions.
return (N + (uint16_t)1) >> 1;
在這邊一樣是用 int
做運算,並不會有整數溢位的問題。
3
要從某個位元開始存取資料時,當前的位元組往左位移 _read_lhs
與下一個位元組往右移 _read_rhs
後,進行 bitwise or 運算,即可得到一整個位元組大小的資料。
寫入時則剛好相反,整個位元組往右位移 _write_lhs
寫入當前的位元組,往左位移 _write_rhs
則寫入下一個位元組。
_*_rhs
的值剛好也是代表當前位元組佔了幾個位元,所以要判斷有沒有跨越位元組時,可以用 bitsize
與 _*_rhs
進行比較。
下圖為 _read_lhs
等於 3
時的示意圖。
_read / 8
即可得知從第幾個位元組開始操作,而 _read & 7
則是計算從此位元組的第幾個位元開始,(_read / 8
可用 _read >> 3
替代)。
_write
部分的操作與 _read
同理。
這邊做了一些小修改,
>> 3
替代 / 8
origin
這個變數,直接用 *dest
表示read_mask
與 write_mask
改名,使其更易理解。
left_mask[b]
代表位元 b 左邊為 1
right_mask[b]
代表位元 b 右邊及本身為 1
第 39 行開始,先把下一個位元組大小的資料放入 data
中,44 行再把多餘的位元利用 left_mask
去除。
下面用了一些遮罩,為了保存頭尾我們沒有要寫入的部分 ,取用 left_mask
以及 right_mask
的引索意義如下,
write_lhs
代表從當前位元組的第幾個位元開始write_lhs + bitsize
代表寫到當前位元組的第幾個位元bitsize - write_rhs
代表寫到下一位元組的第幾個位元 (上述說過,write_rhs
也代表在當前位元組佔了多少位元,所以 bitsize
扣除它後,剩下的就是在下一位元組。)如果 bitsize <= write_rhs
,代表寫入的部分不需要跨位元,54 行後,mask
示意圖如下,以 write_lhs
為 3
,bitsize
為 4
為例。
如果 bitsize > write_rhs
,mask
右側一定都為 0
,下圖以 write_lhs
為 3
,bitsize
為 7
為例。
51 行的 right_mask[bitsize - write_rhs]
則得到另一個 mask,
中間的部分,每次剛好複製 source
的一個位元組,可降低讀取時的運算量。
4
cstr.h
分別用 2 的冪次代表,可以透過 |
(bitwise or) 進行聯集形成集合。
這邊定義 cstr_buffer
為 __cstr_buffer[1]
,在大多時候,array 會退化 (decay) 成一個 pointer,
這邊的功能類似 __cstr_buffer*
,但在使用時,卻不用寫成
而是
,即可在 stack 上有一個 __cstr_buffer
的物件,而名稱可以當作指標使用。
可參考 Everything you need to know about pointers in C
把 var
中的 str
初始成一個字串放在 stack 上的 __cstr_data
。
初使化一個靜態的 cstring
,並透過 __sync_bool_compare_and_swap()
確保在多執行緒的環境下能夠正確執行。
do { } while(0)
可參考 multi-line macro: do/while(0) vs scope block;
cstr.c
進行 malloc
,失敗則直接中止程式。
將物件放入雜湊表。
將 si
中的雜湊表 (hash
) 擴大一倍。
嘗試進行字串駐留,若 si
本身為空,或是裡面的物件多於雜湊表大小的 80%
時,則失敗。
進行字串駐留,若失敗了,則拓展雜湊表,再進行一次。
真正的雜湊函數。
這邊 void *ptr = (void *) (p + 1);
應為 char *ptr = (char *)(p + sizeof(struct __cstr_data));
,在一個 struct __cstr_data
之後,才是配置給字串的空間。
如果字串長度小於 CSTR_INTERNING_SIZE
,就進行字串駐留,否則的話,直接配置一個新的 cstring
。
對字串進行記憶體釋放,這邊使用 sync_sub_and_fetch 來確保在多執行緒的環境下能夠正確運作。
如果 s
是放在 stack 上,hash_size
是它的長度,否則的話,確認是否計算過雜湊值。 (回傳值應改為 uint32_t
)
如果 a
與 b
都是被駐留的字串,那只有當指標相同時才會是一樣的字串;如果字串在 stack 上,可以先用字串長度 (hash_size
) 判斷一下,否則也可以先用雜湊值判斷一下。
這邊 char *ptr = (char *) (p + 1);
應為 char *ptr = (char *)(p + sizeof(struct __cstr_data));
,理由同上。
如果 a
與 b
的長度小於 CSTR_INTERNING_SIZE
,會進行字串駐留,否則的話就另外配置一個 cstring
。
先看看原本 sb
中的字串是不是放在 stack,是的話嘗試把 str
也放入,不行的話就呼叫 cstr_cat2
去進行記憶體配置並完成字串連接。
從 cstr.h
中可以看到,會得到 cstring
的有 cstr_grab()
、 cstr_clone()
與 cstr_cat()
,還有 CSTR_BUFFER
與 CSTR_LITERAL
二個巨集。
可以歸納出處理字串的行為,如果字串長度小於 CSTR_INTERNING_SIZE
的話,就會進行字串駐留, type
為 CSTR_INTERNING
,hash_size
中保存它的雜湊值,否則的話,就直接放在一個 cstring
中而已,type
為 0
,hash_size
也為 0
;比較特別的是,透過 CSTR_BUFFER
宣告的變數,其中的 cstring
一開始是指向放在 stack
上的記憶體,最多可以存放 CSTR_STACK_SIZE - 1
長度的字串,type
為 CSTR_ONSTACK
,hash_size
為字串長度。
另外,CSTR_LITERAL
中透過 cstr_clone()
得到的 cstring
,如果沒有進行字串駐留,type
則會變成 CSTR_PERMANENT
,ref
則為 0
。
程式碼中,會減少 ref
的只有 cstr_release()
,但是當它變為 0
後,又會直接釋放記憶體,另一邊 cstr_grab()
的地方則是在 ref
為 0
時會把 type
轉為 CSTR_PERMANENT
。
這邊對於 ref
這個成員以及 CSTR_PERMANENT
這樣的 cstring
,看起來並沒有太多應用,也不太完整,或許是為了未來開發作保留的。
linux2021