# 2020q3 Homework5 (quiz5)
contributed by < `sammer1107` >
###### tags: `進階電腦系統理論與實作`
==[測驗題目](https://hackmd.io/@sysprog/2020-quiz5)==
# 測驗 1
```c=
#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;
}
```
+ 首先,==5~6== 判斷如果被除數為 0 或是除數為 1 ,就可以直接返回答案了。
+ 再來,第 ==7== 行的 `od` 判斷除數是否為奇數
+ 如果是偶數,我們可以把除術與被除數都除以二,答案也是一樣的
+ 如果是奇數,我們沒辦法把奇數除以二,所以變成把 `slot` 加一再除以二。如此的話,除出來的結果會比較小,之後需要加回來。
+ 根據上面,第 ==8== 行答案應該是 `divop(orig / 2, od ? (slots + 1) >> 1 : slots >> 1)`
+ 第 ==9~10== 判斷如果除數為奇數,那就把多除的部份補回來。因為
$$
\begin{align}
\frac{a}{b} =& \frac{a}{b+1}\frac{b+1}{b} \\
=& \frac{a}{b+1}(1+\frac{1}{b}) \\
=& \frac{\frac{a}{2}}{\frac{b+1}{2}} + \frac{\frac{a}{2}}{\frac{b+1}{2}} \frac{1}{b}
\end{align}
$$
所以我們要補加回來的部份就是 $\frac{\text{result}}{b}$ 也就是 `divop(result, slot)`
# 測驗 2
原題目:
:::spoiler
```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)
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;
}
```
:::
+ ==37~42==: 這裡一並處理數字為 0 或數字為負的情況。
+ 若原數字為負,則轉成 int 來看仍會是負的(都可用最高位判斷); 若原數字為 +0 ,則轉成 int 來看也是 0。所以 ==37== 用 `<= 0` 來判斷。
+ ==38~39== 進一步處理 $\pm 0$ 。把 sign bit 設為 0 若等於 0,即可一次處理兩種情況。
+ 剩下的非 0 負數,則透過產生 NaN 處理。
+ ==44~47==: 這裡要處理 NaN 與 Inf,兩者在 exponent 的部份皆是 255,所以要透過 `& 0x7F800000` 來看 exponent 是不是全 1 。這部份正確的寫法應該是
```c
if ((ix0 & 0x7f800000) == 0x7f800000) {
/* sqrt(NaN) = NaN, sqrt(+INF) = +INF,*/
return x;
}
```
+ ==48~60==: 這裡把指數的部份單獨取出來做處理,因為我們已經去掉負數的情況,所以可以直接 `>> 23` 來取得指數。
```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] */
```
+ ==50~54==: 如果指數部份皆為 0 ,代表數字在 subnormal 模式。我們要重新以類似 normal 的方式表示這個數,方便後續處理。所以我們把 `ix0` 向左移,直到 mantissa 中有 1 overflow 到 exponent 中,並以負數紀錄下相對的指數在 m 中。
+ ==55==: 這裡我們把 bias 補回來,`m` 現在表示真實的指數。
+ ==56==: 這裡我們透過 `& 0x007fffff` 只留下 mantissa 的部份,然後再 `| 0x00800000` 來補回隱藏的 1 ,如此現在 `ix0` 就反映出實際的值了,像 fixed point 的感覺。
+ ==57~60==: 開根號的時候指數的部份其實就是除以二(`>> 1`),但當 `m` 為奇數時,無法被 2 整除,因此我們要把 `ix0` 乘以二,如此指數減 1 ,現在就可以直接用 shift 做除以 2 了。
+ ==62~76==: 這裡用的開根號方式類似[手算十進位開根號](https://www.youtube.com/watch?v=uIrjN2Onn8M&ab_channel=AnilKumar)的方式,只不過改成是在 2 進位做。
+ 觀察 `ix0` 這裡所代表的值,在 ==56== 之後會佔用 24 bit,再經過 ==58,63== 行之後最多佔用 26 bit。
+ 假設有個二進位小數為 `x.xx...` ,則平方後小於 $2^2 = 100.00...$ 所以小數點前最多兩個 bit。依此類推得到開根號後長度應該減半,與十進位中的規則類似。如此我們可以像原本開根號的方式一樣將被開根數兩兩切一段,並逐段處理來算出開根號。
+ 因為 `ix0` 是 25 bit 或 26 bit,所以第一個處理的位置是 bit 25 (從 1 開始數),**對應到 `r = 0x01000000` 這個答案**。之後一次要跳兩格,但是為了計算方便,==74~75== 把 `ix0 << 1` 然後 `r >> 1` 所以和跳兩格是等效的。
+ 我們已知 `r` 代表目前正在處理的 bit 位置,==68,69== 行做的事情就是測試下一個開出來的值是不是 1 ,如果是就:
+ 把 `2r` 加到 `s0` (==68,70== 總共加了兩個 r),這對應到 10 進位中把最右邊的數字再乘以 2 的動作。
+ 把左邊數(`t`)與新的 bit ($=1$)的乘積從現有數 (`ix0`) 扣掉
+ 把這個 bit 加到答案之中 (`q += r`)
```c=68
t = s0 + r;
if (t <= ix0) {
s0 = t + r;
ix0 -= t;
q += r;
}
```
+ 如果下一個值開出來是 0,則不會進 if ,直接換下個位置即可。
+ 原本轉成 fix point 格式後,小數點之後有 23 個 bit,但是 ==63== 行相當於把小數點往上移了一個位置。故出了回圈之後,`q` 應該會是一個 25 bit 的數字,其中 24 個 bit 皆在小數點底下,這多出來的一個 bit,讓我們之後可以做 rounding。
+ rounding: 這裡我們透過把 1 加減很小的數來判斷系統使用的 rounding mode。
+ 如果減了之後小於 1 ,代表系統是 round down,而因為 `ix0 > 0`,所以我們目前的答案已經是偏小的,所以不需再做動作。多餘的 1 等一下就會被 shift 掉。
+ 如果減了之後等於 1 ,且加了之後大於 1,說明系統是 round up,因此直接進位到最低位 (`+2`)
+ 如果加減都回到 1 ,說明系統是 **round to nearest?** 此時如果最低位為 1 ,就進位。
+ ==90~91==: 做完 rounding 之後,要把 mantissa 與 exponent 再次合併,所以**要把 `m` 再 `<< 23` 回去他的位置**。要注意的是這裡的 `m` 並沒有 bias。所以 ==90== 行要把 `m` 從 2 補數轉到 bias representation。
+ 我們把 m 看成 8 bit signed,則轉換方式就是把 `127=0b011111111` 加回去。但`q >> 1` 事實上還有一個 1 在 bit 24,**所以答案是 `0x3f000000`**。
+ 這裡還要注意 m 可能是負的,所以 `+ m << 23` 的動作可能會影響到 float 的 sign bit,但是因為 `m` 不可能是 -128 所以一定會 overflow 把 float sign bit 變回 0。
## LeetCode Sqrt
利用一樣的原理,我們一樣可以做整數開根號
```c=
int mySqrt(int x){
uint32_t pos, t=0, ans=0, s0=0;
if (x <= 1)
return x;
pos = 1 << ((31 - __builtin_clz(x)) & (~1));
while (pos) {
ans <<= 1;
t = s0 | pos;
if (t <= x) {
s0 = t + pos;
x -= t;
ans |= 1;
}
pos >>= 2;
s0 >>= 1;
}
return ans;
}
```
+ ==7==: 這裡先利用 clz 來決定開根號的起始位置。相當於兩個兩個分段後,從最高位那段開始。
+ e.g. 假如 `x = 10011` 則 `pos = 10000`,`x = 110010` 則 `pos = 10000`。
+ ==9~19==: 這裡與原本的方法差不多,除了:
+ 我改成不 shift `x`,而是 shift `s0` 與 `pos`(原本的 `r`)。因為當數字很大的時候,左移 `x` 有可能會 overflow。
+ 再來紀錄答案的方式我改成設定最低 bit 再左移的方式。因為現在 `pos` 一次移動兩格,也不方便透過 `pos` 來設定 bit 了。
![](https://i.imgur.com/MZZhnva.png)
# 測驗 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 += ZZZ;
if (x % i == 0)
ret++;
}
return ret;
}
```
從數學式
$$
kx = N - \frac{k(k-1)}{2}
$$
看,$\frac{k(k-1)}{2}$ 其實就是 $1+2+\ldots+(k-1)$。而 `i` 其實就對應到 $k$。`x` 裡面存放的就是 $N - \frac{k(k-1)}{2}$,所以從 `x=N` 開始,每次減 $k-1$ 即可,對應到程式碼的 `x += 1-i`。檢查有沒有解就是等式右邊是否能被 $k$ 整除,對應到程式碼的 `x % i == 0`。