# 2020q3 Homework5 (quiz5)
contributed by < `zzzxxx00019` >
>[2020q3 第 5 週測驗題](https://hackmd.io/@sysprog/2020-quiz5)
## 測驗 1
### 題目:
考慮到以下浮點數除法程式: (fdiv.c)
```cpp=
#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 / D1, od ? (slots + D2) >> 1 : slots >> 1);
if (od)
result += divop(result, slots);
return result;
}
```
假設 `divop()` 的第二個參數必為大於 0 的整數,而且不超過 int 型態能表達的數值上界。請補完程式碼。
### 原理:
```cpp=5
if (slots == 1 || orig == 0)
return orig;
```
* 判斷除數是否為 1 或是被除數是否為 0 ,如果成立,答案即為被除數 orig
```cpp=7
int od = slots & 1;
double result = divop(orig / D1, od ? (slots + D2) >> 1 : slots >> 1);
```
* 判斷 slots 是否為偶數
* slots 為偶數,可視為 $\dfrac{orig}{slots} = \dfrac{orig/2}{slots/2}$ 並不影響結果,推得 `D1 = 2`
* slots 為奇數,可將 slots 加 1 使其可被 2 整除,故 `D2 = 1`
```cpp=9
if (od)
result += divop(result, slots);
```
* 考慮到 slot 為奇數,若 slot 加 1 ,要維持原結果,則可寫成:
$\dfrac{orig}{slots}=\dfrac{orig}{slots+1}\times\dfrac{slots+1}{slots}=\dfrac{orig}{slots+1}\times(1+\dfrac{1}{slots})=\dfrac{orig/2^n}{(slots+1)/2^n}+\dfrac{orig/2^n}{(slots+1)/2^n}\times\dfrac{1}{slots}$
* $\dfrac{orig/2^n}{(slot+1)/2^n}\times\dfrac{1}{slot}$ 的部分即為 `divop(result, slots)`
---
## 測驗 2
### 題目:
延續 [從 $\sqrt{2}$ 的運算談浮點數](https://hackmd.io/s/ryOLwrVtz),假設 float 為 32-bit 寬度,考慮以下符合 IEEE 754 規範的平方根程式,請補上對應數值,使其得以運作:
```cpp=
#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 = QQ0; /* 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) + QQ1;
ix0 += (m << QQ2);
INSERT_WORDS(z, ix0);
return z;
}
```
請補完程式碼,使上述程式碼得以運作。
### 原理:
* [單精度浮點數](https://zh.wikipedia.org/wiki/%E5%96%AE%E7%B2%BE%E5%BA%A6%E6%B5%AE%E9%BB%9E%E6%95%B8) ,第 1 位表示正負,中間 8 位表示指數,後 23 位儲存有效數位
```
sign exponent(8 bits) fraction(23 bits)
0 01111100 01000000000000000000000
```
以上述為例:
$sign=+1$
$exponent=(-127)+124=-3$
$fraction=1+2^{-2}=1.25$
$value=sign\times 2^{exp}\times fraction=(+1)\times 2^{-3}\times 1.25=0.15625$
因此 `float` 在透過單精度浮點數的變換後,可以用 `unit32_t` 的形式來表達
接著逐步拆解程式的每個步驟:
```cpp=10
/* 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)
```
* `INSERT_WORDS(d, ix0)` 將 `ix0` 轉換成浮點數 `d`
```cpp=17
/* 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)
```
* `EXTRACT_WORDS(ix0, x)` 將浮點數 `d` 的編碼轉換成 `ix0`
```cpp=34
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 */
}
```
* 先將浮點數 `x` 轉換為精準浮點數編碼,以 `ix0` 表示,若 `ix0 = 0` ,則 `ix0 & (~sign) = 0` ,而 $\sqrt{0}=0$ ,因此直接 `return 0` ,反之則代表 `x` 為負數浮點數,根據定理, $\sqrt{-N}$ 並不存在,因此 `return (x - x) / (x - x)`
```cpp=43
/* take care of +INF and NaN */
if ((ix0 & 0x7f800000) == 0x7f800000) {
/* sqrt(NaN) = NaN, sqrt(+INF) = +INF,*/
return x;
}
```
* `0x7f800000` 以二進制呈現為以下結果,代表若 `ix0` 的 `exp` 全為 `1` ,表示 `x` 為 `+INF` ,而 `ix0` 的 `exp` 全為 `1` , `fraction` 為任意值 ,則表示 `x` 為 `NaN` ,直接 `return x`
|sign|exponent|fraction |
| - | - | - |
| 0 | 11111111| 00000000000000000000000|
```cpp=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] */
```
以下參考自 < `RinHizakura` > 文章說明:
* 若 $x=2^{n}\times y$ ,且 $1≤y<4$,則 $\sqrt{x}=\sqrt{2^n\times y}=2^{n/2}\times \sqrt{y}$
* 在第 `48` 行, `ix0 >> 23` 後,留下的為 `sign` 與 `exponent` ,而 `sign` 為 `1` 的情況,在前面已過濾掉,因此 `m` 即為 `exponent`
* 當 `m = 0` ,表示 `float x` 的 `exponent` 為 `0` ,在 `denormalized` 情況下,將浮點數表達為 $x=2^{-126}\times0.fraction$
* 為了使 $x$ 得以表達為 $2^{n}\times y$ 的形式,將 `ix0` 左移直到 `exponent bit` 出現 `1` 為止
* 而原本在 `denormalized` 情況下 `exp = -126` , `m = 0` ,即 `exp = -127 - m + 1` ,而考慮到 `ix0` 左移後,需將左移量補上,因此 `m = m + 1 - i` ,即 `m -= i - 1`
$float=1\times 2^{-126} \times (2^{-1}+2^{-2})=2^{-127}+2^{-128}$
|sign|exponent|fraction|
| - | -- | --- |
| 0 |00..00 | 11..00 |
**normalized float ,left shift 1 bit :**
$m=1-i-127=-127$
$float = 1\times 2^{-127} \times (1+2^{-1})=2^{-127}+2^{-128}$
|sign|exponent|fraction|
| - | -- | --- |
| 0 |00..01 | 10..00 |
* 再透過 `(ix0 & 0x007fffff) | 0x00800000` 這樣一連串的邏輯運算,將 `bit 24` 設為 `1` ,如此可保留原本 `1.fraction` 的部分 ( 二進制分數表示法 $2^0 + 2^{-1} + 2^{-2}...=1.fraction$ )
* 檢查 `m` ,若為偶數, $\sqrt{2^m}=2^{m/2}$ ;反之,若為奇數,將多出來的 `m` 補到 `fraction` ,即 `ix0 += ix0` 的部分,而 `m >>= 1` 會自動略去奇數多出來的 `1`
$m=even,\sqrt{2^m\times y}=2^{m/2}\times \sqrt{y}$
$m=odd,\sqrt{2^m\times y}=2^{m/2}\times \sqrt{2y}$
```cpp=62
/* generate sqrt(x) bit by bit */
ix0 += ix0;
q = s0 = 0; /* [q] = sqrt(x) */
r = QQ0; /* 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;
}
```
* $\sqrt{y}$ 的部分,若 $1≤y<4$ ,則 $1≤\sqrt{y}<2$ ,可從 $1$ 逐漸向 $\sqrt{y}$ 逼近
* 令 $f(x)=x^2-N$ ,透過尋找 $f(x)=0$ 的解,找到 $x=\sqrt{N}$ 的最近似解
:::info
數學推導:
* 令 $q_i$ 為 $\sqrt{y}$ 到小數點第 i 位的精確值,且 $i≥0$ ,則 $q_0=1$
* 透過 $(q_{i}+2^{-(i+1)})^2$ 逐漸逼近 $y$ :
* 若 $(q_i+2^{-(i+1)})^2≤y$,則 $q_{i+1}=q_i+2^{-(i+1)}$ ,即第 `i+1` 位補上 `1`
* 若 $(q_i+2^{-(i+1)})^2>y$,則 $q_{i+1}=q_i$ ,即第 `i+1` 位補上 `0`
* 定義以下變數來簡化算式 :
* $s_i = 2\times q_i$
* $y_i = 2^{i+1}\times (y-q_i^2)$
* 接著將上述 $(q_i+2^{-(i+1)})^2≤y$ 整理:
$\implies q_i^2+q_i\times 2^{-i}+2^{-2(i+1)}≤y$
$\implies q_i\times 2^{-i}+2^{-2(i+1)}≤y-q_i^2$
$\implies 2^{-(i+1)}(2\times q_i^2 + 2^{-(i+1)})≤y-q_i^2$
$\implies q_i\times 2^{-i}+2^{-(i+1)}≤(y-q_i^2)\times 2^{i+1}$
$\implies s_i+2^{-(i+1)}≤y_i$
* 若 $s_i+2^{-(i+1)}≤y_i$ 成立:
* $s_{i+1}=s_i+2^{-i}$
* $y_{i+1}=2\times(y_i-s_i-2^{-(i+1)})$
* 若 $s_i+2^{-(i+1)}≤y_i$ 不成立:
* $s_{i+1}=s_i$
* $y_{i+1}=2\times y_i$
:::
* 回顧程式碼, `ix0 += ix0` 的部分是為了增加精確度,使小數部分有 `24 bits` ,故將整數部分左移,而 `QQ0 = 0x01000000` ,則為了開始從 $2^0$ 開始向小數點迫近
* 回顧上面所定義的變數:
* $s_i = 2\times q_i$
* $y_i = 2^{i+1}\times (y-q_i^2)$
* 而後面 `while` 迴圈就是在執行以下計算:
:::info
$t=s_i+2^{-(i+1)}$
$if( t ≤ y_i):$ ,即 $(s_i+2^{-(i+1)})≤y_i$
$s_{i+1}=t+2^{-(i+1)}=s_i+2^{-(i+1)}+2^{-(i+1)}$ ,即 $s_{i+1}=s_i+2^{-i}$
$y_{i+1}=y_i-t$ , 即 $y_{i+1}=y_i-s_i-2^{-(i+1)}$
$q_{i+1}=q_i+r$ , 即 $q_{i+1}=q_i+2^{-(i+1)}$
$y_{i+1}=2\times y_{i+1}$ ,若 $if$ 成立,$y_{i+1}=2\times (y_i-s_i-2^{-(i+1)})$ ;反之, $y_{i+1}=2\times y_i$
:::
```cpp=78
/* 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);
}
}
```
* 四捨五入所算出來的近似值 `q`
:::warning
TO DO
詳細解釋 rounding 的原理。
:::
```cpp=90
ix0 = (q >> 1) + QQ1;
ix0 += (m << QQ2);
INSERT_WORDS(z, ix0);
```
* 第 `90` 行的部分,將近似值 `q` 以精準浮點數表達
* 最後 `q` 是將小數點點在 `24 bit` 與 `25 bit` 間的二進位小數,因此 `q` 必須往右退一位,才能符合 IEEE 754 對浮點數的規範
* 加上 `QQ1` ,使得 `ix0` 得以正確轉換為浮點數,即 $ix0 = 2^0\times 1.fraction$, 因此在 `exponent` 這邊必須為 `0` ,故 `QQ1=0x3f000000`
* 再乘上一開始的 $2^{n/2}$ ,即 `ix0` 的 `exponent` 部分加上本來的 `m` ,而 `exponent` 部分從第 `24 bit` 開始,因此 `QQ2=23` ,將 `m` 左移至 `exponent` 位置
* 最後透過 `INSERT_WORDS` ,將精準浮點數編碼 `ix0` 轉換為 `float` 型態
### 延伸問題:
* 練習 LeetCode [69. Sqrt(x)](https://leetcode.com/problems/sqrtx/),提交不需要 FPU 指令的實作
* 透過 `left` 與 `right` 兩變數,兩邊同時向 `Sqrt(x)` 迫近
* 根據 [算術-幾何平均值不等式](https://en.wikipedia.org/wiki/Inequality_of_arithmetic_and_geometric_means) 證明 $\dfrac{x+y}{2}≥ \sqrt{xy}$ ,因此將 right 設為 $\dfrac{x}{2}$ 逐漸遞減
* 若 $x=2^n\times y$ ,則 $\sqrt{x}=2^{n/2}\times \sqrt{y}$ ,因此 left 從 $2^{n/2}$ 開始遞增
* 由 `mid` 直接取 `left` 與 `right` 中間值做計算,達到時間複雜度 $O(log$ $n)$的效果
```graphviz
digraph
{
node[shape = record]
Example[label = "0 | 1 | ... | left | → | Sqrt(x) | ← | right | ... | x"]
}
```
```cpp=
int mySqrt(int x) {
if ( x <= 1 ) return x ;
uint32_t m = 32 - __builtin_clz(x) ;
m -= 1 ;
m >>= 1 ;
int l = 1 << m ;
int r = x/2 ;
while(l <= r) {
uint64_t mid = (l + r)>>1 ;
uint64_t check = mid*mid ;
if( check == x) return mid ;
else if ( check > x ) r = mid - 1 ;
else l = mid + 1 ;
}
return r ;
}
```
![](https://i.imgur.com/kKsNNXJ.png)
---
## 測驗 3
### 題目:
LeetCode [829. Consecutive Numbers Sum](https://leetcode.com/problems/consecutive-numbers-sum/)
```cpp
int consecutiveNumbersSum(int N)
{
if (N < 1)
return 0;
int ret = 1;
int x = N;
for (int i = 2; i < x; i++) {
x += ZZZ;
if (x % i == 0)
ret++;
}
return ret;
}
```
請補完程式碼,使上述程式碼得以運作。
### 原理:
根據解題想法說明,若要寫成連續正整數之和,可寫成以下表示式:
$x+(x+1)+(x+2)+...+x+(k-1)=kx+\dfrac{(k-1)k}{2}=N$
整理後得到:
$kx=N-\dfrac{(k-1)k}{2}$ ,其中 $\dfrac{(k-1)k}{2}$ 即為 $1+2+3+...+k$ 之和
而 `x += ZZZ` 就是在做 $N-\dfrac{(k-1)k}{2}$ 的部分,而 $\dfrac{(k-1)k}{2}$ 為從 `1` 慢慢往上加到 `k` 個結果,因此 `ZZZ = 1-i` ,從 $N-1$ 慢慢遞往下減
如果 $kx=N-\dfrac{(k-1)k}{2}$ 等式成立,且 $k$ 、 $x$ 為整數,則 $\dfrac{N-\dfrac{(k-1)k}{2}}{k}$ 必也為整數,那麼就可用 `x % k = 0` 來檢查條件是否成立