---
tags: Linux2020
---
# 2020q3 Homework5 (quiz5)
contributed by < `YLowy` >
## 測驗 1 fdiv.c
考慮到以下浮點數除法程式: (fdiv.c)
```c=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;
}
```
假設 `divop()` 的第二個參數必為大於 0 的整數,而且不超過 ` int ` 型態能表達的數值上界。
### 在欣賞程式碼前可以先看看本程式欲實作概念 :
當我想要進行除法時
以浮點數 $X$ 除以整數 $Y$
1. 在二進位中我們可以很簡單的觀察整數 $Y$ 有無 $2$ 的因數,觀察最後一位即可,且只要右移即可完成除以 2 操作。
2. 浮點數 $X$ 則可以透過將 $exp$ $-1$ 很快速的將其除 2 。
3. 如果除數為偶數,也就是最後一個位元 $=0$ ,透過將除數與被除數都一起除以 $2$ 可以得到相同結果。
e.g. div(8,4) = div(4,2)
4. 最核心問題為如何處理除數為奇數的情況?
a) 這邊使用的做法為先將 除數 $+1$ 也就是先得到 $X ÷(Y+1)$ 這個數字,$Y+1$ 為偶數,我們可以用上述提到的方法。
b) 我們希望的目標為 $X ÷ Y$,算得 $X ÷(Y+1)$ 數值之後,我們需要補上其誤差。我們可以在 $Y$ 為大於 $0$ 整數前提下確定 $X ÷(Y+1)$ $<$ $X ÷ Y$ ,也就是我們需要在 $X ÷(Y+1)$ 值加上誤差值。
c) 誤差值計算 :
>$(原)$ ${ - }$ $(修正 (除數 +1)$
$X ÷ Y$ ${ - }$ $X ÷(Y+1)$
$=$ $\dfrac{X}{Y}$ - $\dfrac{X}{Y+1}$
$=$ $\dfrac{X(Y+1)}{Y(Y+1)}$ - $\dfrac{XY}{(Y+1)Y}$
$=$ $\dfrac{X}{Y(Y+1)}$
我們可以發現 $\dfrac{X}{Y+1}$ 這個結果就是我們在 a) 步驟得到的值
也就是說誤差值就是原先到的 $\dfrac{result}{Y}$ ,$Y$ 為原先傳入函式的除數。
### 有了以上概念我們可以非常輕鬆的觀察程式碼 :
```c=
#include <stdio.h>
#include <stdlib.h>
double divop(double orig, int slots) {
if (slots == 1 || orig == 0)
return orig; /*遞迴終止條件,除數為 1 或者 被除數為 0*/
int od = slots & 1;/*除數為奇數或者偶數以決定之後是否加入誤差值*/
double result = divop(orig / 2, od ? (slots + 1) >> 1 : slots >> 1);
/*奇數偶數分別的處理情況,也是 2,3 小點想處理的事情*/
if (od)
result += divop(result, slots); /* c) 小點的誤差值計算*/
return result;
}
```
:::info
除數部分最後一定會有遞迴停止點 : 7 -> 4 -> 2 -> 1
我覺得挺有意思的地方是當處理 "回傳無限循環小數" 的情況,搭配 GDB 可以觀察到整體函數運算過程
:::
### 延伸問題
1. 編譯器最佳化的角度,推測上述程式碼是否可透過 tail call optimization 進行最佳化,搭配對應的實驗
2. 比照 [浮點數運算和定點數操作](https://hackmd.io/@NwaynhhKTK6joWHmoUxc7Q/H1SVhbETQ),改寫上述程式碼,提出效率更高的實作,同樣避免使用到 FPU 指令 (可用 gcc -S 觀察對應的組合語言輸出),並與使用到 FPU 的實作進行效能比較
## 測驗 2 $\sqrt{2}$
延續 從 [√2](https://hackmd.io/@sysprog/2020-quiz5) 的運算談浮點數,假設 float 為 32-bit 寬度,考慮以下符合 IEEE 754 規範的平方根程式,請補上對應數值,使其得以運作:
```c=
#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)
```
避開 dangling else 的巨集 [參考](https://stackoverflow.com/questions/1067226/c-multi-line-macro-do-while0-vs-scope-block)
```c=24
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);
```
sign 為判斷正負之 mask
`EXTRACT_WORDS(ix0, x);` 可以想成 ix0 有與輸入 x 相同的記憶體,但是是以 int 形態存在。
``` c=34
/* 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;
}
```
處理 輸入為`無限大` `NAN` `負數` 的情況
```c=46
/* 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;
```
到目前為止可以確定輸入為 >0 的浮點數值
F : 23 bits
E : 8 bits
第`46`行 : m 會紀錄 E 的值
第`47`~`51`行 : 如果 m = 0 的情況,則為處理 Denormalize floating number 的操作 可以透過 bitwise 移動取得該值
第`53`行 : [IEEE754](https://en.wikipedia.org/wiki/IEEE_754) 所定義該浮點數的指數值
```graphviz
digraph D {
node_B [shape=record label="{0}|{E}|{E}|{E}|{E}|{E}|{E}|{E}|{E}|{F}|{F}|{F}|{F}|{F}|{F}|{F}|{F}|{F}|{F}|{F}|{F}|{...}|{...}|{F}|{F}"];
}
```
第`54`行 : 處理 Denormalize floating number 其格式,透過在 m 存下原本 E 值,ix0 存下 normalize 表示式,透過這個方法讓每個值都可以已 normalize floating number 格式處理。
也就是說原本Denormalize ${ 0.F.... * 2^E}$ 該 $F$ 的部分最高為會被推向 $E$ ,而在 ix0 中的 $E$ 會以 00...01 表示,但是真實的值會放在 m 中
```graphviz
digraph D {
node_B [shape=record label="{0}|{0}|{0}|{0}|{0}|{0}|{0}|{0}|{1}|{F}|{F}|{F}|{F}|{F}|{F}|{F}|{F}|{F}|{F}|{F}|{F}|{...}|{...}|{F}|{F}"];
}
```
我們可以表示該數字為 ${(-1)^0 * 1.F.... * 2^m}$
第`59`行 :而我們目標為進行數字 N 的方均根,以指數的角度 : ${\sqrt[2]{2^m}}$ = ${2^{m/2}}$
並會在第`88`行位移後寫回 ix0 EE....EE中。
```c=55
if (m & 1) { /* odd m, double x to make it even */
ix0 += ix0;
}
m >>= 1; /* m = [m/2] */
```
之後操作會在 m 格式為偶數情況下操作,所以如果 m 非偶數就和本身的 ix0 借個 2。
為何這麼做?
我們可以很簡單的計算 ${ \sqrt{100} = 10}$ ,${ \sqrt{10000} = 100}$,在這裡 m 可以想成 0的數量。
```c=60
/* 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;
}
```
這邊得思考剩餘部分如何處理
透過牛頓逼近法
${ \sqrt[n]{N} ≒ \dfrac{(n-1)*a+\dfrac{N}{a^{n-1}}}{n} = a_{1}}$
可以透過多次逼近取得更接近的值
n=2 的情況時 :
${\sqrt{N} ≒ \dfrac{(2-1)*a+\dfrac{N}{a^{2-1}}}{2} = \dfrac{a+\dfrac{N}{a}}{2}}$
:::info
[快速反均方根](http://www.hsfzxjy.site/2016-08-03-uncover-the-secret-of-fast-inverse-square-root-algorithm/) 雷神之槌 中程式碼也是有利用到這點。
```c=
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
```
:::
#### 開均方根直式
這裡用到開均方根直式,以下以 625(1001110001) 方均根後得值 25(11001) 為例子
```
n bits 的數字 N ,開根號後只會為 n/2 bits
從最右邊開始左右兩個兩個一組
---------------
√ 10|01|11|00|01
在 X Y 處放進相同數值讓 X位置的數字 * Y那坨數字 為接近且小於的被除數
X
---------------
√ 10|01|11|00|01 (XY選擇放入 1 )
Y -> 1←
---------------
√ 10|01|11|00|01
1← 1 1
----------------
1
第一輪做完之後繼續第二輪,一直做到最後一位
1 X
---------------
√ 10|01|11|00|01
1 1
----------------
1 01 (XY選擇放入 1)
-> 1 1←
10Y ---------------
√ 10|01|11|00|01
1 1
原本Y * 2 ----------------
↘ 1 01
101← 1 01
-----------------
↙ 0
1 1 X
---------------
√ 10|01|11|00|01
1 1
----------------
1 01
101 1 01
----------------
0 11
110Y
(注意只有最後的 Y *2)
(也就是上輪10"1" 這個 1*2)
↘
1 1 0
---------------
√ 10|01|11|00|01
1 1
----------------
1 01
101 1 01
----------------
11
1100 00
----------------
11
↙
1 1 0 X
---------------
√ 10|01|11|00|01
1 1
----------------
1 01
101 1 01
----------------
11
1100 00
----------------
11 00
1100Y ↘
1 1 0 0
---------------
√ 10|01|11|00|01
1 1
----------------
1 01
101 1 01
----------------
11
1100 00
----------------
11 00
11000 00 00
----------------
↙ 11 00
1 1 0 0 X
---------------
√ 10|01|11|00|01
1 1
----------------
1 01
101 1 01
----------------
11
1100 00
----------------
11 00
11000 00 00
----------------
11 00
11000Y
↘ 1 1 0 0 1
---------------
√ 10|01|11|00|01
1 1
----------------
1 01
101 1 01
----------------
11
1100 00
----------------
11 00
11000 00 00
----------------
11 00 01
110001 11 00 01
----------------
0
最上面的 1 1 0 0 1 便是 √ 1001110001 的結果
也是就是 25
```
熟悉以上流程回到程式碼的部分 :
1. **最上面的 q 為所得之方均根答案**
ix0 代表被方均根數,逐步扣除以逼近 0
程式碼第70行 q += r; 以及
第63行 `r = 0x01000000;`
第73行 `r >>= 1;`
使其位移累加完成方均根之答案
2. **左側 藍色框框如下為程式碼中第 68 行 `s0 = t + r;` 之行為,r會再每次判斷後向右為位移,t 為當輪欲扣除的數。**
![](https://i.imgur.com/nL65ttZ.png)
3. **第 67 行中的 if 判斷為決定能不能進行對 ix0 能不能扣除 X * Y那坨數字(也就是 t ),而 X 只可能為 0 或者 1 ,所以第 69 行程式碼 `ix0 -= t;` 讓 ix0 扣除逼近。**
4. **程式碼中的第62行 第72行 都寫有到 `ix0 += ix0;` 讓整個被方均根數直接向左位移一位,透過這個方法可以不用移動 t 以及 s0。**
```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);
}
}
ix0 = (q >> 1) + 0x3f000000;
ix0 += (m << 23);
INSERT_WORDS(z, ix0);
return z;
}
```
1. 最後一位小數會進行捨入,方法為透過 `z = one - tiny`,
`static const float one = 1.0, tiny = 1.0e-30;`,`z > one`判斷 rounding
2. float 可表達的範圍約3.4e-38~e.4e-38
這裡這樣實作原因我不曉得,不過對於操作 rounding 方法大致就 [這些](https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules)
選擇自己希望的 rounding rule 即可
3. 最後把算得的數字拼回去,並且打包成 float 型態就可以回傳了。
### 延伸問題
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 指令的實作
利用牛頓逼近法計算輸出 nhat 的每一個 bit 是 0 或者 1。
```c=
int mySqrt(int v)
{
unsigned long temp, nHat = 0, b = 0x8000, bshft = 15;
do{
if (v >= (temp = (((nHat << 1) + b) << bshft--)))
{
nHat += b;
v -= temp;
}
} while (b >>= 1);
return nHat;
}
```
![](https://i.imgur.com/TozITQl.png)
參考資料 : [快速整数平方根算法](https://andy1314chen.github.io/2017/06/28/FastSqrt/)
## 測驗 3 Consecutive Numbers Sum
[LeetCode 829. Consecutive Numbers Sum ](https://leetcode.com/problems/consecutive-numbers-sum/)
先參考 [這裡](https://hackmd.io/@sysprog/2020-quiz5#%E6%B8%AC%E9%A9%97-3)
```c=
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;
}
```
::: info
看起來超簡單
上課有提到跟著題目走就可以知道怎麼做,可是看到程式的我覺得
![](https://i.imgur.com/JvBY1gr.png)
:::
### 程式概念
直接從例子做起,以 N = 9 為例子 :
第一個最容易看到的為數量為 1 ,也就是 9
觀察表格以及其對應公式 (n>0,n位任意數) ,而我們只需要考慮 0~N 的範圍即可。
> e.g. 若 k=2 ,連續兩個數字之和我們可以發現為 1+2,2+3,3+4,4+5,...
> 也就是表格中的 k=2 該列 : 3,5,7,9,....
```graphviz
digraph D {
node_B [shape=record label="{連續數字的數量(k)|1|2|3|4|5|6|...}|{{對於該數量 k 可能 N 的值}|{1|2|3|4|5|6|7|8|9}|{03|05|07|09|11|13|15|17}|{06|09|12|15|18|21|24}|{10|14|18|22|26|30}|{15|20|25|30|35}|{21|27|33|39}|{....}}|{公式: func(n) =|0+1*n|1+2*n|3+3*n|6+4*n|10+5*n|15+6*n|{...}}"];
}
```
我們只要進行 for 迴圈對 "k" 檢查,也就是(連續之數量)
而判斷是否存在存在連續數量之正整數只需要觀察公式是否符合,
程式碼實作 :
```c=
int consecutiveNumbersSum(int N)
{
if (N < 1)
return 0; /*若是輸入0 回傳0*/
int ret = 1; /*每一個 N 都存在 1*N,所以保存 1 個結果*/
int x = N;
for (int i = 2; i < x; i++) {
x += 1-i; /* 這行是為了應對公式的 "XX" + k*n */
/* 這邊比較特別會在下面解釋 */
if (x % i == 0) /* i可以觀察表格公式該列 */
ret++; /* k 個連續整數加總應要符 */
} /* 合 XX + k*n (這裡i=k) */
return ret;
}
```
對於 `x += 1-i;` 我們可以觀察迴圈在對 N 檢查每個 k 。
參考最右行所列舉,檢查 N 是否有連續數值和 : (粗字體為程式碼中的 -(i-1) )
>(N - **1**) % 2
>((N - 1) -**2**) % 3
>(((N - 1) -2) -**3**) % 3
>..... 以此類推
### 延伸問題
1.嘗試改寫為更有效率的實作