# 2022q1 Homework5 (quiz5)
contributed by < [Nomad1230](https://github.com/Nomad1230) >
## 測驗 `1`
### 解釋程式碼原理
#### 建立質數表
:::spoiler generate_sieve
```c=
static void generate_sieve(int digits)
{
ull max = 0;
for (int count = 0; count < digits; ++count)
max = max * 10 + 9;
max = isqrt(max);
half_max = max >> 1;
/* We need half the space as multiples of 2 can be omitted */
int bytes = (max >> 1) + (max & 0x1);
/* Calculate the actual number of bytes required */
bytes = (bytes >> 3) + (bytes & 0x1);
bytes = ALIGN(bytes, 16); /* Align-up to 16-byte */
psieve = realloc(psieve, bytes);
if (!psieve) {
printf("realloc() failed!\n");
exit(1);
}
memset(psieve, 0, bytes);
/* In psieve bit 0 -> 1, 1 -> 3, 2 -> 5, 3 -> 7 and so on... */
/* Set the 0th bit representing 1 to COMPOSITE
*/
psieve[0] |= COMPOSITE << (1 >> 1);
unsigned char mask = 0x7;
for (ull n = 3; n <= max; n += 2) {
if (((psieve[n >> 4] >> ((n >> 1) & mask)) & 0x1) == PRIME) {
for (ull mul = (n << 1); mul < max; mul += n) {
/* Skip the evens: there is no representation in psieve */
if (!(mul & 0x1))
continue;
/* Set offset of mul in psieve */
psieve[mul >> 4] |= COMPOSITE
<< ((mul >> 1) & mask); /* bit offset */
}
}
}
}
```
:::
此段程式碼基於 Sieve of Eratosthenes 演算法來建立質數表,參數 `n` 代表欲搜尋之有效位數區間。
第 3 ~ 16 行決定質數表的大小,首先根據有效位數取出此區間最大的數值作為 `max` ,例如本題要計算有效位數為 15 的區間,則 `max` 為 10 ^ 15 - 1 ,再來將 `max` 取 isqrt ,因為在進行試除法時只需判斷到 $\sqrt{N}$ 。
:::info
在進行試除法時會將目標數 $N$ 對 1 ~ $\sqrt{N}$ 一一進行試除,若整除則代表 ${N}$ 有一個小於 $\sqrt{N}$ 及一個大於 $\sqrt{N}$ 的因數,因此不用再對大於 $\sqrt{N}$ 的數進行試除。
:::
此時 `max` 為預期的質數表大小,但由於所有的偶數中只有 2 為質數,故可以把所有偶數忽略不計,因此再將 `max` 除以 2 。
最後將 `max` 除以 8 就是要分配給記憶體的 bytes 數。
在分配完空間之後便是要將質數填入表中以方便查詢,第 27 ~ 43 行的程式從 3 ~ `max` 進行試除,若試除成功則將其所有的倍數設成 COMPOSITE。
值得注意的是質數表的儲存方式,一個 `psieve` 指標所指向的值可以儲存 16 個數的質數表,因為偶數忽略不計,剩下 8 個奇數的 state 以 bitwise 的方式儲存在 `uint8_t` 的變數中,如圖所示:
```graphviz
graph graph1{
label = "psieve[0]"
node [shape = record]
num1 [label = "1|3|5|7|9|11|13|15"]
}
```
```graphviz
graph graph2{
label = "psieve[1]"
node [shape = record]
num1 [label = "17|19|21|23|25|27|29|31"]
}
```
故 `psieve` 的索引值都需先 >> 4 ,儲存質數表,且在儲存質數表時都要以 `|=` 運算子來儲存特定的 bit 。
#### 用查表輔助 64 bit 開方根
:::spoiler isqrt
```c=
/* isqrt64_tab[k] = isqrt(256 * (k + 65) - 1) for 0 <= k < 192 */
static const uint8_t isqrt64_tab[192] = {
128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142,
143, 143, 144, 145, 146, 147, 148, 149, 150, 150, 151, 152, 153, 154, 155,
155, 156, 157, 158, 159, 159, 160, 161, 162, 163, 163, 164, 165, 166, 167,
167, 168, 169, 170, 170, 171, 172, 173, 173, 174, 175, 175, 176, 177, 178,
178, 179, 180, 181, 181, 182, 183, 183, 184, 185, 185, 186, 187, 187, 188,
189, 189, 190, 191, 191, 192, 193, 193, 194, 195, 195, 196, 197, 197, 198,
199, 199, 200, 201, 201, 202, 203, 203, 204, 204, 205, 206, 206, 207, 207,
208, 209, 209, 210, 211, 211, 212, 212, 213, 214, 214, 215, 215, 216, 217,
217, 218, 218, 219, 219, 220, 221, 221, 222, 222, 223, 223, 224, 225, 225,
226, 226, 227, 227, 228, 229, 229, 230, 230, 231, 231, 232, 232, 233, 234,
234, 235, 235, 236, 236, 237, 237, 238, 238, 239, 239, 240, 241, 241, 242,
242, 243, 243, 244, 244, 245, 245, 246, 246, 247, 247, 248, 248, 249, 249,
250, 250, 251, 251, 252, 252, 253, 253, 254, 254, 255, 255,
};
/* integer square root of a 64-bit unsigned integer */
static ull isqrt(ull x)
{
if (x == 0)
return 0;
int lz = __builtin_clzll(x) & 62;
x <<= lz;
uint32_t y = isqrt64_tab[(x >> 56) - 64];
y = (y << 7) + (x >> 41) / y;
y = (y << 15) + (x >> 17) / y;
y -= x < (uint64_t) y * y;
return y >> (lz >> 1);
}
```
:::
首先可以看到定義了 `isqrt64_tab` 來儲存 `isqrt(256 * (k + 65) - 1)` for 0 <= k < 192 的結果,看到第 25 、 26 行中在查表之前已經先把 `x` left shift 掉 clz & 62 的個數,若 `x` 的 clz 為奇數,則 shift 完最左邊兩 bits 為 01 ,為偶數則 shift 完最左邊的 bit 為 1 ,為甚麼要做這樣的分別呢?
:::info
在查表時是取 `x` 最左邊的 8 bits 作為 index ,此 index 必為 64 以上,故 table 只需儲存 256 - 64 = 192 個元素。
:::
原因就出在回傳值的部分:
```c
return y >> (lz >> 1);
```
一開始取出 clz 是為了將數值都對齊左側方便接下來直式開方法的運算,所以最後計算出的結果 `y` 要右移 (clz / 2) 個位數(因為開方所以除2),假設 clz 的值為 27 ,由於開方前先把數值乘上了 $2^{27}$ ,所以最後結果應該要右移掉 $2^{13.5}$ ,這樣會造成計算上的問題,故多用了一個變數 `lz` 且使其必為偶數,來確保運算中的正確性與便利性。
第 26 ~ 29 行做的是直式開方法,值得注意的是這邊一次取 8 bits 為單位在做開方法。
先看到一般十進位的直式開方法流程:
![](https://i.imgur.com/DtAlyxj.png)
[圖源](https://highscope.ch.ntu.edu.tw/wordpress/?p=51628)
![](https://i.imgur.com/AhsZPK9.gif)
以上圖為例,若要做 144 的開方,先把面積減掉邊長為 10 的正方形,剩下 44 的面積為兩個矩形及一個小正方形,以數學公式來看就是 $2ab + b^2 = b(2a + b) = 44$ 。
這時再選定小正方形的長度使得小正方形 + 兩個矩形的面積逼近 44 ,若面積還有剩餘則再選定一個更小的正方形一直迭代下去,最後所有正方形加起來的長度就會逼近於開方根。
![](https://i.imgur.com/igZi5DU.png)
從以上流程可以寫出直式開方法的演算法流程:
(1).先求最高位數的平方根 a
(2).選定一數 b 使得 b(2a + b) 接近但不超過餘數
(3).若餘數不為 0 則重複第二步
再來看到程式碼,可以看出其是將 64 位元的數切成 8 等份,每一份都是一個 byte 的大小,因此可以看成是 256 進位的 8 位數的直式開方法。
第 26 行為步驟 1 , 第 27 、 28 行為步驟 2 的迭代。
以第 27 行為例:
```c
y = (y << 7) + (x >> 41) / y;
```
`y` 在第 26 行的結果是最高位的 byte 的開方根,
#### 質數判斷
:::spoiler isprime
```c=
/* Check if a number is prime. */
static bool isprime(const ull val)
{
if (!(val & 0x1)) /* Test for divisibility by 2 */
return false;
ull *pquadbits = (ull *) psieve;
ull next = 3; /* start at 7 (i.e. 3 * 2 + 1) */
for (ull quad = ~*pquadbits & ~0b111, prev = 0; prev <= half_max;
quad = ~*++pquadbits) {
if (!quad) {
prev += 64;
continue;
}
while (quad) {
ull i = __builtin_ctzll(quad); //EXP1;
next = prev + i;
if (!(val % ((next << 1) + 1)))
return false;
quad &= ~(1ULL << i); //EXP2;
}
prev += 64;
}
return true;
}
```
:::