contributed by < OscarShiang
>
linux2020
首先 KK1
是在剛進入 bitcopy
函式的地方,根據 read_lhs
, read_rhs
與 source
位移的操作可以判斷 _read & 7
的目的在於計算 _read % 8
的數值,因為如果除數是 時,可以利用將變數與 進行 AND 運算取得餘數,所以根據 uint8_t *dest = _dest + (_write / 8);
可以得知
KK1
=7
根據 KK2
所在的位置可以知道,KK2
是用來表示當前迴圈所處理的 bit 的個數,所以根據迴圈中與 bit 計算有關的 bitsize = (_count > 8) ? 8 : _count;
就可以知道
KK2
=bitsize
首先我們可以先從函式的參數開始了解:
*_dest
: 目標的起始位址_write
: 寫入位置的位移量*_src
: 複製對象的起始位址_read
: 讀取位置的位移量_count
: 要複製的位元數量在函式之中,因為我們要複製的位元不一定是剛好與位元組對齊,所以需要考慮到我們要複製的位元可能會需要位移才能放進 data
中,所以我們需要計算 read_lhs
與 read_rhs
,並在執行複製的時候將位元利用位移並向左對齊,方便之後貼上到 dst
指標所指向位元中。
而在執行貼上的時候,因為 dst
原本可能也有自己的資料,所以我們需要透過,write_mask
將多餘的位元利用遮罩消除。在將 data
寫入 dst
時,也要考慮到位移的問題,所以利用 write_lhs
與 write_rhs
將 data
對齊 dst
寫入的 offset
,並將資料寫入,最後根據寫入的位元數,更新 _count
的數值,重複執行直到 _count
歸零就表示位元複製結束
根據原始的實作,每次的 while 迴圈都要重新判斷 read_lhs
, read_rhs
等等變數的數值,但是其中包含太多針對特定情況的處理。
在去除針對特定情況的 if-else
後,可以把程式碼簡化為下列形式:
bitsize = (_count > 8) ? 8 : _count;
這樣的敘述可換為 bitwise operation
已成功實作 bitwise operation 的版本
Oscar[color= orange]
使用 perf
測量分支數量
原始版本的測量結果如下所列
改寫的版本測量如下
從結果可以看到減少了許多的分支
接下來進行實驗測量簡化後的版本與原始版本的差異
實驗的結果如下圖(以排除干擾因素)
從實驗結果可以看出因為去除了多餘的判斷,所以在執行上可以減少許多為了判斷特定情況而花費的時間,在 bitcopy 的數量為 時可以減少大約 的時間
VV1
是位在 NEXT_POWER_OF_2
的 macro 之中,該 macro 在實作的就是計算大於給定 s 中最小的 2 的次方,如果符合 VV1
的條件,就直接回傳回去,如果不符合 VV1
的條件,則利用 __builtin_clzl
來計算
所以從程式碼中得知 VV1
是在判斷給定的 s 是否為 2 的次方,而其所使用的技巧就是如果 s 是一個 2 的次方的話,其在二進位的表示上就會是: 這種的形式,而 s - 1 就會是 ,表示將兩者進行 AND 運算時將會得到 0 的結果,所以在這題中:
VV1
=(s & (s - 1)) == 0
而 VV2
的判斷上,因為VV2
的位置位於 pop_back
的 macro 中。vector 的 size 為目前元素的個數,而 capacity
則是最大容量,所以根據 pop_back
要將元素刪除的需求,VV2
應該為
VV2
=v.size -= 1
在測驗 2 中,由於使用下列 macro 實作 vector 的宣告
所以在使用 v(type, size, name, ...)
時,編譯器就會根據輸入的參數展開 macro,達到自訂內部元素資料型態的功能
在 STRUCT_BODY
的宣告中
採用了類似 folly::string
的實作方式,所以可以達到在小容量時使用 stack 儲存資料,大容量時使用 heap 儲存資料的功能。
而較為特別的是因為 v
本身可以傳入多個參數,所以使用 __VA_ARGS__
來取得在 name
之後的參數,並將他們存入 buf
之中
__attribute_((cleanup(vec_free)))
的用法則是設定該變數在程式結束時會呼叫綁定的函式 vec_free
釋放記憶體空間
在原始的實作中, vec_pop_back
就是將 vector 的 size
直接減一,讓其不會在 display
的時候被印出,但是這樣的實作因為沒有檢查邊界,有可能會導致記憶體 BAD_ACCESS
的問題產生,所以在實作中加入檢查邊界的判斷
根據 assert
在 Linux Programmer's Manual
中的定義
The assert() macro tests the given expression and if it is false, the calling process is terminated. A diagnostic message is written to stderr and the abort(3) function is called, effectively terminating the program.
我們可以使用定義於 assert.h
的 assert
進行檢查,在使用 vec_pop_back
會超出邊界時,呼叫 abort()
停止程式
因為在 vec_pos
的實作如下列
因為其並未實作出檢查邊界,所以在 n
超出 v.size
時會存取到未使用的元素空間或是 n < 0
時會產生分預期的行為。
使用函式將取值與 assert
包裝起來:
參考 __vec_push_back
與 __vec_reserve
的實作,將 __vec_pos
的回傳值定義為 void *
,並使用 macro 將回傳回來的位址轉型後取值
測試超出邊界的呼叫
完成實作邊界的檢查
因為在 vector 中不能夠存入像是 void
或是 void *
的資料型態,所以我們可以利用 _Static_assert
來觸發編譯時期的錯誤
在 macro 中加入 _Static_assert
,接著定義 SAFE_TYPE
的判斷
原本想要使用 _Generic
來實作,但是發現 _Generic
不能接受型態作為參數使用,所以改以 macro 字串化的特性來實作 SAFE_TYPE
宣告一個元素型態為 void
的 vector 來進行測試
確認其在編譯時期會導致錯誤
因為在實作 vec_insert
的時候會需要調整與移動元素的位置,所以我使用 string.h
的 memmove
來進行實作,因為根據 Linux Programmer's Manual
的描述
The memmove() function copies len bytes from string src to string dst. The two strings may overlap; the copy is always done in a non-destructive manner.
但是如果使用 memcpy
而 dst
與 src
如果重疊,則是 undefined behavior ,所以利用 memmove
來避免未定義行為的發生
因為在使用 vec_insert
的時候有可能會導致容量不足,所以需要依照情況調整容量,之後將給定 pos
之後的所有元素向後位移一個單位,最後將元素複製到指定位置上
接著利用 macro 實作介面:
在 vec_erase
的實作上也與 vec_insert
類似,但是因為不用考慮超過容量的問題,所以只需要將給定位置之後的元素向前位移即可
建立 vec_erase
的介面
接下來針對 push_back
, insert
與 erase
分析 C++ STL vector 與我們實作版本的花費時間差異
在 push_back
中,因為 C++ 版本的 std::vector 有使用記錄 tail 的指標 vector::end()
,所以可以在常數時間完成。而我們的版本則因為有記錄 vector 長度,所以也可以實作出常數時間甚至比 vector::push_back
(from C++ STL) 還高效率的 函式
圖中的高峰是因為兩者 push_back
在根據 vector 的容量在調整
兩個版本的 insert
的實作差異如下
可以看到因為需要將資料向後位移,元素的大小與花費時間呈正相關
從結果可以看到,因為越接近 tail 在 erase
的時候需要搬運的元素也較少,所以花費時間與 erase
的位置呈負相關
而從上述的情況都可以看出 C 實作出來的 vec 在 push_back
, insert
與 erase
執行的效率上都較 C++ STL 的 vector 好