contributed by < hankluo6
>
選項不用保留,原題目已有。你該專注探討你的思路和延伸討論
已更改!
D1
(c)
2
D2
(d)
1
解釋上述程式碼運作原理;
分數可以透過拆解來計算,分成兩種 Case:
if (slots == 1 || orig == 0)
會成立。第二個 term 在算的其實就是分數精度,持續計算直到超出 floating point 的上限為止。程式會先判對奇偶數並將結果放置在 od
中,因不管奇偶數,都需將分子除 2,所以可以保證程式最後會終止。根據上面的推導,D1
與 D2
自然就知道了。
以編譯器最佳化的角度,推測上述程式碼是否可透過 tail call optimization 進行最佳化,搭配對應的實驗;
推測此程式第一個遞迴函式能透過 TCO 最佳化,而第二個不能,因程式最後不為 function call,而是 result
的加法動作。
透過 gcc -S -O3
分析編譯後的組合語言
前面 .LFB39
處理 if
的部份,會先判斷 od
是否為 1,並根據結果跳到 .L4
或 .L3
。
在 od
非 1 時會進入 .L4
,從程式碼中可以看到,因底下 if
不會進入,故此部份應可以用 tail call optimization 優化,在 assembly language 中能發現沒有使用到 call
指令,而是用 je .L4
來遞迴,符合我們的推測。
在 od
為 1 時進入 .L3
,此部份需要計算兩次 divop
的結果,且最後要相加回 result
,故此部份不能優話,在 assembly language 中也能看到呼叫兩次 call divop
,且最後需要呼叫 addsd
指令來做最後的相加。
比照 浮點數運算和定點數操作,改寫上述程式碼,提出效率更高的實作,同樣避免使用到 FPU 指令 (可用 gcc -S 觀察對應的組合語言輸出),並與使用到 FPU 的實作進行效能比較
假設我們只專注在效能而不是精度上,可以利用 Fast inverse square root 中的原理來計算除數倒數,在乘上被除數來完成除法。
程式碼來自 Fast 1/X division (reciprocal):
還有一點要注意的是,原先題目程式碼會有無窮迴圈的情況發生,原因是因為浮點數在判斷式上會有誤差,故 orig == 0
這個判斷式有可能永遠不會成立,可參考 Why doesn’t my floating-point comparison work?,將判斷的部份改為 orig <= 1e-10
來解決。
因為原程式有用到遞迴操作,時間與其他兩種方法差距相當大。
將其他兩個版本放大:
可以發現使用 FPU 的運算效率還是比沒有使用的好,且此實作版本的精確度相當低,故在有 FPU 架構的硬體上還是使用內建除法較好。
QQ0
(a)
0x01000000
QQ1
(d)
0x3f000000
QQ2
(g)
23
此部分處理 float 為 0 與 negative 的部分,當為 0 時,回傳 0,為負數時回傳 NaN。
當 exponent 部分全為 1 時為 IEEE 754 定義的特殊值,回傳原本數字。
接著先計算 exponent 部分,(ix0 >> 23)
取得 exponent 的值,如果為 0,表示為 subnormal number,
將數值 normalize,將 exponent 轉為真正的指數,並用 ix0
紀錄 normalize 後的有效數部分。
這邊有效數(亦稱尾數)指的是轉換為科學記號後 中 的部分,須滿足 ,與 IEEE 754 中 mantissa 的意義稍有不同。
因開根號為 次方,相當於指數部分除 2,故底下 m >> 1
用來對指數部分開根號,特別注意當指數次方為奇數時,會將指數的一次方移到有效數,讓指數形成偶數能整除。
故現在有兩種情形:
接著為計算 square root 的部分,詳細演算法可以參考 How to calculate a square root without a calculator 或 Wikipedia,利用每次選擇次方最接近目標數值的值,來漸漸逼近目標數值。演算法的證明可以從 Why the square root algorithm works 來理解。
寫成流程圖方便理解:
網路上有很多此直式除法開根號的例子,故這邊主要討論一般直式開根號方法與程式間的差異:
r
表示當前處理位置r
的位置就能形成位移除數的效果r
與暫存變數來方便運算ix0
與 r
從例子來觀察:
在程式中,只有當估測的值(目前商數)與除數相乘小於被除數時,才會將進行處裡位元乘 2 的步驟。
r
用來檢查被除數位置上的 bit,故必須要比被除數的位元數還要大。57 行將 ix0
設定為 24 bit,59 行與 63 行各自位移 1 bit。所以 ix0
最高可能為 26 bit 的數字,r
必須要比 ix0
大且只有一位元為 1,所以答案為 0x01000000
。
while
部分執行下列行為:
t
決定猜測的商數之目前的 bit 為 1 或 0
s0
),並將除數加上 2r
因在 63 行有額外 shift 一次 ix0
,所以我們做完迴圈後實際上小數點後有 24 bit 的數字,最後一個 bit 可以用來做 round 來讓結果更正確。
將 1 與極小的數字做運算來判斷當前系統對 float point 運算的規則,並依照此規則來做 round。
q >> 1
是因為我們多計算一個 bit 用來做 round,要把最後一個 bit 去掉,讓 mantissa 為 23 bit。
最後要把數字轉換為 IEEE 754 的格式,因要把 m 轉換為 excess-127 格式,並移到 float 格式中 exponent 的位置,所以 QQ1
與 QQ2
分別為 0x3f000000
與 23
。
解釋上述程式碼何以運作
試改寫為更有效率的實作,並與 FPU 版本進行效能和 precision / accuracy 的比較
練習 LeetCode 69. Sqrt(x),提交不需要 FPU 指令的實作
實作 Wikipedia 上開根號程式:
Runtime: 4 ms, faster than 67.45% of C online submissions for Sqrt(x).
Memory Usage: 5.8 MB, less than 46.78% of C online submissions for Sqrt(x).
ZZZ
(d)
1 - i
數學式:
對應到程式中,i
為式子中的 k
,x
為相減後的值,每次迴圈皆減掉下個數字 k
,所以答案為迴圈內應為 x -= (i - 1)
,故答案為 1 - i
。
嘗試改寫為更有效率的實作
參考 討論區 的程式,因為 可以拆成連續數字
可以寫成:
等式右邊可看出一定為一奇數 * 一偶數,且式子中 可為任意正整數,表示我們只要找到兩個數字能將 分解成一奇數一偶數,就能分解 成連續正整數。
因偶數乘以奇數必為偶數,所以我們可以從數字 的中任選一大於 2 的質因數,則剩餘的部分必為偶數 (因 為偶數),滿足上述分解的條件。
所以問題被簡化成:找到數字 的所有大於 2 的質因數的可能組合
實作程式如下:
質數用奇數來檢驗,因後處理的奇數會被前面的奇數處理,故不會有多蒜的情況,可以改為查質數表來加快效率。
一般情況質因數皆小於 ,但當 本身為質數時也須計算一種可能,故最後有個條件式判斷 是否被完全分解。
程式時間複雜度與題目程式一樣為 ,但在 leetcode 上此版本的運行時間為 0ms
,較題目的 4ms
還要有效率。
linux2020