# 2018q3 Homework (quiz2) ###### tags: `bauuuu1021`,`BroLeaf` contributed by <`BroLeaf`,`bauuuu1021`> [2018q1 第 2 週測驗題](https://hackmd.io/s/SJO5LN9_M) --- ### 測驗 `一` ![](https://i.imgur.com/Mp0cHII.jpg) 請完成下方程式碼,依循 IEEE 754 單精度規範,輸出 $2^x$ 的浮點數表達法,而且考慮兩種狀況: * 當 $x$ 過小的時候,回傳 $0.0$ * 當 $x$ 過大的時候,回傳 $+\infty$ 注意:這裡假設 `u2f` 函式返回的浮點數值與其無號數輸入有著相同的位數,也就是至少 32-bit ```Clike #include <math.h> static inline float u2f(unsigned int x) { return *(float *) &x; } float exp2_fp(int x) { unsigned int exp /* exponent */, frac /* fraction */; /* too small */ if (x < 2 - pow(2, Y0) - 23) { exp = 0; frac = 0; /* denormalize */ } else if (x < Y1 - pow(2, Y2)) { exp = 0; frac = 1 << (unsigned) (x - (2 - pow(2, Y3) - Y4)); /* normalized */ } else if (x < pow(2, Y5)) { exp = pow(2, Y6) - 1 + x; frac = 0; /* too large */ } else { exp = Y7; frac = 0; } /* pack exp and frac into 32 bits */ return u2f((unsigned) exp << 23 | frac); } ``` 求 Y0 ~ Y7 使上面程式碼可以正常運作 #### 初步了解 搭配 CS:APP 3/e 80頁 `圖 2-35`來觀看,以單精度浮點數 32-bit 來看 |情況|signed bit|exponent|mantissa| |:-:|:-:|:-:|:-:| |無限大|X|11111111|0000....00| |NaN|X|11111111|XXXX....XX | |denormalized|X|00000000|XXXX....XX| |最小(負的)|1|00000000|0000....01| |$\wr$|1|00000000|1111....11| |-0.0|1|00000000|0000....00| |+0.0|0|00000000|0000....00| |$\wr$|0|00000000|0000....01| |最大(正的)|0|00000000|1111....11| |normalized|X|XXXXXXXX $\neq$ 0 , 255|XXXX....XX| |最小(負的)|1|11111110|1111....11| |最大(正的)|0|11111110|1111....11| 有了上面表格的基本認識,接下來就可以分析程式碼了 ```C /* too small */ if (x < 2 - pow(2, Y0) - 23) { exp = 0; frac = 0; ``` * too small $2^x$ 小到連 float 都不能表示出來,也就是比 float 最小的值還要小 $2^{-126} \times 2^{-23} = 2^{-149}$ 接著就可以得到 $2 - pow(2, Y0) - 23 = -149$ 移項一下 $pow(2,Y0) = 126$ 得到 $Y0 = 7$ ```C /* denormalize */ } else if (x < Y1 - pow(2, Y2)) { exp = 0; frac = 1 << (unsigned) (x - (2 - pow(2, Y3) - Y4)); ``` * denormalized 這裡是處理 non-normalized 的情況,exponent 的部份都是零,數值界於 $2^{-149}$ ~ $2^{-126}$ 經過上一個的 if 條件可以知道 $x > -149$ 所以 $Y1 - pow(2, Y2) = -126$ $pow(2, Y2) = 128$ $Y1 = 2,Y2 = 7$ * 接著來看 `frac = 1 << (unsigned) (x - (2 - pow(2, Y3) - Y4));` frac 就是 float 的 mantissa ,這裡可以直接帶入最小的 denormalize 值來判斷參數 Y3、Y4 x = -149, frac = 1 => (unsigned) (x - (2 - pow(2, Y3) - Y4)) = 0 $Y3 = 7,Y4 = 23$ ```C /* normalized */ } else if (x < pow(2, Y5)) { exp = pow(2, Y6) - 1 + x; frac = 0; ``` * normalized 這一段要看的是 normalized 的最大限制 exponent 的值是 $-126 ~ 127$ 簡單就能看出 $pow(2, Y5) = 128$ $Y5 = 7$ 在這裡要把扣過的偏移值加回來,偏移值可以從 exponent的範圍知道 也可以從 IEEE754 對 單精度浮點數的定義: $2^{k - 1}-1 = 127$ 所以 $pow(2, Y6) = 128$ $Y6 = 7$ ```C /* too large */ } else { exp = Y7; frac = 0; } ``` * too large 根據表格,無限大 exp 部份是 11111111 也就是 0xff $Y7 = 0 \times ff$ Reference: * [Exploration of the IEEE-754 Floating Point Standard](http://blog.ruofeidu.com/exploration-ieee-754-floating-point-standard/) [90] :::info 延伸題: 給定一個單精度的浮點數值,輸出較大且最接近的 $2^x$ 值,需要充分考慮到邊界 ::: >[color=blue]BroLeaf --- ### 測驗 `二` ```Clike typedef unsigned int float_bits; float_bits float_x0_5(float_bits f) { unsigned sig = f >> 31; unsigned rest = f & 0x7FFFFFFF; unsigned exp = f >> 23 & 0xFF; unsigned frac = f & 0x7FFFFF; /* NaN or INF */ if (exp == A0) return f; /* round to even. Last 2 bits of frac are considered: * 00 => 0 just >> 1 * 01 => 0 (round to even) just >> 1 * 10 => 1 just >> 1 * 11 => 1 + 1 (round to even) just >> 1 and plus 1 */ int addition = (frac & 0x3) == 0x3; /* Denormalized */ if (exp == 0) { frac >>= A1; frac += addition; /* Normalized to denormalized */ } else if (exp == 1) { rest >>= A2; rest += addition; exp = rest >> A3 & A4; frac = rest & 0x7FFFFF; /* Normalized */ } else { exp -= A5; } return sig << A6 | exp << 23 | frac; } ``` * 根據 IEEE754 對 float 的規範 * sign 為第 31 位 * exp 為第 23~30 位 * frac 為 0~22 位 * 將數值改寫為 $(-1)^{sign}*M*2^{exp}$,normalize 時 M 為 1+0.(frac),denormalize 時 M 為 0.(frac) * A0 = ? ``` /* NaN or INF */ if (exp == A0) return f; ``` 根據 IEEE754 定義,當 exp 為 0xFF 時為 NaN 或 INF;`A0 = 0xFF` * A1 = ? ``` /* Denormalized */ if (exp == 0) { frac >>= A1; frac += addition; } ``` f 為 Denormalized 時 exp 為 0,與數值大小有關的剩下 frac 。 因此將 frac 往右一位即可將數值 $*0.5$ ,`A1 = 1` * A2 = ? A3 = ? A4 = ? ``` /* Normalized to denormalized */ else if (exp == 1) { rest >>= A2; rest += addition; exp = rest >> A3 & A4; frac = rest & 0x7FFFFF; } ``` 因為 exp 只有 1,而 exp 直接與 frac 相連,因此 `rest >>= A2;` 可以直接視為 denormalized 的情況做運算。`A2=1` <br> 在 exp 為 1 的狀況下 *0.5 後一定是 denormalized ,也就是 exp = 0 。因此先將 rest 右移到使 exp 在最低位,再用一個 mask 讓 exp 只有 8 位。`A3=23 A4=0xFF` * A5 = ? ``` /* Normalized */ else { exp -= A5; } ``` f 為 Normalized 時透過 exp-1 即可將數值 $*0.5$,`A5 = 1` * A6 = ? ``` return sig << A6 | exp << 23 | frac; ``` 將數值的每個部分透過 `|` 連結起來, sig 在最高位元,`A6=31` Reference: * [Floating Point Multiplication](https://www.doc.ic.ac.uk/~eedwards/compsys/float/) [95] >[color=blue]bauuuu1021 --- ### 測驗 `三` 考慮到某些實數的二進位表示形式如同 $0.yyyyy...$ 這樣的無限循環小數,其中 $y$ 是個 $k$ 位的二進位序列,例如 $\frac{1}{3}$ 的二進位表示為 $0.01010101...$ (y = `01`),而 $\frac{1}{5}$ 的二進位表示為 $0.001100110011...$ (y = `0011`),考慮到以下 y 值,求出對應的十進位分數值。 * y = `010011` => $\frac{19}{X1}$ * y = `101` => $\frac{5}{X2}$ * y = `0110` => $\frac{2}{X3}$ X1 = ? X2 = ? X3 = ? :::success Reference: * [Binary Fractions](https://www.electronics-tutorials.ws/binary/binary-fractions.html) [83] ::: * 令循環小數的一組循環有 k 位,則 $0.yyyy...$ 可以想成 0.y, 0.y * $2^{-k}$, 0.y * $2^{-2k}$,.....無窮等比數列之和。其中首項為 0.y ,公比為 $2^{-k}$。 * 無窮等比和的公式為 $\frac{首項}{1-公比}$,帶入後答案為 $\frac{0.y}{1-2^{-k}}$。 * 第一小題 y = 010011,$0.y$ 為 $\frac{19}{64}$,k 為 6 ,帶入公式得 $\frac{19}{63}$ 。因此答案選`(c)63`。 * 第二小題 y = 101,$0.y$ 為 $\frac{5}{8}$,k 為 3 ,帶入公式得 $\frac{5}{7}$ 。因此答案選`(f)7`。 * 第三小題 y = 0110,$0.y$ 為 $\frac{3}{8}$,k 為 4 ,帶入公式得 $\frac{2}{5}$ 。因此答案選`(g)5`。 :::warning 接著你可以繼續思考:「如何一般化 (generalize) 呢?」 給定的分數 (即 N/M 形式,當 N 和 M 都是正整數),如何轉換成二進位,而且在表達 bit 數量限制的狀況,轉換的誤差是多少?(對比看 CS:APP 提及的愛國者導彈的誤差議題) :notes: jserv ::: * 一般化 * 先將分數寫為整數與小於 1 的分數 : int+frac * int 部份可以直接轉換成二進位 * frac 部份先乘以 2 ,判斷是否 >1 * \>1 : frac=(frac-1)*2,寫下 1 * <1 : frac=frac*2,寫下 0 * 重複此步驟直到完成或找出規律<br> * eg : 求 $\frac{12}{7}$ 之二進位表示 * $\frac{12}{7}=int+frac=1+\frac{5}{7}$ * 先將 frac * 2 再開始運算 * | Current | >1? | 小數點 | Next | | :-: | :-: | :-: | :-:| | $\frac{10}{7}$ | T | 1 | $(\frac{10}{7}-1)*2$ | | $\frac{6}{7}$ | F | 0 | $\frac{6}{7}*2$ | | $\frac{12}{7}$ | T | 1 | $(\frac{12}{7}-1)*2$ | | $\frac{10}{7}$ | T | 1 | $(\frac{10}{7}-1)*2$ | | $\frac{6}{7}$ | F | 0 | $\frac{6}{7}*2$ | | $\frac{12}{7}$ | T | 1 | $(\frac{12}{7}-1)*2$ | * binary = 1.[101][101][101]... * bit 數量限制 * 用上面的例子做延伸,觀察可發現 $\frac{12}{7}$ 是每 3 bits 為一循環,以下討論不同小數點位數下誤差大小 * $誤差=\frac{轉分數-實際值}{實際值}=\frac{轉分數-\frac{12}{7}}{\frac{12}{7}}$ | 限制位數 | 轉分數 | 誤差 | | :-: | :-: | :-: | | 3 | $1+\frac{5}{8}$ | 5.208% | | 6 | $1+\frac{45}{64}$ | 0.651% | | 9 | $1+\frac{365}{512}$ | 0.081% | | 12 | $1+\frac{2925}{4096}$ | 0.071% | | ... | ... | ... | | 24 | $1+\frac{11983725}{117440512}$ | 0.00000248% | * 24 bits 接近 float 可儲存的位數範圍,可發現對一般運算來說誤差已經不大。但如果是要應用在距離長的運算如火箭等,隨著距離增加誤差也會拉大 :::warning 從上表不難發現,這種數值轉換的誤差值較高,實務上如何縮減呢? :notes: jserv ::: --- ### 測驗 `四` 在 [你所不知道的C語言: 遞迴呼叫篇](https://hackmd.io/s/rJ8BOjGGl) 提到以下程式碼: ```Clike= #include <stdio.h> int p(int i, int N) { return (i < N && printf("%d\n", i) && !p(i + 1, N)) || printf("%d\n", i); } int main() { return p(1, 10) && printf("\n"); } ``` 預期會得到以下輸出: ``` 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 9 -> 8 -> 7 -> 6 -> 5 -> 4 -> 3 -> 2 -> 1 -> ``` * 開始前要先講解很重要的觀念,在 C 語言裡面,邏輯運算如下: `(A && B && C) || D` * 若 A 為 false 則會直接跳過 BC 去看 D 的結果,同理 A true 且 B 為 false 也會跳過 C ,若 ABC 為 true 則不會去看 D 的結果。 * 先觀察第一個程式,直接帶入 1, 10 trace 一遍就可以得到結果。 |次數|1|2|3|~|9|10|9|~|2|1| |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:| |i|1|2|3|~|9|10|9|~|2|1 |N|10|10|10|10|10|10|10|10|10|10 |print|1|2|3|~|9|10|9|~|2|1 * 要注意的是第 10 次在終止條件 i < N 這邊就是 False 了,所以根據上面提過的運算,會直接跑到第 4 行的 printf 印出 10,且回傳 true * 但是第 3 行呼叫 recusion call 有一個 not 所以實際上 `(i < N && printf("%d\n", i) && !p(i + 1, N))` 的結果一直都是 False ,所以遞迴回傳到前面時會跑到第 4 行的 printf ,再印一次當前的 i * 總結來說,**print 出來的前面 1 ~ 9 是呼叫第 3 行印的,後面的 10 ~ 1 是呼叫第 4 行印的**。 * 因為 printf 回傳印出的 character 數,其結果一直都是 true ,最後回傳 true 到 main 裡面,再執行最後一個 printf ,所以結果會再多一行。 注意 `10` 只會出現一次。請填補下方程式碼,使得輸出為 `1 -> 2 -> 3 -> 4 -> 5 ->`,並且包含換行符號 (`\n`) ```Clike #include <stdio.h> int p(int i, int N) { return (i < N && printf("%d -> ", i) && Q1(i + Q2, N + Q3)); } int main() { return p(1, 10) && printf("\n"); } ``` 提示: Q1 是函式名稱,可能包含 `~` (bitwise NOT) 操作,Q2 和 Q3 是有號整數 * 因為只要輸出一半的結果,等同於只呼叫一半次數的遞迴,用等差級數的概念去判斷,相較於上面的程式是相差 1 ,這裡呼叫的參數要相差 2 ,根據輸出結果是 12345 所以一邊一定會是 i + 1,另一邊則是 N - 1 得出,Q2 = 1, Q3 = -1。 * 最後要能執行到 main 裡面的 printf ,也就代表著 `return p(1, 10)` 一定要是 true |次數|1|2|3|4|5|6 |:-:|:-:|:-:|:-:|:-:|:-:|:-: |i|1|2|3|4|5|6 |N|10|9|8|7|6|5| |print|1|2|3|4|5| * 第 6 次遞迴呼叫 Q1(5 + 1, 6 - 1),很明顯結果會回傳 false ,所以我們在這邊使用 ~ (bitwise NOT) 強制把結果轉成 true ,所以最後回傳 main true ,就會執行後面的 printf 了。Q1 = ~p :::info 為了得到使回傳值回傳相反的結果而使用 ~ (bitwise NOT),我認為並不恰當。 根據本題回傳值有可能是 int ,例如:000111 (true),在使用 ~ 後,會變成111000,結果還是 true,只有在回傳 false 也就是 0 的時候用 ~ 才會得到相反的結果,有可能造成邏輯上的 bug 。 直接改用 ! Logical negation (NOT),可以得到相同 output ,也比較恰當。 ::: >[color=blue]BroLeaf > ### 參考資料 * [2018q1 Homework2 (作業區)](https://hackmd.io/s/BykAwRuKz) * [bauuuu1021](https://hackmd.io/s/Bkmr6V6tM) 共筆內容 * [raygoah](https://hackmd.io/s/HyCc94IKM#Week-2) 共筆內容 * [BroLeaf](https://hackmd.io/s/B1QbUVz9G) 共筆內容 * [Bitwise operators (NOT)](https://en.wikipedia.org/wiki/Bitwise_operation#NOT)