contributed by < tinyynoob
>
這題的目標是對兩數取平均,由於直覺的寫法會有 overflow 的問題,因此這裡試圖用 bitwise operation 來達成。
這裡的 >> 1
顯然是除以 2 的意思,那麼這個 EXP1
應該就是來解決兩個奇數分開運算造成的錯誤,例如:
要確認兩個都是奇數,我想到的最簡易方法是
EXP1
= a & b & 1
取平均的另外一種等價的寫法
根據你所不知道的 C 語言:數值系統篇裡的提點,我們了解到可以直接在 C 語言上應用數位邏輯設計,嘗試再次推導:
注意到不要把這個數學推導過程的左右移想成 C 語言內的左右移,否則會誤以為最後一個步驟不相等。
這裡的加法長得是如此簡單又沒有 ripple 的問題,使我納悶在邏輯設計時為何 carry lookahead adder 要用那麼複雜的方式,或許 shift register 影響此法的硬體可行性?
已解決。
前面推導的過程將 +
誤改成 ^
導致我想錯了
結論得
EXP2
= a & b
EXP3
= a ^ b
開啟編譯器最佳化 -O2
,對於上述兩個函式得到以下結果:
兩種 C 語言寫法在 assembly 層級會相差 3 行指令,主要的差異來自於第 5, 8, 10 行。
第一種算法 average1 意圖在 %eax
和 %edx
分別算出 a >> 1
和 b >> 1
,並在 %edi
算出 a & b & 1
,最後加總到返回值 %eax
。
可以想見若我們先做 a & b & 1
則可以不必用到 4 個 register ,也有機會減少 1 行指令,變成:
不過我認為編譯器原先的作法可以降低前後指令的相依關係,帶來更好的平行性。假如以指令的行數來表示,則可以有:
在多處理器的情況下有機會做 3 步就得出結果
第二種算法 average2 雖然生成的組合語言指令較少,但前後的相依性大,在多處理器的情況下可能需要執行更久:
不過如此短的計算應該還是會在單一處理器上完成,結論而言我認為仍是法二較佳。
不確定我這裡對 multicore 相關的認知是否正確
從定義
可以看出這個 macro 接收了 3 個參數,並且可以從註解中得知
precision
是定點數在小數點以下的精度weight_rcp
是加權平均的權重,令它為 則被規定須選為 power of 2 ,如此一來除法便可以化為簡單的右移運算。
我們主要需要解釋的是這一段程式碼:
令 weight_rcp = _weight_rcp
若 internal
非 0 ,則依據 Equation 更新 internal
值。若為 0 ,則可以直接省略加號右邊的部份。
WRITE_ONCE()
的功能是在多執行緒的情境下進行正確寫入,把第二個參數的值 assign 給第一個參數。
不知道哪裡可以找到 WRITE_ONCE()
的第一手資料,它不在 compiler.h 裡
我推測這裡是使用定點數運算,不過 >> precision
不是等同於把低位元丟棄嗎,這是如何把 unsigned long 變成定點數的呢?
moving average 的功能主要是讓資料變得更平滑,不因突發的波動而隨之起舞,其實它就是離散時間的低通濾波器。以這題而言,我們可以嘗試由 Z transform 計算 frequency response :
換成頻域得
對於頻率響應形式為 的離散濾波器,有:
因為 ,我們的 moving average 是一個低通濾波器。
studying…
該程式計算 gcd ,首先處理 u
, v
有 0 的情形。
第 6, 7 行基於 ,先將答案中 2 的成份 (以質因數分解的角度) 之 power 存到 shift
。
此時必有其一為奇數,接著要利用 的性質,先嘗試將 u
的大小盡量降低。
最後 11 到 21 行進行 Euclidean algorithm ,不停做相減直到出現 1 :
v
是每次相減後新出現的值,有可能又是偶數, 12, 13 行先確認能不能再用性質把值降下來。v
並將原先較小的數放到 u
。因此 COND
為 Euclidean algorithm 繼續的條件,即是 v
不等於 1
,考量在計算過程中 u
, v
可能變相同,例如: ,因此 v
的中止條件也可能為 0
,總結來說 COND
為 (v != 1) || (v != 0)
。
答案便是留下來的 u
值再補回早先存的 shift
,因此 RET
為 u << shift
。
拿去測試之後發現有錯,練習使用 gdb 除蟲:
u = 3
, v = 5
呼叫u |
v |
---|---|
3 | 2 |
1 | 2 |
1 | 0 |
判斷 while 條件有問題,將 COND
更正為 (v != 1) && (v != 0)
後得到理想結果。
__builtin_ctz
改寫並比較效能由於目前程式碼中存在大量判斷偶數與右移的操作,因此可以用 ctz()
來處理,從 gcc doc 找到說明:
Built-in Function: int __builtin_ctz (unsigned int x)
Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined.
improved 版實作程式碼:
為了進行實驗,我需要一些隨機的整數輸入 gcd()
, C 常用的 rand()
最大只到 32767 。考量兩種計算 gcd 的方法可能在大數 cases 有更顯著的差距,因此我參考了 stack overflow ,使用 C++ 來產生亂數,程式碼。
我打算分成兩組輸入數值範圍做比較:
這個測試方法好像怪怪的,因為隨機出來的 pair 大部分都會互質,但是我不確定什麼樣的測試方式更好
參考共筆在 user space 量測時間的方式,撰寫用來測量的程式碼。
輸入
進行量測
每組 100000 個數據進行測量,得到結果:
small numbers | big numbers | |
---|---|---|
original | 27944393 | 44117441 (ns) |
improved | 16084115 | 23787207 (ns) |
the ratio | 1.73739 | 1.85467 |
small number 組:
big number 組:
顯示出利用 __builtin_ctz()
優化後的版本比原版加快將近一倍,看來 shift operation 的運算量在原版程式中佔了很大的比例;而 big number 組的差距並不比 small number 組來得顯著,與我原先猜想的不同。
首先看 naive 版
因為看不出這到底在做什麼,直接丟數字進去 run :
得到
與輸入值 {0000 0011 0011 1111, 1111 1111 0010 0010}
(左邊為高位) 一起觀察,發現 out
算出了每一個 set bit 的 。而 gdb 第 3 行的 18 則是算出 set bit 的總數。理解程式功能後往下繼續看 improved 版:
由於要使用 ctz()
函式,因此每次計算完低位後就要把它設為 0 ,讓更高位可以繼續使用 ctz()
,以 00001010
舉例來說,首先 out[0]
會被設為 1 ,接著為了讓 out[1]
被設為 3 ,必須在算完 out[0]
之後將 bitset
改為 00001000
,因此 EXP6
應為 1u << __builtin_ctzll(bitset)
,如果這裡能把第 10 行往前顯然更好。
我用了四種 bitmap density 進行實驗, set bit 的個數分別是 8, 24, 40, 56 ,每一組都跑 100000 個數據。得到如下結果:
暫且用 Hamming weight 來稱呼 bitmap 中 set bit 的個數。
可以看到在 weight 8 的情境下, improved 比 naive 快了將近 10 倍,不過當 Hamming weight 變大時,兩條線就越來越靠近,此結果佐證了作業說明中的:
若 bitmap 越鬆散 (即 1 越少),於是 improved 的效益就更高。
可以見到當 weight = 56 時, improved 雖然仍比 naive 更好,但已經沒有顯著的改善了。
improved()
的效能隨著 Hamming weight 增大而隨之掉落符合我們的預期,值得注意的是在另一方面,我們可以發現 naive()
在 weight 56 的效能好於 weight 40 ,甚至好於 weight 8 ,目前不曉得該如何解釋這個現象
待
Let be constants.
Suppose that we want to do . we may consider
If we can precompute , then the division can be replaced by multiplication and bit-shift.
由於整數運算到小數點後就捨去,所以我嘗試用一些亂數跑跑看 accuracy 和 precision,整體看起來 precision 並不高,甚至在少部份的情況會出現低 accuracy 的結果:
接著我們嘗試分析 is_divisible()
函式
簡記 UINT64_MAX
為 MAX
因此,若 滿足
則有:
假設 的型態皆為 uint32_t
,該條件可被滿足。
直接列出全部四種情況:
(div3 && div5)
(div3)
(div5)
於是可以輕易推得:KK1
= div3
, KK2
= div5
, KK3
= 4 * div3