# 2018q1 Homework3 (assessment) contributed by <`PunchShadow`> ## 測驗題重作 ### 第三週第一題 [題目](https://hackmd.io/s/SknkEfVFf):主要程式目的為進行$\sqrt{2}$ 的 double(兩個32 bits) float point 運算 #### 程式理解及解題想法 ```clike= typedef union { double value; struct { uint32_t lsw; uint32_t msw; } parts; } ieee_double_shape_type; ``` 首先將一個 double floating point 之值存於 value 中,並將其 IEEE754 表現法拆成 32 bits 的兩部份(lsw 和 msw),並採用 Little Endian 的方式 > ![](https://i.imgur.com/GW0q1Wx.png) > msw 和 lsw 分別代表 Most Significiant Word 和 Least Significiant Word > 以此圖為例,若資料為 0x0A0B0C0DFFFFFFFF 則若採 Little Endian 方式儲存,則 0A 會存於 memory address 最小的地方,而其 msw 和 lsw 則分別是 0x0A0B0C0D 和 0xFFFFFFFF ![](https://i.imgur.com/beqLogO.gif) 由上圖可以看出 lsw 為 double-precision 的前半段(相當於single-precision 的長度),其中包含了 sign bit、exponential 和一部分的 fraction,而 msw 則全部都是 fraction 的部份。 ```clike= /* Set a double from two 32 bit ints. */ #define INSERT_WORDS(d, ix0, ix1) \ do { \ ieee_double_shape_type iw_u = { \ .parts.msw = ix0, \ .parts.lsw = ix1, \ }; \ (d) = iw_u.value; \ } while (0) ``` 此段程式碼是定義從32 bit ints 轉成 double-precision floating point 的方式,延續上面的 type define,將 .parts.msw 和 .parts.lsw 為 [designated initializor](https://gcc.gnu.org/onlinedocs/gcc/Designated-Inits.html) 宣告在 structure type iw_u 中 msw 和 lsw 的值為 ix0 和 ix1(==這兩個搞了超久才知道原來是一個 int32_t(ix0)和 uint32_t(ix1) 變數而已:sob: 原本還以為是16進位的某個表示法勒==),並將 iw_u.value assign 到 d 中, :::info 不知道是否可以將這個 function 看成 **輸入 ix0 和 ix1 兩個 int32_t 得到 一個 double floating point d ?** 用 **define 和 function 有甚麼差別嗎** ? ::: 這邊為了瞭解 Macro 的使用及為何這個 function 可以將兩個 int32 轉換成 double ,因此找到了這個程式再 Android 內的[原始碼](https://chromium.googlesource.com/android_tools/+/af1c5a4cd6329ccdcf8c2bc93d9eea02f9d74869/ndk/sources/android/support/src/msun/math_private.h),看到很多 Macro 的形式都長得像這樣,讓我有了一個想法 :::info 有沒有辦法利用兩個連續記憶體位置的 int32 去表示一個 double-precision floating point ? 於是就有了以下的實驗 ::: #### 2個連續 int32 轉成 double-precision floating point ```clike= #include <stdio.h> #include <stdint.h> /* 使用 sqrt function 內 union 的 data structure */ typedef union { double value; struct { /* CPU 為 i5-4200h 為 Little Endian 的儲存方式 */ /* 宣告的 lsw 記憶體位置就會小於 msw */ uint32_t lsw; uint32_t msw; } parts; } ieee_double_shape_type; int main(){ ieee_double_shape_type test; /* 0x4034000000000000 = 20.0 */ test.parts.lsw = 0x00000000; test.parts.msw = 0x40340000; /* Want to read the continuous address * of lsw and msw in double floating type. */ double *d = &test.parts.lsw; printf("lsw's address : %d\n", &test.parts.lsw); printf("msw's address : %d\n", &test.parts.msw); printf("d is %lf\n", d); return 0; } ``` 跟預想的一樣,lsw 的記憶體位置在 msw 前面 4 個 bit 但 d 卻沒有像預想中一樣印出,看來想用 pointer 存取兩個連續的 int32 是行不通的 ``` lsw's address : 343809464 msw's address : 343809468 d is 0.000000 ``` 後來在 Stackoverflow 上面看到有關 [bit sequential combination](https://stackoverflow.com/questions/11824762/convert-two-32-bit-floats-to-one-64-bit-number-and-vice-versa) 的問題,於是就將 d 直接改用 union 內宣告的 double value ```clike= double d = test.value ``` 想不到真的可以成功,而且 `test.value` 的初始 memory address 竟然是跟 test.parts.lsw 一樣,這讓我有點驚訝,於是就去求助 C99 規格書啦! >(6.7.2.1-15) Within a structure object, the non-bit-field members and the units in which bit-fields reside have addresses that increase in the order in which they are declared. A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning. 這裡很明確的指出會有一個 pointer 指向 object 的起始,且後來宣告的會 padding 在 object 的後方,不過還是覺得奇怪,為什麼 double 不會在一開始就 allocate 了呢? 後來查了一下,發現原來問題出在 **Union** 上面阿,原來 Union 和 Struct 雖然功能很像,不過差別就差在 **Memony Allocation** 上面阿, Union 一次只准一個 member access ,若不同 member 同時 access,則會造成 **data garbage** 的現象,請參考 [C Programming Unions](https://www.programiz.com/c-programming/c-unions) 這樣就很蠻合理的,由於一次只能 access 一個 member,所以 address 會一樣,原本應該要被視為 garbage 的兩個 int32 ,卻可以當成轉換成 double-precision floating point 的兩部份,真是高招! --- 拉回來~拉回來~ ```clike= #define EXTRACT_WORDS(ix0, ix1, d) ``` 這個 function 則是與 INSERT_WORDS 相反,將一個 double floating point `d` 轉成兩個 int32_t `ix0` 和 `ix1` ```clike= static const double one = 1.0, tiny = 1.0e-300; double ieee754_sqrt(double x) { double z; int32_t sign = 0x80000000; uint32_t r, t1, s1, ix1, q1; int32_t ix0, s0, q, m, t, i; EXTRACT_WORDS(ix0, ix1, x); /* take care of INF and NaN */ if ((ix0 & KK1) == KK2) { /* sqrt(NaN) = NaN, sqrt(+INF) = +INF, sqrt(-INF) = sNaN */ return x * x + x; } ....... ``` * x : 想進行平方根的數 * z : 平方根後的結果 * sign : 0x80000000 寫成二進位的話即是取出 32 bits 中的第一位 * **ix0 : 代表 x 的 Most Significant Word 部份** * **ix1 : 代表 x 的 Least Significant Word 部份** * if ((ix0 & ==KK1==) == ==KK2==) : 這邊是要判斷 INF 和 NaN 的部份,首先我們先從 INF 和 NaN 的定義下手 ![](https://i.imgur.com/PDpAQ9G.png) 上圖為 single-precision 在 IEEE 754 中定義的 INF 和 NaN 皆是 exponent 部分全為一,而 double-precision 亦是如此,因此要 找出部分(1~11個 bits)需要 ix0 和 ==0x7ff00000 (KK1)== 做 AND,且得出的結果應該也是 ==0x7ff00000 (KK2)== * return x * x + x : sqrt(NaN) = NaN, sqrt(+INF) = +INF, sqrt(-INF) = sNaN 這邊還蠻好玩的,先稍微簡介一下在 IEEE 754 中所定義的 +-INF 和 NaN * INF : * 數學運算上出現無限時 e.g : 1/0 = INF * 當運算大於所定義的最大值時(single-precision : $\ {3.4*10^{34}}$ , double-precision : $\ 1.797 * 10^{304}$) e.g : $\ 10^{1000}$ = INF * 有正負 INF * NaN : * 未定義的數學運算 e.g : 0/0 = NaN * 分為 `quiet NaN` 和 `signal NaN` : 前者會在程式執行完成後再進行檢查,後者則會產生 exception 需要進入 exception handle 進行例外排除 * INF 和 NaN 運算 : * INF + 10 → INF * INF/0 → INF * 10 - INF → -INF * 100/0 = INF → True * ==INF - INF → NaN== * INF/INF → NaN * ==INF * -INF = -INF== * ==INF - INF → NaN== * 各種例外情況 : * x 為 NaN : NaN * NaN = NaN, NaN + NaN = NaN * x 為 +INF : (+INF) * (+INF) = (+INF), (+INF) + (+INF) = (+INF) * x 為 -INF : (-INF) * (-INF) = (+INF), (+INF) + (-INF) = NaN (引發 exception) * **所以 x * x + x 就可以用上面黃底的運算得到所定義的內容,真是太強大惹** :+1: :+1: >參考 : [INF, NAN, and NULL - Exception values](http://wiki.analytica.com/index.php?title=INF,_NAN,_and_NULL_-_Exception_values) ```clike= /* take care of zero */ if (ix0 <= 0) { if (((ix0 & (~sign)) | ix1) == 0) return x; /* sqrt(+-0) = +-0 */ if (ix0 < 0) return (x - x) / (x - x); /* sqrt(-ve) = sNaN */ } ``` 這邊是要處理零的情況 (不是靈的轉移 :laughing: ) 想要是 0,exponent 的部分就必須要是 0 才可以,一開始有點疑惑, ix0 是 int32_t 的形式,所以若 sign bit 是 1 的話不論 exponent 部分為多少,條件皆會成立,進入迴圈判斷後才決定是 0 或是負數,雖然邏輯上面是沒有錯,不過這樣是不是有點多此一舉? :::info 加上 **if (ix0 <= 0)** 的目的是希望能減少 branch 嗎? 不然在程式邏輯上,這條件和下面兩個條件好像有點重疊 ::: :::danger 程式碼是給你修改實驗用,不是拿來背誦,有想法很好,就動手改! 順便研究該如何有效測試程式 :notes: jserv ::: ```clike= /* normalize x */ m = (ix0 >> 20); if (m == 0) { /* subnormal x */ while (ix0 == 0) { m -= 21; ix0 |= (ix1 >> 11); ix1 <<= 21; } for (i = 0; (ix0 & 0x00100000) == 0; i++) ix0 <<= 1; m -= i - 1; ix0 |= (ix1 >> (32 - i)); ix1 <<= i; } m -= KK3; /* unbias exponent */ ix0 = (ix0 & 0x000fffff) | 0x00100000; if (m & 1) { /* odd m, double x to make it even */ ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; } m >>= 1; /* m = [m/2] */ ``` 一開始看到這邊的時候有被 >> 困惑住,讓我開始想這個 shift right operator 是 arithmetic 或是 logical 的,後來查了 C11 規格書後,在(6.5.7)中很明確的告訴我們 shift right 是 arithemtic >The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of E1 / 2E2. If E1 has a signed type and a negative value, the resulting value is implementation-defined. ><C11 Standard> (6.5.7) 這下我們就知道 **m 是 ix0 扣除掉 fraction 的值,也就是剩下 sign bit 和 exponent 的值**,而此時 m 若等於 0 代表其 exponent 和 sign bit 皆為零,因此其值應該要以 Subnormal(Demornal) 的形式呈現,這邊原本有個小小疑問,若為**負的 subnormal**,則 m 應為 0xfffff800 != 0 則不會進入判斷 subnormal 中,但後來發現,**負數的情形竟然在上面已經討論過了**,以下已經不會再有負數的情況出現,所以==條件的編排順序也是很重要的阿!!!== 一開始想到讀到 Submornal 正確值,應該要主要的部份是 fraction 內的值,所以應該是想辦法找出 ix0[12:31] 和 ix1[0:31] 然後其值應為 0.xxxx * $\ 2^{-1024}$(xxxx為 fraction 部分) ```clike= while (ix0 == 0) { m -= 21; ix0 |= (ix1 >> 11); ix1 <<= 21; } ``` 上面這段還蠻玄的,想了很久終於想通倒底在做甚麼 * while (ix0 == 0) : 已經知道 0~11 bit 皆未 0 ,那用此條件的用意在哪?其實很簡單,就是要找出 fraction 前半段(12~31 bit) 是否為 0 * m -= 21 : 若 fraction 前 21 個 bits 皆為 0 ,那麼若要正規化前 21 個 bits 皆為零的二進位,就要乘以 $\ 2^{-21}$ ![](https://i.imgur.com/psJSGcT.png) * ix0 |= (ix1 >> 11) : 進行這段動作的原因是,將 ix0 的 fraction 部分(12~31 bit)套上 ix1 的 0~19 個 bits (如上圖) * ix1 <<= 21 : 因為已經 * $\ 2^{-21}$ ,所以位數上升 21 位 * while 用意 : 可能 ix1 之 0~19 bits 皆為零,此時就要再進行一次 bit operation,而離開迴圈時,可以將 ix0[12:31] 填入非 0 值,並得到 $\ *2^{m}$ ```clike= for (i = 0; (ix0 & 0x00100000) == 0; i++) ix0 <<= 1; m -= i - 1; ix0 |= (ix1 >> (32 - i)); ix1 <<= i; ``` * for 迴圈的重點在於 0x00100000,是將 ix0 向左 shift 直到**第12個 bit 為 1**為止,目的在將 fraction 的第一個 bit 對齊為 1 * m -= i-1 : shift 了多少個 bit 就要將 m 減去多少,以維持正確的數值 * ix0 |= (ix1 >> (32 - i)) : 由於 ix0 shift 完後最後面幾個 bits 會都是 0 ,所以比需將 fraction 部分從 ix1 中遞補上來 * ix1 <<= i : ix1 遞補了幾個 bits 就要 shift right 幾個 bits :::success 處理到這裡完,已經將 Subnormal number 化成以下形式 : ![](https://i.imgur.com/gcFrArQ.png) 真是可喜可賀阿 :100: :100: :100: 不過沒想到ㄚㄚㄚㄚ~~~ 這樣處理完之後竟然可以**和 normailized number 一起處理**了呢,真是太神奇了 :+1: :+1: ::: > 第12個應為編號11才對喔 > [name=HexRabbit][color=orange] >> 你是說圖的部份嗎?double 的 exponent 部份不是從編號 1~11 嗎?還是 fraction 部份錯了? >> [name=PunchShadow][color=green] >>> double 的 exponent 的確是 1~11, >>> 但是這個操作正如你所說的, >>> 會將 ix0 向左 shift 直到**第12個 bit 為 1**, >>> 所以 1 的位置編號應是 11 >>> [name=HexRabbit][color=purple] >>>> 沒錯,編號 11 的位置是 1,感謝 H大,已更新圖 >>>> [name=PunchShadow][color=green] ```clike= m -= KK3; /* unbias exponent */ ix0 = (ix0 & 0x000fffff) | 0x00100000; if (m & 1) { /* odd m, double x to make it even */ ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; } m >>= 1; /* m = [m/2] */ ``` 當 subnormalized number 和 normalized number 都化成同一個形式之後,我們就可以一起來處理啦 * m -= ==KK3== : 這邊可以從上面處理 subnormalized number 可以得知 m 就是所謂的 exponential bias, 公式為 $\ {2^{ exponent 位數 - 1} - 1}$ 所以single-precision 是 $\ {2^{8- 1} - 1=128}$, double-precision 則是 $\ {2^{ 11 - 1} - 1 = 1023}$ 得到 ==KK3 = 1023== >參考 : [floating point representation](http://fourier.eng.hmc.edu/e85_old/lectures/arithmetic/node12.html) * ix0 = (ix0 & 0x000fffff) | 0x00100000 : 將 ix0 中除了第 11 個 bit 和 fraction 部分外,都變成 0 ,因為已經取得 bias ,所以將 ix0 都變成 **0x001xxxxx** 的形式 * if (m & 1) : 考慮 m 為奇數的情況,假設 m = -5 則在進行++第7行++程式 m >>= 1 時, [m/2] = -3,所以要將 fraction 內值 ***2**,其數值才會正確 * ix0 += ix0 + ==((ix1 & sign) >> 31)== : 這邊會這樣寫是要考慮當 ix1 兩倍時,是否會進位的問題,所以才要 (ix1 & sign) 去取得 ix1 的第 1 個 bit,如果是 1 則會進位,因此要在 ix0 加上 1,而這種寫法可以**省去一個判斷式,應該就是有品味的程式了吧!!** :::success 至此終於全部解釋完要開根號的前置作業 (主菜都還沒上就快不行了 :sob:) ,然後一噴就是一個下午,這就是修行阿 :cry: ::: ```clike= /* generate sqrt(x) bit by bit */ ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; q = q1 = s0 = s1 = 0; /* [q,q1] = sqrt(x) */ r = 0x00200000; /* r = moving bit from right to left */ ``` * r = 0x0020000 : 設定第10個 bit 為 1,以方便後面 check bit by bit * q = q1 = s0 = s1 = 0 : 為牛頓法的範圍 :::info 這邊有個疑問,為何 ++2~3++ 還要將 ix0、ix1 還要乘以 2 ? 不是已經處理完奇數的部份了,應該是不用這兩行才對 ::: ```clike= while (r != 0) { t = s0 + r; if (t <= ix0) { s0 = t + r; ix0 -= t; q += r; } ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; r >>= 1; } ``` 這邊就是核心部份了,然後.......尷尬想不出來.....只能先往後面發展.... 20180326 : 看了 [HexRabbit ](https://hackmd.io/s/BkMaaAmcM#)共筆後,有種豁然開朗的感覺,現在終於可以理解為何老師一再強調**合作的重要性!!** * 首先要先去理解 2 進位的找平方根方法 這邊用 [Binary numeral system (base 2)](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Binary_numeral_system_(base_2)) 上面的範例解釋這段程式碼 ![](https://i.imgur.com/jF9ULhn.png) 1. **r** : 可以看成一個指標,從0x00200000,每次 shift right 一個 bit 直到 0,每經過一個 bit 就檢查對應的 ix0 夠不夠減去 t 2. **t** : 如圖所示,是代表左邊進行短除法的數字,若商數(q)出現 1 ,則 t 之最左邊的 bit 需要 *2 加回去 t 當成下一次的 t 3. **s0** : 為進行完一次除法後,要準備下次的暫存值 ```clike= r = sign; /* 看成從第0個 bit 往左邊掃*/ while (r != 0) { t1 = s1 + r; t = s0; if ((t < ix0) || ((t == ix0) && (t1 <= ix1))) { s1 = t1 + r; if (((t1 & sign) == sign) && (s1 & sign) == 0) s0 += 1; ix0 -= t; if (ix1 < t1) ix0 -= 1; ix1 -= t1; q1 += r; } ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; r >>= 1; } ``` * 這邊可以看成結合 ix0 和 ix1 的平方根,有幾種情況: * **ix0 為完全平方** : 在上段程式結束後就會歸零,所以 ==if ((t < ix0) || ((t == ix0) && (t1 <= ix1)))== 就會不會成立,直到 ix1 前面進位上來才會執行 if 內的操作 * **ix0 不為完全平方** : ix0 內的剩餘值會和 ix1 做結合,當 ix0 一直 shift left 與 t1 對齊時就會進入++第5行條件式++ * **t1** : 對 ix1 相減的除數,相當於 **t** 對 ix0 的功能,提供更精細的除法用 * **s1** : 相當於上一段程式中 **s0** 的角色,負責當作邊界,然後加回去 **t1** * **s0** : 承接上一段程式的值,繼續對 ix0 進行平方根,因為 ix0 會持續 shift left ,所以還是要繼續減去此值以維持正確 * **段落邏輯分析** : ``` if (((t1 & sign) == sign) && (s1 & sign) == 0) s0 += 1; ``` * (t1 & sign) == sign : t1 的第 0 個 bit 為 1,代表 t1 以無法再更大,若要進位只能去更改 t * (s1 & sign) == 0 : s1 的第 0 個 bit 不為 1,因為 s1 = t1 + r ,若 t1 = $\ {1xxxxxx.....}_2$ ,加上 r 卻讓第一個 bit 為 0,代表說要進位,因此要去將 s0 += 1,其值才會正確 ``` ix0 -= t; ``` * 如同上方說的, ix0 會一直左移,因此每次進行減法時,都要考慮 t 進去 ``` if (ix1 < t1) ix0 -= 1; ``` * if (ix1 < t1) 情況 : 當 t < ix0 正要進行減法時,但 ix1 卻又小於 t1 無法直接進行相減,所以就要從 ix0 借一位後方可減去 ``` q1 += r; ``` * **q1** : 相當於 ix1 的**商數**,想法和 q 有異曲同工之妙 ``` ix0 += ix0 \+ ((ix1 & sign) >\> 31); ix1 += ix1; r >>= 1; ``` * 將 ix0 和 ix1 shift left 1 bit (和上面做法一樣) :::info 以上這段完全沒有概念,只覺得有點像快速除法的感覺,++8-9++應該 ix0 和 ix1 *2 的樣子,覺得 *2 很詭異,完全不知道這段的用意,帶數字 trace 一遍還是不曉得,總覺得是在實現 [Binary numeral system (base 2) ](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Binary_numeral_system_(base_2))不過完全沒有想法 ::: :::success 太好了,再花了一個下午和看了 `HexRabbit` 的分析後,很多東西都慢慢可以稍微推導出來了,開心 :100: ::: 於是崩潰的跳過了無數行程式,終於來到我比較看得懂的地方,這裡是要結束的部分,想說從後面反推搞不好會有意想不到的進展 ```clike= ix0 = (q >> 1) + 0x3fe00000; ix1 = q1 >> 1; if ((q & 1) == 1) ix1 |= sign; ix0 += (m << KK5); INSERT_WORDS(z, ix0, ix1); return z; ``` 從這段可以看出來,最後 output 的是 z ,而 z 是由 ix0 和 ix1 結合而來的,而 ix0 又是從 (q>>1) + 0x3fe00000 而來,所以我推測,==前面的 q 一定是與平方根數有絕對的關係 * 這邊可以先猜測一下 **KK5**的值,前面得到的 m 是 bias 開根號後的值,也就是說在最後面要合成 z 的時候要將其加回去,因為要加到 exponent 的部分,所以推測是要 **shift left ==20==** 才有辦法對齊 ix0 之 exponent 部分 --- ## 因自動飲料機而延畢的那年心得 (未完成) 在看完了全部23篇的網誌後,心中其實是百感交集的,有很多熱血感動的情緒,但有更多的是對未來的惶恐和不安。 同樣身為大四生,同樣都是飲料的愛好者,同樣希望自己有朝一日能夠改變世界,哪怕只是一小部份也好,也希望為這個世界貢獻一己之力,但令我值得欽佩的是,作者願意勇敢踏出創業的那一小步。大家都在嚷攘著大學教育無法與實做接軌,但又有多少人,是真正去想辦法把想做的東西做出來?又或者說,**有多少人真正有想做的事或者想實現的目標**,這讓我回想起在看 Jserv 的 [你所不知道的C語言:指標篇(上)](https://www.youtube.com/watch?v=G7vERppua9o&t=6742s)中老師所提到的,為什麼有人會對C語言的本質有相當多的誤解和不熟悉,那是因為你**只是把它當作一個語言在學習,而沒有實際想利用c做的事情**。 一聽到這句話,當下其實很心有戚戚焉,不只是c語言,想要精通某事物或達到某個目的,所需要的絕對不僅只是一個**想學好的念頭**,而是一個很強烈的==動機==,而這個動機,必須是與它的本質相符合的。以本身為例,經歷過了研究所考試,雖然旁人都覺得我看起來很辛苦很努力,不過其實我念得很開心,當我很明確的知道我為何要讀這些,讀這些東西對我以後做的事會有什麼幫助後,一切都變得相當充實、相當理所當然,甚至會自發性的去深掘一些背後的理論和延伸,這些東西在我看來,其實一點都不辛苦,因為我知道,這都是為了我以後想做的事情在打的基礎,但也有很多人只是為了考上研究所而讀書,常常就會顯得力不從心(好像有點講多了,套句老師的口頭禪:`拉回來~拉回來` :laughing: :laughing: ) 回到飲料機,在現實世界中 To be continued ..... --- ## 課後學習及 CS:APP 3/e 心得、紀錄