owned this note
owned this note
Published
Linked with GitHub
# 2018q1 Homework2 (assessment)
contributed by < `TingL7` >
## 第 1 週 測驗 `一`
考慮以下程式碼:
```clike
#include <stdlib.h>
int isMultN(unsigned int n) {
int odd_c = 0, even_c = 0; /* variables to count odd and even SET bits */
if (n == 0) // return true if difference is 0.
return 1;
if (n == 1) // return false if the difference is not 0.
return 0;
while (n) {
if (n & 1) // odd bit is SET, increment odd_C
odd_c++;
n >>= 1;
if (n & 1) // even bit is SET, increment even_c
even_c++;
n = n >> 1;
}
/* Recursive call till you get 0/1 */
return(isMultN(abs(odd_c - even_c)));
}
```
其作用為檢查輸入整數是否為 N 的倍數,那麼 N 為多少?
---
### 解題想法&思考
* 程式碼解讀
* 由這兩段程式碼,可以得知這是一個遞迴程式,結束條件是 odd_c 和 even_c 相等或相差 1 ,且相等時,為N的倍數。
```clike
if (n == 0) // return true if difference is 0.
return 1;
if (n == 1) // return false if the difference is not 0.
return 0;
```
`return(isMultN(abs(odd_c - even_c)));`
* 而由這段程式碼可以知道 odd_c 及 even_c 分別在計算奇數位及偶數位為 1 的個數
```clike
while (n) {
if (n & 1) // odd bit is SET, increment odd_C
odd_c++;
n >>= 1;
if (n & 1) // even bit is SET, increment even_c
even_c++;
n = n >> 1;
}
```
* 綜合上述,並簡單化這個題目,在程式只有進行一次遞迴時,奇數位及偶數位總合相等時,即為 N 的倍數。將選項帶入有此特性的只有 3(11~2~) 。
### 延伸問題: 為什麼上述程式碼可運作呢?
>數字與文字間也要空白喔!
>[name=課程助教][color=red]
>>好的,已修改
>>[name=TingL7]
* 程式碼可以運作,是依據 3 的倍數,在二進位的情況下的特性,即奇數位數字和及偶數位數和相等或相差 3 的倍數。因此,只要證明 3 的倍數有此特性,即可確認上述程式碼是運作良好的。
* 這個特性與十進位中的 11 很像。參考[ 100 以內的質數倍數判別法及其延伸](http://www.shs.edu.tw/works/essay/2015/11/2015111502175190.pdf)中, 11 的倍數判別法,可寫出以下證明:
設有一 (n+1) 位數 N = (a~n~a~n-1~..a~1~a~0~)~2~ ,其中 a~n~$\neq$ 0
令 k 為 $\le n$ 的正整數
\begin{align}
a_n &=\sum_{i=0}^na_i\times2^i\\
& = [a_0+a_2(3+1)+a_4(15+1)+...+a_{2k}(2^{2k}-1+1)+...]\\
& +[a_1(3-1)+a_3(9-1)+a_5(33-1)+...+a_{2k+1}(2^{2k+1}+1-1)+...]\\
& = (a_0+a_2+a_4+...+a_{2k}+...)\\
& +[a_2(3)+a_4(15)+...+a_{2k}(2^{2k}+1)+...]\\
& - (a_1+a_3+a_5+...+a_{2k+1})\\
& + [a_1(3)+a_3(9)+a_5(33)+...+a_{2k+1}(2^{2k+1}+1)+...]...第一式
\end{align}
利用歸納證明法,可證明 2^2n^-1 及 2^2n+1^+1 為 3 的倍數:
n=1:則 2^2^-1=3 為 3 的倍數
令 n=k 成立,即 2^2k^-1=3x , x 可為任一正整數
n=k+1:
2^2(k+1)^-1=2^2k+2^-1=4\*2^2k^-1=4(2^2k^-1)+3=4(3x)+3=3(4x+1)
得證: 2^2n^-1 為 3 的倍數
因此, [a~2~(3)+a~4~(15)+...+a~2k~(2^2k^+1)+...] 為 3 的倍數...**第二式**
n=0:則 2^1^-1=3 為 3 的倍數
令 n=k 成立,即 2^2k+1^+1=3x , x 可為任一正整數
n=k+1:
2^2(k+1)+1^+1=2^2k+3^+1=4\*2^2k+1^+1=4(2^2k+1^+1)-3=4(3x)-3=3(4x-1)
得證: 2^2n+1^-1 為 3 的倍數
因此, [a~1~(3)+a~3~(9)+a~5~(33)+...+a~2k+1~(2^2k+1^+1)+...] 為 3 的倍數...**第三式**
由第一、二、三式可得知:
如果 (a~0~+a~2~+a~4~+...+a~2k~+...)-(a~1~+a~3~+a~5~+...+a~2k+1~+...) 為 3 的倍數,則 N 必為 3 的倍數。
因此,要判定二進位中,是否為 3 的倍數,可由 奇數位數字和及偶數位數和相等或相差 3 的倍數 的特性去判斷。
### 延伸問題: 將 N 改為其他質數再改寫為對應的程式碼,一樣使用遞迴
* 要將 N 改為其他質數,要先找出其他質數是否類似 3 的特殊判斷特性。
* N=5
* 參考[從二進位判斷數字是否被 5 整除](https://www.ptt.cc/bbs/Prob_Solve/M.1223098109.A.99F.html), N 是否為 5 的倍數判定,可由 (N>>2)-(N&3) 是否為 5 的倍數決定。
* 證明:
(N>>2)-(N&3) 為 N 除以 4 的商數 (a) 減去 N 除以 4 的餘數 (b)。寫成算式為
$N\div4=a...b\\
=>N =a\times4+b=a\times5-a+b\\
=>如果 b-a 為 5 的倍數,則 N 為 5 的倍數$
* 程式碼
```clike
#include <stdlib.h>
int isMultN(unsigned int n) {
if (n == 0|| n == 5) // return true if n is 0 or 5.
return 1;
if (n < 5) // return false n is smaller than 5
return 0;
n = (n >> 2) + (n & 3);
/* Recursive call till n <= 5 */
return(isMultN(n));
}
```
* N=7
* 由 N=5 的證明,是推廣到 N=7 ,甚至可以說是任何數字都可以用,只要找出與 2 的次方的關係。
* $N =a\times8+b=a\times7+(a+b)\\
=>如果 a+b 為 7 的倍數,則 N 為 7 的倍數$
* 程式碼
```clike
#include <stdlib.h>
int isMultN(unsigned int n) {
if (n == 0|| n == 7) // return true if n is 0 or 7.
return 1;
if (n < 7) // return false n is smaller than 7.
return 0;
n = (n >> 3) + (n & 7);
/* Recursive call till n <= 5 */
return(isMultN(n));
}
```
* N=3
* 由上述推得的方法,可以將 N=3 改寫。
* 改寫一:
$N =a\times4+b=a\times3+(a+b)\\
=>如果 a+b 為 3 的倍數,則 N 為 3 的倍數$
* 程式碼
```c
#include <stdlib.h>
int isMultN(unsigned int n) {
if (n == 0|| n == 3) // return true if n is 0 or 3.
return 1;
if (n < 3) // return false n is smaller than 3.
return 0;
n = (n >> 2) + (n & 1);
/* Recursive call till n <= 5 */
return(isMultN(n));
}
```
* 改寫二:
$N =a\times2+b=a\times3+(-a+b)\\
=>如果 b-a 為 3 的倍數,則 N 為 3 的倍數$
* 程式碼
```c
#include <stdlib.h>
int isMultN(unsigned int n) {
if (n == 0) // return true if n is 0.
return 1;
if (n == 1) // return false n is 1.
return 0;
n = (n >> 1) - (n & 1);
/* Recursive call till n <= 5 */
return(isMultN(n));
}
```
* 通用
* 其實,上面的方法是可以通用的,不論是否為質數,只要算清楚與 2 的次方差多少,再利用 shift 及 + 或 - 就可以判斷倍數。
## 第 2 週 測驗 `一`
![](https://i.imgur.com/Mp0cHII.jpg)
請完成下方程式碼,依循 IEEE 754 單精度規範,輸出 $2^x$ 的浮點數表達法,而且考慮兩種狀況:
- 當 $x$ 過小的時候,回傳 $0.0$
- 當 $x$ 過大的時候,回傳 $+∞$
注意:這裡假設 `u2f` 函式返回的浮點數值與其無號數輸入有著相同的位數,也就是至少 32-bit
```c
#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);
}
```
---
### 解題想法&思考
* 要解這題,需要充分了解浮點數的邊界判定。
* 浮點數 = (-1)^s^ \* 2^E^ \* M
* bias = 2^8-1^-1 = 127
* denormalized:(0~1)
* s: +/-,本題中沒有負的
* E: 1-bias= -126 = 2-2^7^
* M: 0.f~22~f~21~f~20~...f~0~
* normalized:(1~max)
* s: +/-,本題中沒有負的
* E: e-bias = e+1-2^7^
* M: 1.f~22~f~21~f~20~...f~0~
* too small
```
if (x < 2 - pow(2, Y0) - 23) {
exp = 0;
frac = 0;
```
* 判斷d enormalized 可以表達的最小數值,在單精度的情況下為
`0 00000000 00000000000000000000001` 即為 $(-1)^0 \times 2^{2-2^{7}} \times 2^{-23}=2^{2-2^{7}-23}$
因此, ++Y0=7++
* x 太小,回傳 0.0 。
* denormalized
```
} else if (x < Y1 - pow(2, Y2)) {
exp = 0;
frac = 1 << (unsigned) (x - (2 - pow(2, Y3) - Y4));
```
* 判斷 denormalized 可以表達的最大數值為
`0 00000000 11111111111111111111111` 即為 $(-1)^0 \times 2^{2-2^{7}} \times \sum_{i=1}^{23}2^{-i}=2^{2-2^{7}} \times (1-2^{-23})$
* (1-2^-23^) 取近似值為 1 ,因此 ++Y1=2++ , ++Y2=7++ 。
* $2^x = 2^{2-2^7}\times(frac\times2^{-23})$ ,則 $frac=2^{x-(2-2^7-23)}$
因此, ++Y3=7++ , ++Y4=23++ 。
* normalized
```
} else if (x < pow(2, Y5)) {
exp = pow(2, Y6) - 1 + x;
frac = 0;
```
* 判斷 normalized 可以表達的最大數值為
`0 11111110 11111111111111111111111` 即為 $(-1)^0 \times 2^{2^8-1-1-(2^7-1)} \times (1+1-2^{-23})=2^{2^7-1}\times (2-2^{-23})=2^{2^7}-2^{104}$
* 最大可表示的值不會超過 $2^x$ 為 $2^{2^7}$ ,因此 ++Y5=7++ 。
* 由於要表達 2 的次方,不需要小數點後的表示,因此 frac=0 。
* $2^x = 2^{exp-bias}=2^{exp-(2^7-1)}$ ,則 $exp=x+(2^7-1)$
因此, ++Y6=7++ 。
* too large
```
} else {
exp = Y7;
frac = 0;
}
```
* x 太大,回傳 $+∞$, INF 表示 exp 為 `11111111` = `0xFF` , frac=0 。
因此, ++Y7=0xFF++ 。
### 延伸問題: 給定一個單精度的浮點數值,輸出較大且最接近的 $2^x$ 值,需要充分考慮到邊界
* 邊界
* denormalized 最小值:
`0 00000000 00000000000000000000001` = $(-1)^0 \times 2^{2-2^{7}} \times 2^{-23}=2^{2-2^{7}-23}$
* denormalized 最大值:
`0 00000000 11111111111111111111111` = $(-1)^0 \times 2^{2-2^{7}} \times \sum_{i=1}^{23}2^{-i}=2^{2-2^{7}} \times (1-2^{-23}) = 2^{log_2(2^{-126}-2^{-149})}$
* normalized 最大值:
`0 11111110 11111111111111111111111` = $(-1)^0 \times 2^{2^8-1-1-(2^7-1)} \times (1+1-2^{-23})=2^{2^7-1}\times (2-2^{-23})=2^{log_2(2^{128}-2^{104})}$
* exp&frac
* too small
* 為 0
* exp = 0
* frac = 0
* denormalized
* exp = 0
* $2^x = 2^{2-2^7}\times(frac\times2^{-23})$,則$frac=2^{x-(2-2^7-23)}$
* normalized
* $2^x = 2^{exp-bias}\times(1+frac\times 2^{-23})=2^{exp-127}\times (1+frac\times 2^{-23})$
* $exp=127+x(整數部分)$
* $frac=(2^{x(小數部分)}-1)\times 2^{23}$
* too large
* INF
* exp = 0xFF
* frac = 0
* 程式碼
```c
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
static inline float u2f(unsigned int x) { return *(float *) &x; }
float exp2_fp(float x) {
unsigned int exp /* exponent */, frac /* fraction */;
float frac_f;
/* too small */
if (x < 2 - pow(2, 7) - 23) {
exp = 0;
frac = 0;
/* denormalize */
} else if (x < log(pow(2, -126) - pow(2, -149))/log(2)) {
exp = 0;
frac_f = pow(2.0, (x + 149));
frac = ((int)(frac_f*2)&1)?(int)frac_f+1:(int)frac_f;//向上捨入
/* normalized */
} else if (x < log(pow(2, 128) - pow(2, -104))/log(2)) {
exp = 127 + (int)((x<0)?x-0.99:x);//x為負數時,無條件進位
frac_f = ((pow(2.0,x-(float)((int)((x<0)?x-0.99:x)))-1.0)*(1<<23));
frac = ((int)(frac_f*2)&1)?(int)frac_f+1:(int)frac_f;//向上捨入
/* too large */
} else {
exp = 0xFF;
frac = 0;
}
/* pack exp and frac into 32 bits */
return u2f((unsigned) exp << 23 | frac);
}
```
* 執行結果
```
2^-150
exp2_fp():0.000000000000000000000000000000000000000000000000000000000000
powf():0.000000000000000000000000000000000000000000000000000000000000
2^-149.5
exp2_fp():0.000000000000000000000000000000000000000000000000000000000000
powf():0.000000000000000000000000000000000000000000001401298464324817
2^-127
exp2_fp():0.000000000000000000000000000000000000005877471754111437500000
powf():0.000000000000000000000000000000000000005877471754111437500000
2^-126.3
exp2_fp():0.000000000000000000000000000000000000009547961485342182800000
powf():0.000000000000000000000000000000000000009547961485342182800000
2^-126
exp2_fp():0.000000000000000000000000000000000000011754943508222875000000
powf():0.000000000000000000000000000000000000011754943508222875000000
2^127
exp2_fp():170141183460469230000000000000000000000.000000000000000000000000000000000000000000000000000000000000
powf():170141183460469230000000000000000000000.000000000000000000000000000000000000000000000000000000000000
2^127.5
exp2_fp():240615965050037600000000000000000000000.000000000000000000000000000000000000000000000000000000000000
powf():240615965050037600000000000000000000000.000000000000000000000000000000000000000000000000000000000000
2^128
exp2_fp():1.#INF00000000000000000000000000000000000000000000000000000000
powf():1.#INF00000000000000000000000000000000000000000000000000000000
2^0.5
exp2_fp():1.414213538169860800000000000000000000000000000000000000000000
powf():1.414213538169860800000000000000000000000000000000000000000000
```
* 結果探討
:::info
- 可以從結果看出,基本上與 powf() 的結果相同,除了 -149.5 ,為何 -149~-150 之間仍可以表示?
- (2^0.5^)^2^=2,但 1.41421353816986080^2^=1.99999993154 ,比實際值略小,如何輸出較大且最接近的 $2^x$ 值?
:::
:::warning
這發現很重要!你應該要能從數學的角度計算出上述程式碼和 powf(x, 0.5) 以及理論值的差異,在 IEEE 754 中,已有明確規範。
另外,請改善程式碼縮排,一率用 4 個 space,而非 tab,務必讓版面簡潔清爽。
:notes: jserv
:::
## 第 3 週 測驗 `一`
延續 [從 2 – √ 2 的運算談浮點數](https://hackmd.io/s/ryOLwrVtz),假設 double 為 64-bit 寬度,考慮以下符合 IEEE 754 規範的平方根程式,請補上對應數值,使其得以運作:
```clike=
#include <stdint.h>
/* A union allowing us to convert between a double and two 32-bit integers.
* Little-endian representation
*/
typedef union {
double value;
struct {
uint32_t lsw;
uint32_t msw;
} parts;
} ieee_double_shape_type;
/* 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)
/* Get two 32 bit ints from a double. */
#define EXTRACT_WORDS(ix0, ix1, d) \
do { \
ieee_double_shape_type ew_u; \
ew_u.value = (d); \
(ix0) = ew_u.parts.msw; \
(ix1) = ew_u.parts.lsw; \
} while (0)
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;
}
/* 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 */
}
/* 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] */
/* 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 */
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;
}
r = sign;
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;
}
/* use floating add to find out rounding direction */
if ((ix0 | ix1) != 0) {
z = one - tiny; /* trigger inexact flag */
if (z >= one) {
z = one + tiny;
if (q1 == (uint32_t) 0xffffffff) {
q1 = 0;
q += 1;
} else if (z > one) {
if (q1 == (uint32_t) KK4)
q += 1;
q1 += 2;
} else
q1 += (q1 & 1);
}
}
ix0 = (q >> 1) + 0x3fe00000;
ix1 = q1 >> 1;
if ((q & 1) == 1)
ix1 |= sign;
ix0 += (m << KK5);
INSERT_WORDS(z, ix0, ix1);
return z;
}
```
---
### 解題想法與思考
* 程式碼解讀
```clike=6
typedef union {
double value;
struct {
uint32_t lsw;
uint32_t msw;
} parts;
} ieee_double_shape_type;
```
* 建立union,為了能夠分別操作前後32bit,如下所示。
|bit|63|62 ~~~ 52|51 ~~~~~~ 32|31 ~~~~~~~~~~ 0|
|--|:---:|:---:|:---:|:---:|
|value|s|exp|frac|frac|
|parts|msw|msw|msw|lsw|
| |31|30 ~~~ 20|19 ~~~~~~ 0|31 ~~~~~~~~~~ 0|
```clike=14
/* 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)
/* Get two 32 bit ints from a double. */
#define EXTRACT_WORDS(ix0, ix1, d) \
do { \
ieee_double_shape_type ew_u; \
ew_u.value = (d); \
(ix0) = ew_u.parts.msw; \
(ix1) = ew_u.parts.lsw; \
} while (0)
```
* 這兩段 define 主要功能是使 union 中的 double 和 parts 互相轉換。
* #define在做的事,基本上就是代換,將程式其他地方出現 INSERT_WORDS(d, ix0, ix1) ,代換成下面一段程式碼,不做任何修改。通常在使用會以 `INSERT_WORDS(d, ix0, ix1);` 的樣子出現,因此在代換的程式碼中,最後不會在加 `;`。
* 因為直接代換的關係,可能會產生 dangling else ,所以需要加上 `do{...}while(0)`。
* dangling else : 在使用 if-then(-else) 時,產生語意不清的情況,如:
`if A then if B then C else D` ,會有兩種解讀:
`if A then (if B then C)else D` 或
`if A then (if B then C else D)`
* [Linux Kernel 巨集 do{...}while(0) 的撰寫](http://www.runpc.com.tw/content/content.aspx?id=108451)中,舉了一個很好的例子,將其簡化來看:
```c
#define func(A, B) \
{ \
if(A) \
B = 1; \
else \
B = 0; \
} \
int funcA(int A,int B){
if(B)
func(A, B);
else
printf("Error B");
return B;
}
```
將 func(A,B) 用define的內容代換:
```c
int funcA(int A,int B){
if(B)
{
if(A)
B = 1;
else
B = 0;
};
else
printf("Error B");
return B;
}
```
可以發現一個很尷尬的地方,else 前面有 `;` ,表示if在else之前就做完了, else 是多的,語意與原本要表達的意思完全不同,這就是dangling else的情況。
若加上 `do{...}while(0)` 即可避免這樣的情況。
```c
int funcA(int A,int B){
if(B)
do{
if(A)
B = 1;
else
B = 0;
}while(0);
else
printf("Error B");
return B;
}
```
```clike=44
/* take care of INF and NaN */
if ((ix0 & KK1) == KK2) {
/* sqrt(NaN) = NaN, sqrt(+INF) = +INF, sqrt(-INF) = sNaN */
return x * x + x;
}
```
* 考慮 INF 及 NaN 的情況,要判斷是否為這兩者,只要看exp是否全為 1 ,對 ix0 來說, bit 20~30 的位置為 exp ,因此如果是 INF 或 NaN , ix0 必為
`x 11111111111 xxxxxxxxxxxxxxxxxxx` ,需要在意的只有 exp 那一段,因此 ++KK1 = `0 11111111111 0000000000000000000` = 0x7FF00000++
* 且, exp 必須全為 1 ,因此, ++KK2 = 0x7FF00000++ 。
* 負數開根號為 sNaN,因此,回傳值不能直接回傳 x 。要用-INF得到sNaN,要先了解INF的運算:
* +INF + +INF = +INF
* -INF + -INF = -INF
* +INF + -INF = +INF - +INF = -INF - -INF = sNaN
* +INF * +INF = +INF
* -INF * -INF = +INF
* +INF * -INF = -INF
基本上,要得到sNaN,就必須使 +INF 和 -INF 相加,而 -INF 得到 +INF 的方法只能乘以自己,因此,回傳值為 `x * x + x` 才符合要求。
* 那麼,如果想要不用乘法做回傳值,且完成sqrt(NaN) = NaN, sqrt(+INF) = +INF, sqrt(-INF) = sNaN,有什麼辦法?
* 先判斷負號,讓 -INF 在進到這個if之前,就因為負號被驅逐了,這樣就可以直接回傳 x 。
```clike=49
/* 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 */
}
```
* 判斷為 0 或負數,+-0 的開根號為 +-0 ,負數的開根號為 sNaN 。可由 ix0 判斷,這也是為什麼 ix0 的 type 要為 int32_t 而非 uint32_t 。
* x 為 0 ,除了 sign bit 無所謂以外,其他 bit 都為 0 。拆成 msw 及 lsw 兩部份來看: `ix0 & (~sign)` 即是判斷 msw 中除了 sign bit 以外,其他 bit 是否全為 0 , `ix1` 就是lsw全部是否為0。
* x 為負數,須回傳sNaN。
```clike=56
/* 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));//x << i
ix1 <<= i;
}
```
* `m = (ix0 >> 20);`,表示 m 為 sign + exp 。不過,透過前面的if負數判斷, sign 必定為 0 。
* 在非正規化(exp = 0)的情況下,先將它變成正規化,透過 shift 來操作。先找出 frac 中,第一個 1 的位置j,再將其移到 exp 的最小位,並修改 m 。也就是說,原本 x = 2^1-bias^ * f ,現在 x = 2^e-bias^ * (f * 2^j^) ,因此,實際上 x = 2^m^ * 2^-bias^ * (f * 2^j^) , m = -j+1,1代表e
* 假設原本 f 為 0001010...0,視為 0.0001010...0, shift 後, f 為 (1) 010...0 ,視為 1.010...0。不需要再考慮是否加一,這大概是非正規化數 exp 和 frac 設計巧妙的地方。
```clike=70
m -= KK3; /* unbias exponent */
ix0 = (ix0 & 0x000fffff) | 0x00100000;
```
* 之後,又進一步將 2^-bias^ 也包進 2^m^ 之中,同時,修改 ix0 exp 的部分為 0x001。此時, x = x = 2^m^ * f' (f' = f * 2^j^ , m = e - j - bias)
因此,++KK3 = bias = 2^11-1^-1 = 1023++。
```clik=72
if (m & 1) { /* odd m, double x to make it even */
ix0 += ix0 + ((ix1 & sign) >> 31);
ix1 += ix1;
}
m >>= 1; /* m = [m/2] */
```
* 這段要做 exp 部份的開根號,基本上就是次方除以二。但要考慮到 m 可能是奇數的情況。
* m 如果是奇數的話,要將 exp+frac 的部分乘以二,以表示會被忽略的部份。
```clike=78
/* 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 */
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;
}
r = sign;
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;
}
```
* 參考[二進制根式法](https://home.gamer.com.tw/creationDetail.php?sn=2032528)、[wikipedia](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Binary_numeral_system_(base_2))、[√Square Roots](http://www.azillionmonkeys.com/qed/sqroot.html),了解直式開根號的方法,基本上就對應了這段程式碼。兩個while迴圈,基本上就是對ix0及ix1做開根號的動作。
```clike=115
/* use floating add to find out rounding direction */
if ((ix0 | ix1) != 0) {
z = one - tiny; /* trigger inexact flag */
if (z >= one) {
z = one + tiny;
if (q1 == (uint32_t) 0xffffffff) {
q1 = 0;
q += 1;
} else if (z > one) {
if (q1 == (uint32_t) KK4)
q += 1;
q1 += 2;
} else
q1 += (q1 & 1);
}
}
```
* 這一段在做捨入, IEEE 754 預設捨入方法是向偶數捨入,但我看不懂 z 是在做什麼。
* 在需要進位的情況下,因為[q,q1]實際為一組數字,如果 q1 全為 1 的話,進位之後,會變成 1 0000...00。
* q1 + 2 之後,q 需要 +1 ,這就表示 ++KK4 = 1 000...00-2 = 111...10 = 0xfffffffe++ 。
* `q1 += (q1 & 1);` 就比較單純,表示向偶數捨入。
* 直接跑過程式,看能不能幫助理解。結果,都如我所預測,`z = one - tiny` 由於 tiny 很小,會被忽略,所以 z == one ,進入第二層 if ,在 q1沒有剛好為 `0xffffffff` 時,會直接進入第二個 else , 因為 `z = one + tiny;` 的 tiny 也會被忽略,所以 z == one 。
* tiny = 1.0e-300,double可表達的範圍約1.7e-308~1.7e-308
:::info
- `z = one - tiny; /* trigger inexact flag */`
`if (z >= one)`的目的是什麼?在什麼情況下會出現 z > 0 ?
- 在什麼情況下,tiny 不會被忽略?
:::
```clike=131
ix0 = (q >> 1) + 0x3fe00000;
ix1 = q1 >> 1;
if ((q & 1) == 1)
ix1 |= sign;
ix0 += (m << KK5);
INSERT_WORDS(z, ix0, ix1);
return z;
```
* exp 要將 1-bias 加回去,因此加上 0x3fe00000 ,此時為 2^0^。
* m 為開根號後的 exp ,要加回 ix0 ,exp 在20~30的位置,因此,要做 shift ,且 ++KK5 = 20++ 。
* 最後再將 ix0 和 ix1 合成 z 就大功告成了~~~
### 延伸題目: 改為 float (單精度) 版本,注意應該用更短的程式碼來實作
* 想法與思考
* 要注意 ix0 和 ix1 的運算,float不需要拆成兩部分。
* 不需要拆成兩部份,是否還需要union的轉換?
float 無法做 bitwise 的操作,需要轉換成int才行。
* 先做負號判斷,再做 +INF and NaN ,這樣可以直接回傳 x 。 -INF 的部分,在負號判斷時,已回傳 sNaN 。
* bias = 2^8-1^ -1 = 127
* tiny 改為1e-30,因為 float 可表達的範圍約3.4e-38~e.4e-38
* 程式碼
```clike=
#include <stdint.h>
/* A union allowing us to convert between a float and a 32-bit integers.*/
typedef union {
float value;
uint32_t v_int;
} ieee_float_shape_type;
/* Set a float from a 32 bit int. */
#define INSERT_WORDS(d, ix0) \
do { \
ieee_float_shape_type iw_u; \
iw_u.v_int = (ix0); \
(d) = iw_u.value; \
} while (0)
/* Get a 32 bit int from a float. */
#define EXTRACT_WORDS(ix0, d) \
do { \
ieee_float_shape_type ew_u; \
ew_u.value = (d); \
(ix0) = ew_u.v_int; \
} while (0)
static const float one = 1.0, tiny = 1.0e-30;
float ieee754_sqrt(float x)
{
float z;
int32_t sign = 0x80000000;
uint32_t r;
int32_t ix0, s0, q, m, t, i;
EXTRACT_WORDS(ix0, x);
/* take care of zero */
if (ix0 <= 0) {
if ((ix0 & (~sign)) == 0)
return x; /* sqrt(+-0) = +-0 */
if (ix0 < 0)
return (x - x) / (x - x); /* sqrt(-ve) = sNaN */
}
/* take care of +INF and NaN */
if ((ix0 & 0x7ff00000) == 0x7ff00000) {
/* sqrt(NaN) = NaN, sqrt(+INF) = +INF,*/
return x;
}
/* normalize x */
m = (ix0 >> 23);
if (m == 0) { /* subnormal x */
for (i = 0; (ix0 & 0x00800000) == 0; i++)
ix0 <<= 1;
m -= i - 1;
}
m -= 127; /* unbias exponent */
ix0 = (ix0 & 0x007fffff) | 0x00800000;
if (m & 1) { /* odd m, double x to make it even */
ix0 += ix0;
}
m >>= 1; /* m = [m/2] */
/* generate sqrt(x) bit by bit */
ix0 += ix0;
q = s0 = 0; /* [q] = sqrt(x) */
r = 0x01000000; /* r = moving bit from right to left */
while (r != 0) {
t = s0 + r;
if (t <= ix0) {
s0 = t + r;
ix0 -= t;
q += r;
}
ix0 += ix0;
r >>= 1;
}
/* use floating add to find out rounding direction */
if (ix0 != 0) {
z = one - tiny; /* trigger inexact flag */
if (z >= one) {
z = one + tiny;
if (z > one)
q += 2;
else
q += (q & 1);
}
}
ix0 = (q >> 1) + 0x3f000000;
ix0 += (m << 23);
INSERT_WORDS(z, ix0);
return z;
}
```
* 執行結果
```
sqrt 1e+78
1.#INF00
sqrt -1e+78
-1.#IND00
sqrt 9
3.000000
sqrt 1.44
1.200000
sqrt 17453723534
132112.546875
```
## [因為自動飲料機而延畢的那一年](http://opass.logdown.com/posts/1273243-the-story-of-auto-beverage-machine-1)及學習本課程 3 週之後的感想與啟發
- 現實與想像的落差。
現實其實有更多的層面需要考量,我們時常將事情簡單化,無論是面對自以為龐大的專案,還是認為很簡單的幾個流程,實際上都是需要經過很多的研究時間、成本。我沒想過,一隻十塊錢不到的原子筆、坐上去舒服的椅子,這些生活用品是怎麼做出來的。
- 學用落差。
我曾經以為,土木系學用落差嚴重只是因為我們不可能真的找一個地方給房子、蓋橋,只能紙上談兵,那牽涉到太多人力、成本。而資訊系這方面的問題,比較沒那麼嚴重,畢竟寫程式每個人的電腦都可以做,而且馬上就可以看到結果。實際上不是,寫寫小程式不難,就像土木系在做模型時一樣,但面對大一點的就容易出現問題了。
- 全心投入。
看完這片文章,讓我想起一年前,為了做一個小房子的模型,而做過無數的實驗跟研究。那時是要蓋一個混凝土的模型,原本的想像也是很簡單,設計尺寸、做模、灌水泥、拆模。但實際下去做之後,才發現現實世界有太多問題,那時為了做這件事,整天都在想、在試。做完之後,成果也不怎麼樣,最後也是收到角落。而這之中,最寶貴的,我想,不是結果多厲害,而是在那段時間全心投入的自己。在實習過後,體會更深的,工作之餘,想要再將心力投入到其他事情上,是沒有那麼容易的,尤其是需要耗費腦力的事。希望自己能一直保有那份熱情。
- 關於課程
一開始覺得壓力很大,這種上課模式沒有遇過 : 每週都小考,還有很多課後學習的教材要看,作業還會被同學批評、觀摩 ~~(害羞)~~,一週花費超過16個小時在上面,不過,還是很多教材沒看完。但,這好像比較接近我想像中的大學課程,有點半自學的性質,也可以說很幸運的在最後一個學期修到這門課。
- 程式寫得好的人,數學都很厲害。
這三、四週下來,深深的體悟到什麼是真正在用的程式。其實很多事情的原理並不難,只是我沒有好好理解過,隨堂考的題目要解出來其實不難,但是在沒有細細看過程式前,很多都是靠感覺猜的,因為那些背後的理論我尚未融會貫通。但是,要完全看懂程式碼在做什麼,為什麼要那樣做就完全是另外一回事了,這大概就是理論跟實做的差距吧。
## 「[課後學習](http://wiki.csie.ncku.edu.tw/sysprog/schedule)」教材和 [CS:APP 3/e](http://csapp.cs.cmu.edu/)心得和提問