contributed by < Jings1017
>
linux2021
先從參數看起 :
_dest
: 表示欲貼上的起始位址_write
: _dest 之偏移量_src
: 表示欲複製的起始位址_read
: _src 的偏移量_count
: 表示欲複製的位元數在程式中,策略是從 _src
中一次搬動一個 byte 到 _dest
中,若 bit offset 非 8 的倍數時,則需要做處理。
處理方式又分為讀和寫,讀的時候使用 read_lhs
及 read_rhs
,寫的時候則使用 write_lhs
及 write_rhs
。
以讀為例,read_lhs
表示左半不需讀取的 bit 數,而 read_rhs
表示右半不需讀取的 bit 數,寫的部分同理故省略之。
進入 while 迴圈時,先把 8-bit 的資料複製到 data
中,然後分兩種情形
read_lhs
位移為 0 : 直接把 source
的 8 bit 給 dataread_lhs
位移非 0 : 非從 source
第一個 bit 開始複製,以下圖表示假設 _read_lhs
為 3,即從第三個位元開始複製,則 read_lhs = 3; read_rhs = 5
。而目前 data
是從 source
第 0 位元開始,所以必須把 data
往左移動 3 格,如圖左下方所示。而下一個 source
(因為先前已做了 data=*source++
) 則往右移動 5 格,如圖右下方所示。最後再將處理好的左右兩部分做 |
運算,即可得到我們要的前 8 bit 。
如果目前還需再讀取的 bit 數小於 8 時,這時候就要透過 mask 來將多餘的部分設成 0
再來是處理寫到 dest
的部分
目前的 data
為經過讀取時處理後的資料,在此直接給 dest
。
而 mask
則用來保留左邊 bit 和清除右邊 bit ,因為若有位移的話,不能影響到左邊 bit
在此舉一實例做為參考:
假設 _write = 3
,則得到 write_lhs = 3; write_rhs = 5
。
如果該次需要寫入的位元超過 write_lhs
( 即 bitsize > write_rhs
), 代表接下來要貼上的 8 位元有跨單位 ,故需要做以下處理。
分述如下:
dest
中,其中不能汙染到前 3 bit,且需將 dest
往下移動一格dest
的右邊 5 個 bit 保留好存到 original
( 其中左邊 3 個清成 0 )original
往左移 5 bit,所以目前的前三 bit 就是上次 data 還沒複製到 dest 的資料。透過 | 把第二步驟留下的左三 bit 填滿示意圖如下 :
粉紅色為貼上 data 的部分
上圖之詳細過程如下 :
而第二種情況則不需考慮跨單位的處理
(original & mask)
讓左邊保持原本的狀態,右邊清成 0,接著透過 | 把右邊清掉的位元填上 data
。
最後再更新還需寫多少 bit 即可。
由於過多的 branch miss 會造成整體效率下降,所以可以減少程式中不必要的 if 條件式之使用,便可減少分支的產生,以利提升程式效能。
這段 bitcpy
讀取的過程中,因為最後都能夠用 read_mask
將 data
中不需要的部分剔除,因此在這之前其實不需要再去調整 data 要讀取的 range 有多大、以及有沒有跨越到下一個 byte,直接將完整一個 byte 的大小讀取出來,最後再用 read_mask
留下需要的部分即可,因此程式可以改成如下,將所有 if 條件式移除:
此外,程式中另外宣告的兩個 mask
,在使用時需要來回查看其內容,使用上有些許不便,額外宣告這兩個陣列也會增加記憶體 stack 空間的使用,應該有更好的實作方式,觀察後發現兩陣列的內容有規律性,其實透過 bitwise 的操作就可以直接完成了,取代方式如下:
但是實驗結果發現,以上取代方式反而會導致時間成本上的增加。
所以參考 bakudr18 同學的實做方式,將 mask
的部份改成
又在 / 8
的部份可以換成 >> 3
,以提升運算速度
再來,可以減少變數的宣告, orginal
其實可以直接用 *dest
取代,並不影響結果。
為了提升運算效能,可以將 if 換成 bitwise ,避免不必要的 branch miss
因此將 bitsize = (count > 8) ? 8 : count
換成 bitwise 運算
想法是利用 min(a,b)
之 bitwise 運算,其中 b
帶入 8
即 min(a,8)
,符合預期結果
並參考 先前一對一討論筆記 分析出 min(a,b)
已知 A xor B xor A = B
並考慮 a-b
是否為正,
針對 a-b
最左 bit 進行判斷,若為 1
即表示 a<b
,否則表示 a>b
寫成程式碼可表示成可用 (a-b)&(1<<31)
再將此結果右移 31 位到最右 bit,即 (((a-b) & (1<<31))>>63)
此時 結果為 1
or 0
加上負號以二補數表示即 111..111
or 000...000
再與 (a^b)
做 &
運算,
其結果為 (a^b)&(111...111) = (a^b)
or (a^b)&(000...000) = 0
最後再與 b
做 ^
運算,可得 b^(a^b) = a
or b^0=b
,
因此可判斷出 min(a,b)
operation | a < b | a > b |
---|---|---|
a-b | 1xx..xxx | 0xx..xxx |
(a-b)&(1<<31) | 100..000 | 000…000 |
((a-b) & (1<<31)) >>63 | 000..001 | 000..000 |
-(((a-b) & (1<<31))>>63) | 111..111 | 000..000 |
(a ^ b) & -(((a-b) & (1<<31))>>63) | a^b | 000..000 |
b ^ ((a ^ b) & -(((a-b) & (1<<31))>>63) | a | b |
可得以下結果
將上述整理之後,my_bitcpy
程式碼如下
main
程式碼如下
使用 perf
測量分支數量,參考自 Linux 效能分析工具: Perf
原始版本的測量結果如下
改寫後的版本測量如下
可從結果看出明顯減少許多分支指令
再使用 perf
測量 cache miss 等資訊
原先的版本
改良後的版本
做以上改善之後,利用 gnuplot 製圖觀察與原先程式碼在時間成本上的比較。
然而,原實做方法必須對 count > 8
的資料拆成三段進行處理,但在參考同學的共筆之後,發現可以使用一個迴圈及一個暫存器來處理這一個部份,最後再針對剩餘部份複製即可,這裡須注意剩餘部份是否橫跨兩個 byte ,處理方式如最後的 if 判斷,額外做複製。
利用 gnuplot 製圖結果如下
效能圖干擾因素之修正
因為測量時間短,用機率統計(CDF)進行後處理,排除干擾
另外 cache , timer(microsecond) 也有影響
可參考 Linux 效能分析的提示
改良後卻沒有顯著改善之解決方法
經過實驗證實,主要是將 mask 用 bitwise 運算代替造成的,若不改寫此部份,製圖後可看出其他改寫部份對時間成本的改善
目前已做以下更改,時間成本上有獲得改善,但仍未找出原方法造成時間成本上升之原因
bitmap_copy
及 bitmap_copy_clear_tail
採逐 bit 進行資料複製,但此兩函式皆不會保留 byte 內沒有複製到的 bit。