# 2022q1 Homework5 (quiz5) contributed by < `Kevin-Shih` > > [第 5 週測驗題](https://hackmd.io/@sysprog/linux2022-quiz5#測驗-1) ## 測驗 `1` **題目** 利用 [Sieve of Eratosthenes](https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes),嘗試找出第 1500 個回文質數,有效位數是 15。 預期執行結果為 `100191191191001`。 完整程式碼: [prime_palindrome.c](https://gist.github.com/Kevin-Shih/5dc5ac521875f56958721ba14f80f3a8) :::warning :warning: 不用複製原題目,專注於探討程式原理,包含你的推測及驗證,只要列出關鍵程式碼。善用 GitHub 或 gist 保存完整程式碼及相關測試。 :notes: jserv ::: :::info 收到,另外放在 gist 中。 :ballot_box_with_check: Kevin Shih ::: #### 運作原理與解題 部分較為重要的片段及本題答案 **以查表方式求 64-bit uint 的整數平方根** > 從 [第 3 週測驗題 `11`](https://hackmd.io/@sysprog/linux2022-quiz3#%E6%B8%AC%E9%A9%97-11) 改寫裡頭的整數開平方根實作,藉由 lookup table (LUT) 的手法降低分支的數量 --quiz5 題目敘述 ```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); } ``` :::warning TODO: 解釋上述程式碼的原理 :notes: jserv ::: **建構 bitmaps 用於進行 Sieve of Eratosthenes algorithm** ```c /* Generates a table of prime numbers up to the maximum having digits. * All even numbers (including 2) are ignored. Bit n in the bitmap * represents the odd number (2n + 1). This enables the table to be * half the size of a naive Sieve of Eratosthenes implementation. */ 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); // why not psieve[0] |= COMPOSITE unsigned char mask = 0x7; for (ull n = 3; n <= max; n += 2) { /* 若 n 是質數 */ if (((psieve[n >> 4] >> ((n >> 1) & mask)) & 0x1) == PRIME) { /* 所有篩選表中 n 的倍數所代表的 bit 均設為 COMPOSITE (從 2n 到 max 之間) */ 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] |= psieve[0] |= COMPOSITE << ((mul >> 1) & mask); /* bit offset */ } } } } ``` Sieve of Eratosthenes 是透過依序刪去表中質數的倍數,最後剩下的就是質數的一種搜尋質數的方式,由於 2 以外的偶數均不是質數,在建立篩選表時只保留奇數可以節省一半的空間。 這個 bitmap 的 max 是最大的回文的 isqrt,因為後續檢驗質數不需查大於 sqrt 值 (若有大於 sqrt 的因數必然有對應的小於 sqrt 的因數) :::warning TODO: 補上質數測試時,為何只要檢驗到 $\sqrt n$ 的數學證明。 :notes: jserv ::: 關於 bit offset ```graphviz digraph "reverse-bits" { node [shape=record]; rankdir=LR; a [label="{<0> 1 |<1> 3 |<2> 5 |<3> 7 |<4> 9 |<5> 11 |<6> 13 |<7> 15 }"]; b [label="{<0> 1 |<1> 1 |<2> 1 |<3> 0 |<4> 0 |<5> 0 |<6> 0 |<7> 0 }"] node [shape="plain"] "uint8_t psieve[0]"->a:0:w "mask" -> b:0:w } ``` `psieve` 陣列中的每個元素都可表示 16 個數 (只存奇數),因此 `n >> 4` 取得對應 n 所在的元素;第 `(n - 1) / 2` 個 bit 代表 n (即第 `n >> 1` bit),前面已經處理過 n 所在的元素,因此只取 `(n >> 1) % 8` 即可,這邊透過 `AND` 一個 0x7 的 mask 達成,最後 `AND` 0x1 取得最低為的 bit 即為代表 n 的 bit,而若是指派數值給 n 則找到 n 所在的元素,並將要給定的值右移 `(n >> 1) & mask` 後,指派到該元素。 **透過篩選表檢驗給定的回文是否為質數** ```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); next = prev + i; if (!(val % ((next << 1) + 1))) return false; quad &= ~(1ULL << i); } prev += 64; } return true; } ``` :::warning 不用列出 `EXP1` 和 `EXP2` :notes: jserv ::: :::info 留下答案已移到 gist 了。 (其實寫在這單純為了方便查閱、複習) Kevin Shih ::: 這個函式用來檢驗找出的回文是否為質數,對 `2`, `3`, `5` 是否可整除已經處理過了,後續迴圈內只要處理是否被其他質數整除即可。 - `prev`: 先前檢驗到這個 bit - `next`: 下一個要檢驗的質數對應 psieve 中的第幾個 bit (即檢驗 `val` 是否被 `next * 2 + 1` 整除) - `quad`: 先前建立的篩選表,每次取 64-bit 取 `NOT`,用於查找下一個要檢驗的質數,起始值再與 `~0b111` 做 `AND` 以忽略以另外處理的前三個奇數(`1`, `3`, `5`) `quad = ~*pquadbits` 得到的(部分)篩選表中 `0` 表示非質數或檢驗過的質數 `1` 表示尚未檢驗過的質數,如此每次只要取 ctz 就可以得到要檢驗的質數對應 psieve 中的第幾個 bit,檢驗後不論是否能整除都將該 bit 設為 1 以表示檢驗過了即可。 當 quad 為 0 時表示這 64-bit 中所有質數都已被檢驗過了,故將 prev 加 64 並進入下一次迴圈。 因 `quad` 為 ull 型態故 `EXP1` = `__builtin_ctzll(quad)`,而用於標註檢驗過的質數的 `EXP2` = `~(1ULL << i)`,即尾隨 `i` 個 0 就左移 `i` bit。 **產生下一個回文** ```c /* Generate the next palindrome after a palindrome * 123454321 -> 123464321 * 123494321 -> 123505321 * 123999321 -> 124000421 */ static ull getnextpalin(char *buf, const int *len) { midindex = (*len >> 1) - 1; /* Handle the case of odd number of digits. * If the central digit is 9, reset it to 0 and process adjacent digit, * otherwise increment it and return. */ if ((*len & 0x1) == 0x1) { if (buf[midindex + 1] - '0' != 9) { buf[midindex + 1] += 1; return atol(buf); } buf[midindex + 1] = '0'; } /* Adjust other digits */ for (; midindex >= 0; --midindex) { if (buf[midindex] - '0' != 9) { buf[midindex] += 1; buf[*len - midindex - 1] += 1; return atol(buf); } buf[midindex] = '0'; buf[*len - midindex - 1] = '0'; } /* If we have exhausted numbers in *len digits, increase the number of * digits and return the first palindrome of the form 10..0..01 */ *len += 2; /* The last palin will always be of the form 99...99 */ return atol(buf) + 2; } ``` 當位數不夠時應修改 `*len` 以觸發 main 中 refresh buffer 的部分。 **產生回文質數** ```c int main() { int count = LIMIT; ull i = 100000000000001; int len = 0; char *buf = ltoa(i, &len); if (len < N_DIGITS) { printf("len: %d\n", len); exit(1); } generate_sieve(N_DIGITS); /* We started at 1000000000001. */ while (1) { /* If number of digits are even, all palindromes are divisible by 11. * Get first palindrome of next odd number of digits. */ if (!(len & 0x1)) { i = 1ULL | fastpow10(len); buf = ltoa(i, &len); continue; } /* Check if number is divisible by 5 or 3 */ if ((buf[len - 1] != '5') && !is_divisible_by3(buf, len)) { if (isprime(i) && (--count == 0)) { printf("%s\n", buf); return 0; } } int oldlen = len; i = getnextpalin(buf, &len); /* Refresh buffer if number of digits has increased */ if (oldlen != len) buf = ltoa(i, &len); } free(psieve); return 0; } ``` 發現並不是找出第 1500 個回文質數,而是**第 1500 個 15 位數的回文質數**。 因為當 `LIMIT` 設為 1 時其輸出為 `100000323000001` 而非 `2`。 因為 `len` 一開始就是 15 位數,而非從個位數慢慢往上找(第一個回文 `i` 設為 `100000000000001`)。