2018q1 Homework2 (assessment) === contributed by <`raygoah`> # 課前測驗參考解答 ## Week 1 ### 題目: 考慮以下整數乘法的實作程式碼: ```clike int mul(int n, int m) { int ret = 0; for (int c = 0; m; c++, m /= 2) if (!(m % 2 - 1)) OP return ret; } ``` ### 想法及思考 1. 一開始看到題目,可以知道此題是為了要做乘法 2. 看到在 `for` 迴圈中,每次都將 m / 2 ,同時將 c 的值加一,可以聯想到 `n << 1` 代表把 n 乘 2 乘一次,因此若寫成 `n << c` ,及代表要將 n 乘 2 乘 c 次 3. 而觀察到 `for` 迴圈中的 `if` ,猜測本題重點便是在於為什麼達成 `if` 的條件後要進行 OP ,這背後代表的意思,就是完成這題乘法的關鍵 ### 解題思緒 * 從 `if (!(m % 2 - 1)) OP` 中可以知道,只有在 m 為奇數時,OP 才會執行,而這代表著只有在 m 一開始就是奇數,第一次進迴圈的時候 OP 會被執行,此外還有在跳出迴圈前一次,m = 1 時,OP 會被執行 * 而從以上幾點來看,迴圈中每次將 m 除以 2,並將 c 加一的目的,便是要將 m 改寫成 $2^c$,但這樣會因為 m 是奇數時而無法順利表示,因此若要在 m 是奇數時表示成 $2^c+1$ 的話,便需要利用 m 是奇數時第一次進入 `for` 迴圈中,`if` 的條件式會成立,OP 會執行的這一點,即可以順利的將 m 表示成 $2^c+1$ * 而在了解了 m 是如何表示後,便可以了解此題想要做的,應該是要把 $n * m$ 改寫成以下兩種狀況 * m 為奇數時: $(n * 2^c) + (n * 1)$ * m 為偶數時: $n * 2^c$ * 而這時便可以利用 `<<` 的特性,每向左移一位元代表乘以二,以及結合上述的想法,便可以選出正確答案:OP 為 `(e)` ret += n << c; ### 延伸問題 #### 解釋為何乘法可運作? - 如我在上方所提到,此題將乘數 m 改寫,依照 m 為奇或偶分別寫成 $2^c + 1$ 以及 $2^c$,因此整個乘法便會改寫成如下所示: * m 為奇數時: $(n * 2^c) + (n * 1)$ * m 為偶數時: $n * 2^c$ - 在將$n * m$ 改寫後,便可以利用 `<<`,寫成如題目的程式碼,順利的完成可運作的乘法程式 ### 參考 * `rwe0214` 在課堂上的講解 ## Week 2 ### 題目: ```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; } ``` ### 想法及思考 * 看完題目可以知道,此題的 function 想要做的,是將以 IEEE 754 表示的 32 位元浮點數乘與 0.5 * 要先搞清楚 IEEE 754 的規則,當初就是沒有搞清楚,結果這幾題都錯很慘,念資工連這都不會,慚愧... * 主要要測驗的,是其中註解為 Denormalized 以及 Normalized 的部份,熟悉規則便可解決 * fraction 為 0 ~ 22 bits,exp 為 23 ~30 bits,sign bit 為第 31 bit ### 解題思緒 * 照著題目順序從 A0 看到 A6,雖然每一題的選項都很多,但只要知道 IEEE 754 的規則,要選出正確答案不難 * A0: ```clike /* NaN or INF */ if (exp == A0) return f; ``` * 可以看到註解清楚的告訴我們,這邊是要處理關於遇到 NaN 以及 INF 時該做什麼 * 這裡是決定當遇到這兩種情況時,就不用再乘 0.5,直接回傳原本的數字 * 因此考慮到 IEEE 754 對於 NaN 以及 INF 的定義,這兩種情況的 exp 值會是全為 1 ,因此要選擇一個全為 1 的答案,也就是 0xFF * A1: ```clike /* Denormalized */ if (exp == 0) { frac >>= A1; frac += addition; } ``` * A1 關係到的部份,是在遇到非規格化的值時,該做些什麼處理 * 這部份也是滿直觀的,因為非規格化的值,exp 為 0 ,因此在這裡不需要做什麼,只需要將 fraction 的部份乘上 0.5,也就是右移一次即可,所以 A1 選 1 * 而在 `frac += addition;` 的部份是用於做捨入的,採用 round to even 的方法,捨入的方法在 code 中的註解有詳細的介紹 * A2 A3 A4: ```clike /* Normalized to denormalized */ else if (exp == 1) { rest >>= A2; rest += addition; exp = rest >> A3 & A4; frac = rest & 0x7FFFFF; } ``` * 在這個 else if 中,需要填的是 A2 A3 以及 A4 * 而同樣的註解有說明,這部份所要做的是將規格化的數字在乘上 0.5 後,成為非規格化的數字時,所需要做的處理 * 這邊利用了在前面先把 sign bit 拿掉後得到的 rest,把 rest 先乘上 0.5,也就是右移一次,所以這裡 A2 和 A1 一樣,也是選 1 * 接著在 `rest += addition;` 的部份,同樣的也是做捨入 * 而在 `exp = rest >> A3 & A4;` 這裡,要從 rest 中把第 23 到第 31 bit 的數字取出,也就是只要留下 exp 的部份,因此需要先向右移位 23 次,再和 0x7FFFFFFF 做一次遮罩,便可以得到,因此 A3 為 23,而 A4 則是選擇 0x7FFFFFFF :::info 這邊不理解的一點是,對於 exp 為何不直接將其設為 0 因為在乘完 0.5 後,得到的答案會是非規格化的數字,而在 IEEE 754 中,只要是非規格化的數字,exp 都會是 0,那麼為何要選擇用 bitwise 的方式而非將 exp 直接設為 0 呢? ::: :::warning 有猜測和質疑很好,那去寫程式驗證吧! :notes: jserv ::: * A5 ```clike /* Normalized */ else { exp -= A5; } ``` * 在 A5 的部份,是關於規格化的數字乘上 0.5 後,還是規格化的數字時,所要做的處理 * 因為我們要做的是要將整個數乘上 0.5,也就是表示成 $(-1)^s * M * 2^E$ 時,E 的值必須要減一,因此我們將 exp 減 1,即可解決,A5 的答案為 1 * A6 ```clike return sig << A6 | exp << 23 | frac; ``` * 在最後要將上面得到的 sig, exp 以及 frac 合成一個資料型態為 unsigned int 的數字回傳,因此依照 IEEE 754 規定,sign bit 為第 32 個 bit,因此將 sig 向左移位 31 次,A6 選澤 31 ### 參考 * CS:APP 3/e ## Week 3 ### 題目: 考慮到下方函式 `shift_right_arith` 和 `shift_right_logical` 分別表示算術右移和邏輯右移,請嘗試補完程式碼。可由 `sizeof(int) * 8` 得知整數型態 `int` 的位元數 `w`,而移位量 `k` 的有效範圍在 0 到 `w - 1`。 ```clike #include <stdio.h> int shift_right_arith(int x, int k) { int xsrl = (unsigned) x >> k; int w = sizeof(int) << P1; int mask = (int) -1 << (P2); if (x < 0) return xsrl P3 mask; return xsrl; } unsigned shift_right_logical(unsigned x, int k) { unsigned xsra = (int) x >> k; int w = sizeof(int) << P4; int mask = (int) -1 << (P5); return xsra P6 P7; } ``` ### 想法及思考 * 先了解算術右移以及邏輯右移兩者的不同 * 題目有兩點提示: 1. 可由 `sizeof(int) * 8` 得知整數型態 `int` 的位元數 `w` 2. 移位量 `k` 的有效範圍在 0 到 `w - 1` ### 解題思緒 * 首先先由算術右移的部份下手: * 算術右移的特點在於,右移完後會依照 sign bit 是多少,把空缺的 bits 補上,讓右移完成後的結果和原數正負號相同 ```clike int shift_right_arith(int x, int k) { int xsrl = (unsigned) x >> k; int w = sizeof(int) << P1; int mask = (int) -1 << (P2); if (x < 0) return xsrl P3 mask; return xsrl; } ``` * `int xsrl = (unsigned) x >> k;` 首先將 x 右移 K 位 * `int w = sizeof(int) << P1;` 這邊可由提示中提到,由 `sizeof(int) * 8` 得知整數型態 `int` 的位元數 `w`,因此要乘上 8 就等同於向左移位 3 次,因此 P1 為 3 * `int mask = (int) -1 << (P2);` 在這裡 mask 為了要讓原本是小於 0 的數,在經過算術右移後得到的結果仍舊是負數,因此之後要對從右邊數來 k 個 bits 做遮罩,所以在這邊要將 -1 向左移 (w-k) 次,因此 P2 選擇 `w-k` * 在有關 P3 的部份,接續了 P2,也就是為了要讓原本是小於 0 的數,在經過算術右移後得到的結果仍舊是負數,因此要把 xsl 和 mask 的每個 bit 做邏輯運算的 'or' ,把右邊數來 k 個 bits 全部設為 1 ,因此在這邊 P3 要選 `|` * 接著是邏輯右移的部份 * 邏輯右移與算術右移不同,在右移後,不管原本數字是正或是負,左邊補上的都是 0 ,因此可能會有原本是負數但經過算術右移後得到正數的結果 ```clike unsigned shift_right_logical(unsigned x, int k) { unsigned xsra = (int) x >> k; int w = sizeof(int) << P4; int mask = (int) -1 << (P5); return xsra P6 P7; } ``` * P4 的地方和 P1 一樣,`sizeof(int) * 8` 得知整數型態 `int` 的位元數 `w`,因此 P4 為 3 * P5 也是和 P2 的概念一樣,只是之後遮罩的作法不同,但這邊也是為了讓 mask 從右數來 k 個 bits 皆為 1 其他位元皆為 0,因此在這裡 P5 也是選擇 w-k * P6 P7 的部份是邏輯右移與算術右移最重要的區別,在邏輯右移中要讓右移後從左邊補上的 bits 全部為 0 ,因此要先將 mask 做一次邏輯運算的 `not` ,讓 mask 從原本的右邊數來 k 個 bits 皆為 1 其他位元皆為 0 ,變成從右數來 k 個 bits 皆為 0 其他位元皆為 1,接著和 xsrl 做 & ,確認右邊數來 k 個 bits 全部為 0 ,而左邊數來 k 個 bits 留下移位後要的數字,得到最後的結果,因此 P6 為 `&` 而 P7 為 `~mask` ## 延伸題目 * 題目:在 x86_64 和 Aarch32/Aarch64 找到對應的指令,並說明其作用和限制 * x86_64: * 邏輯移位:shr 以及 shl * 用法: `shr src, dest ` 將 dest 向左移位 src 個 bits * 範例: ``` movw $ff00,%ax # ax=1111.1111.0000.0000 (0xff00, unsigned 65280, signed -256) shrw $3,%ax # ax=0001.1111.1110.0000 (0x1fe0, signed and unsigned 8160) # (logical shifting unsigned numbers right by 3 # is like integer division by 8) shlw $1,%ax # ax=0011.1111.1100.0000 (0x3fc0, signed and unsigned 16320) # (logical shifting unsigned numbers left by 1 # is like multiplication by 2) ``` * shr 為向右移位,shl 為向左移位,兩者皆是將超過範圍的 bits 移除,並將空缺的 bits 補上 0 * 算數移位:sar 以及 sal * 用法: `sar src, dest ` 將 dest 向右移位 src 個 bits,並依照原本的 sign bit 是多少,把空缺位都補上一樣的 0 或 1 * 範例: ``` movw $ff00,%ax # ax=1111.1111.0000.0000 (0xff00, unsigned 65280, signed -256) salw $2,%ax # ax=1111.1100.0000.0000 (0xfc00, unsigned 64512, signed -1024) # (arithmetic shifting left by 2 is like multiplication by 4 for # negative numbers, but has an impact on positives with most # significant bit set (i.e. set bits shifted out)) sarw $5,%ax # ax=1111.1111.1110.0000 (0xffe0, unsigned 65504, signed -32) # (arithmetic shifting right by 5 is like integer division by 32 # for negative numbers) ``` * Aarch32/Aarch64: * 邏輯移位: * 向右移位:LSR * 向左移位:LSL * 算術移位: * 向右移位:ASR * 向左移位:ASL ## 參考 * [邏輯左移、邏輯右移、算術左移、算術右移、循環左移、循環右移](http://blog.csdn.net/u011070169/article/details/53894154) * [X86 Assembly/Shift and Rotate](https://en.wikibooks.org/wiki/X86_Assembly/Shift_and_Rotate) * [ARM-Assembly: Arithmetic Shift / Logical Shift](https://stackoverflow.com/questions/14565444/arm-assembly-arithmetic-shift-logical-shift) # 因為自動飲料機而延畢的那一年 - [讀後心得及啟發](https://hackmd.io/YXnTlwD1Ty2rHv4WcZDHzw)