# 2020q3 Homework5 (quiz5)
###### tags: `sysprog2020`
contributed by < `Msiciots` >
## :page_facing_up: 題目
- [2020q3 第 5 週測驗題](https://hackmd.io/@sysprog/2020-quiz5)
- [2020q3 Homework5 (作業區)](https://hackmd.io/@sysprog/2020-homework5)
:::info
目的: 檢驗學員對 [浮點數運算](https://hackmd.io/@sysprog/c-floating-point), [數值系統](https://hackmd.io/@sysprog/c-numerics) 及 [bitwise 操作](https://hackmd.io/@sysprog/c-bitwise) 的認知
:::
## 測驗 `1`
`D1` = ?D
- `(c)` 2
`D2` = ?
- `(d)` 1
:::success
延伸問題:
1. 解釋上述程式碼運作原理;
2. 以編譯器最佳化的角度,推測上述程式碼是否可透過 [tail call optimization](https://en.wikipedia.org/wiki/Tail_call) 進行最佳化,搭配對應的實驗;
搭配閱讀 [C 語言: 編譯器和最佳化原理](https://hackmd.io/s/Hy72937Me) 及 [C 語言: 遞迴呼叫](https://hackmd.io/s/rJ8BOjGGl)
3. 比照 [浮點數運算和定點數操作](https://hackmd.io/@NwaynhhKTK6joWHmoUxc7Q/H1SVhbETQ),改寫上述程式碼,提出效率更高的實作,同樣避免使用到 FPU 指令 (可用 `gcc -S` 觀察對應的組合語言輸出),並與使用到 FPU 的實作進行效能比較
:::
### 延伸問題 1. 程式運作原理
```cpp=1
#include <stdio.h>
#include <stdlib.h>
double divop(double orig, int slots) {
if (slots == 1 || orig == 0)
return orig;
int od = slots & 1;
double result = divop(orig / 2, od ? (slots + 1) >> 1 : slots >> 1);
if (od)
result += divop(result, slots);
return result;
}
```
- 假設被除數為浮點數 $A$,除數為整數 $B$:
```c=8
double result = divop(orig / 2, od ? (slots + 1) >> 1 : slots >> 1);
```
1. 若 $B$ 為偶數,$\frac{A}{B}=\frac{\frac{A}{2}}{\frac{B}{2}}$
2. 若 $B$ 為奇數,$\frac{A}{B}=>\frac{\frac{A}{2}}{\frac{B+1}{2}}$,讓 $B+1$ 成為偶數再進行 `1.`,經過加 1 的除數使結果值變小,我們需要補上差值
```c=10
result += divop(result, slots);
```
$差值=\frac{\frac{A}{B+1}}{B}$,我們加回原本的 `result` 可得到 $\frac{A}{B}=\frac{A}{B+1}+\frac{\frac{A}{B+1}}{B}=\frac{AB+A}{B(B+1)}=\frac{A(B+1)}{B(B+1)}=\frac{A}{B}$
- 遞迴運算至被除數 `orig` 接近`0` 到精確度已經不足,或者除數 `slots == 1`(偶數除法完成)
```c=5
if (slots == 1 || orig == 0)
return orig;
```
以下為計算 $\frac{1.0}{3}$ 結果:
```c=
orig: 0.500000 , slot:2 , result:0.250000 , od:0
orig: 0.125000 , slot:2 , result:0.062500 , od:0
orig: 0.031250 , slot:2 , result:0.015625 , od:0
orig: 0.007812 , slot:2 , result:0.003906 , od:0
orig: 0.001953 , slot:2 , result:0.000977 , od:0
orig: 0.000488 , slot:2 , result:0.000244 , od:0
orig: 0.000122 , slot:2 , result:0.000061 , od:0
orig: 0.000031 , slot:2 , result:0.000015 , od:0
orig: 0.000008 , slot:2 , result:0.000004 , od:0
orig: 0.000002 , slot:2 , result:0.000001 , od:0
.
.
.
orig: 0.000001 , slot:3 , result:0.000000 , od:1
orig: 0.000004 , slot:3 , result:0.000001 , od:1
orig: 0.000015 , slot:3 , result:0.000005 , od:1
orig: 0.000061 , slot:3 , result:0.000020 , od:1
orig: 0.000244 , slot:3 , result:0.000081 , od:1
orig: 0.000977 , slot:3 , result:0.000326 , od:1
orig: 0.003906 , slot:3 , result:0.001302 , od:1
orig: 0.015625 , slot:3 , result:0.005208 , od:1
orig: 0.062500 , slot:3 , result:0.020833 , od:1
orig: 0.250000 , slot:3 , result:0.083333 , od:1
orig: 1.000000 , slot:3 , result:0.333333 , od:1
```
## 測驗 `2`
QQ0 = ?
- `(a)` 0x01000000
QQ1 = ?
- `(d)` 0x3f000000
QQ2 = ?
- `(g)` 23
:::success
延伸題目:
1. 解釋上述程式碼何以運作
- 搭配研讀 [以牛頓法求整數開二次方根的近似值](http://science.hsjh.chc.edu.tw/upload_works/106/44f6ce7960aee6ef868ae3896ced46eb.pdf) (第 19 頁)
2. 嘗試改寫為更有效率的實作,並與 FPU 版本進行效能和 precision /accuracy 的比較
3. 練習 LeetCode [69. Sqrt(x)](https://leetcode.com/problems/sqrtx/),提交不需要 FPU 指令的實作
:::
### 延伸問題 1. 程式運作原理
延續 從 [$\sqrt{2}$ 的運算談浮點數](https://hackmd.io/s/ryOLwrVtz),假設 float 為 32-bit 寬度,考慮以下符合 IEEE 754 規範的平方根程式
```c=1
#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 & 0x7f800000) == 0x7f800000) {
/* 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;
}
```
以下一步步解說程式運行流程:
### 資料形態處理
```c=3
/* 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)
```
- 由於 `float` 與 `uint32_t` 皆佔 4 bytes 空間 利用 [union](https://en.wikipedia.org/wiki/Union_type) 可以存取 `float` 資料型態的 32 bit 數值。
- `EXTRACT_WORDS(ix0, d)` 輸入 float `d`,藉由 union 的方式得到 int 資料型態值
- `INSERT_WORDS(d, ix0)` 輸入 int `ix0`,藉由 union 的方式得到 float 資料型態值
- 在 macro 中使用 do {...} while(0) 來避免 [dangling else](https://en.wikipedia.org/wiki/Dangling_else),參考自 [C multi-line macro: do/while(0) vs scope block ](https://stackoverflow.com/questions/1067226/c-multi-line-macro-do-while0-vs-scope-block)
### 根號內特殊數值處理
```c=36
/* 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;
}
```
根據 [IEEE 754](https://zh.wikipedia.org/wiki/IEEE_754):
> **特殊值**
這里有三個特殊值需要指出:
1. 如果**指數**是 0 並且尾數的**小數部分**是0,這個數±0(和符號位相關)
2. 如果**指數** = $2^{e}-1$並且尾數的**小數部分**是0,這個數是±∞(同樣和符號位相關)
3. 如果**指數** =$2^{e}-1$並且尾數的**小數部分**非0,這個數表示為[非數(NaN)](https://zh.wikipedia.org/wiki/NaN)。
以上規則,總結如下:
>| 形式 | 指數 | 小數部分 |
>| -------- | -------- | -------- |
>| 零 | 0 | 0 |
>| 非正規形式 | 0 |大於0小於1|
>| 正規形式 | 1 到 $2^{e}-2$ | 大於等於1小於2|
>| 無窮 |$2^{e}-1$ | 0 |
>| NaN | $2^{e}-1$ | 非0|
- $\sqrt{\pm{0}} = 0$
- 根號內為負數回傳 NaN, 不處理虛數狀況
- $\sqrt{\pm{∞}} = \pm{∞}$
- $\sqrt{NaN}$ = $NaN$
### 根號內數值正規化
```c=48
/* 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] */
```
- 根據 [IEEE 754](https://zh.wikipedia.org/wiki/IEEE_754):
> 如果浮點數的指數部分的編碼值是0,分數部分非零,那麼這個浮點數將被稱為非正規形式的浮點數
>
```c=49
m = (ix0 >> 23);
```
`m` 代表浮點數的 $Exponent$ 位元值,如果 `m` 是 `0`,那程式會進行 normalize,從 $fraction$ 位元的最高為找到 `1` 往左位移直到 `m` 被補上 `1` ,再從 $Exponent$ 扣掉位移的數量
```c=55
m -= 127; /* unbias exponent */
```
- 根據 [IEEE 754 Exponent bias](https://en.wikipedia.org/wiki/Exponent_bias),$Exponent$ 中的 $1\sim254$ 分別表示 $-126\sim127$,所以將 `m` 減去 127 可以得到 $Exponent$ 位元的實際值
```c=56
ix0 = (ix0 & 0x007fffff) | 0x00800000;
```
- `ix0` 存放 $fraction+1$ = $1.fraction$
```c=57
if (m & 1) { /* odd m, double x to make it even */
ix0 += ix0;
}
m >>= 1; /* m = [m/2] */
```
- 若 $m$ 為偶數,$\sqrt{x \times 2^{m}} = \sqrt{x} \times 2^{\frac{m}{2}}$
- 若 $m$ 為奇數,$\sqrt{x \times 2^{m}} = \sqrt{2x\times2^{m-1}}=\sqrt{2x}\times2^{ \lfloor{\frac{m}{2}}\rfloor}$
### 開根號運算
```c=62
/* generate sqrt(x) bit by bit */
ix0 += ix0;
q = s0 = 0; /* [q] = sqrt(x) */
r = QQ0; /* r = moving bit from right to left */
// QQ0 = 0x01000000
while (r != 0) {
t = s0 + r; // t = s_i + r_i
if (t <= ix0) {
s0 = t + r; // s_{i+1} = t + r_i
ix0 -= t; // x_{i+1}/2 = x_i - t
q += r; // q_{i+1} = q_i + r_i
}
ix0 += ix0; // x_{i+1}
r >>= 1;
}
```
- 參考 [Methods of computing square roots: Binary numeral system (base 2)](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots)
- `q` 為均方根值,`ix0` 為根號內值,`r` 為每次處理的位元
- `if (t <= ix0)` 代表目前處理的 `ix0` 位元為 `1` ,`s0 = s0 + 2r` ,`ix0` 減去目前運算值`t`,目前運算位元 `r` 存入 `q`
- `t > ix0`,則目前 `ix0` 處理位元為 `0`
-
```c=75
ix0 += ix0; // x_{i+1}
r >>= 1;
```
位元移動,繼續處理下兩位值
### 處理進位
```c=75
/* 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);
}
}
```
- 參考 [IEEE 754: Rounding rules](https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules)
-
```c=25
static const float one = 1.0, tiny = 1.0e-30;
```
- 透過 `one - tiny` 的進位行為來判斷當前系統使用何種 Rounding rules
- `one - tiny >= one` 代表系統進行進位,需要進一步判斷是何種進位方式
- `one + tiny > one` 得知系統屬於無條件進位,所以直接進位,`q += 2`
- `one + tiny <= one` 得知系統屬於四捨五入,只會在 `q & 1` 為 `1` 的時候進位
- `one - tiny < one`,代表系統不進位,不需要特別處理 `q`
### 運算值轉回 `float` 回傳
```c=90
ix0 = (q >> 1) + 0x3f000000;
ix0 += (m << 23);
INSERT_WORDS(z, ix0);
return z;
}
```
-
```c=63
r = 0x01000000; /* r = moving bit from right to left */
```
第 63 行 `r`從左邊多一位開始算所以 `q>>1`,加上 $bias$ (這裡加 $126$ 因為當初運算使用 $1.fraction$ 已經加上了 $1$)
- `m` 移到原本 $Exponent$ 位數加上 `ix0`
- 最後將運算值轉成 `float` 型態回傳
## 測驗 `3`
ZZZ = ?
- `(d)` 1 - i
:::success
延伸題目:
1. 解釋上述程式碼何以運作
2. 嘗試改寫為更有效率的實作
:::
### 延伸問題 1. 程式運作原理
LeetCode [829. Consecutive Numbers Sum](https://leetcode.com/problems/consecutive-numbers-sum/)
給定一個正整數 $N$,問 $N$ 能寫成多少種連續正整數之和,例如 $9$ 可寫成 $4+5$,或 $2+3+4$。
```cpp=
int consecutiveNumbersSum(int N)
{
if (N < 1)
return 0;
int ret = 1;
int x = N;
for (int i = 2; i < x; i++) {
x += 1 - i;
if (x % i == 0)
ret++;
}
return ret;
}
```
從數學式可看出
$$
kx = N - \frac{k(k-1)}{2}
$$
- 只要找到 $K$ 滿足$N-(1+2+3...+(k-1))$ 整除 $K$ 就有一種連續正整數組合
- `x` 值就是存放 $N-(1+2+3...+(k-1))$
- `i` 對應到 `k`