# 2020q3 Homework3 (quiz3)
contributed by < `zzzxxx00019` >
> [2020q3 第 3 週測驗題](https://hackmd.io/@sysprog/2020-quiz3#2020q3-%E7%AC%AC-3-%E9%80%B1%E6%B8%AC%E9%A9%97%E9%A1%8C)
> [Github](https://github.com/zzzxxx00019/quiz3)
## 測驗 1
### 題目:
$-5≫^{arith}1=-3$
上述 $-3$ 正是 $\dfrac{-5}{2}$ 的輸出,並向 $−∞$ 進位。
針對有號整數的跨平台 ASR 實作如下:
```cpp
int asr_i(signed int m, unsigned int n)
{
const int logical = (((int) -1) OP1) > 0;
unsigned int fixu = -(logical & (OP2));
int fix = *(int *) &fixu;
return (m >> n) | (fix ^ (fix >> n));
}
```
請補完程式碼。
### 原理:
* 根據 C 語言規格書所述,對有號型態的物件進行算術右移歸屬於 [unspecified behavior](https://hackmd.io/@sysprog/c-undefined-behavior) ,程式行為==仰賴編輯器實作和平台特性==而定
>The result of E1 >> E2 is E1 right-shifted E2 bit positions.
…
If E1 has a signed type and a negative value, the resulting value is implementation-defined.
* 因此 logical 所做的事,就是確認目前的執行環境,是「邏輯右移」還是「算數右移」
* 透過 `(int) -1` 確認執行環境狀態,將 `-1` 右移一個 bit ,因此 OP1 為 `>>1`
* 邏輯右移:將最左的 bit 補 0 ,結果 > 0 ,`logical = 1`
* 算數右移:將最左的 bit 補上原本失去的符號,在此補上 1 ,結果 < 0 ,`logical = 0`
* 若執行環境為邏輯右移 ( `logical = 1` ),且` m < 0` ,就需要對最左 bit 補上有號狀態,因此可推測出 OP2 為 `m < 0`
* `*(int *) &fixu` 用意在於將 `fixu` 強制轉型為 `int` 型態
* 若環境為`邏輯右移`且 `m < 0` , `fix` 經強制轉型後為 `-1`,透過 `(fix ^ (fix >> n))` 保留下需補上的有號 bit ,確保與 `m >> n` 後的結果做 `or` 運算不會失真
**邏輯右移示意圖**
右移後最左 bit 空下來的部分補 0 ,導致輸出結果 > 1, `logical = true`
```graphviz
digraph
{
node[shape = record]
int[label = "(int) -1", shape=plaintext]
data_1 [label = "<f0>1|<f1>1|<f2>...|<f3>1|<f4>1"]
{rank = same ; int data_1}
shift[label = ">>1", shape=plaintext]
data_2 [label = "<f0>0|<f1>1|<f2>...|<f3>1|<f4>1"]
{rank = same ; shift data_2}
data_1:f0 -> data_2:f1
data_1:f1 -> data_2:f2
data_1:f2 -> data_2:f3
data_1:f3 -> data_2:f4
}
```
**算數右移示意圖**
右移後最左 bit 補上原本的算術符號,最後輸出結果 < 0 ,`logical = false`
```graphviz
digraph
{
node[shape = record]
int[label = "(int) -1", shape = plaintext]
data_1 [label = "<f0>1|<f1>1|<f2>...|<f3>1|<f4>1"]
{rank = same ; int data_1}
shift[label = ">>1", shape = plaintext]
data_2 [label = "<f0>1|<f1>1|<f2>...|<f3>1|<f4>1"]
{rank = same ; shift data_2}
data_1:f0 -> data_2:f1
data_1:f1 -> data_2:f2
data_1:f2 -> data_2:f3
data_1:f3 -> data_2:f4
}
```
如果執行環境試算數右移,那麼 logical = false , fixu = 0 ,因此這邊只針對邏輯位移特別討論,觀察程式是如何補上原本失去的有號狀態,這邊以 $-5≫^{arith}1$ 為例子:
`fixu = -(logical & (m < 0))`
```graphviz
digraph
{
node[shape = record]
logical [label = "(logical) 1", shape = plaintext]
data_1 [label = "<f0>0|<f1>0|<f2>...|<f3>0|<f4>1"]
{rank = same ; logical data_1}
check [label = "(m < 0) 1", shape = plaintext]
data_2 [label = "<f0>0|<f1>0|<f2>...|<f3>0|<f4>1"]
{rank = same ; check data_2}
data_1:f4 -> data_2:f4
and_result [label = " logical & (m < 0) ", shape = plaintext]
data_3 [label = "<f0>0|<f1>0|<f2>...|<f3>0|<f4>1"]
{rank = same ; and_result data_3}
data_2:f4 -> data_3:f4
result [label = "fixu = - ( logical & (m < 0) )", shape = plaintext]
data_4 [label = "<f0>1|<f1>1|<f2>...|<f3>1|<f4>1"]
{rank = same ; result data_4}
data_3:f0 -> data_4:f0
data_3:f1 -> data_4:f1
data_3:f2 -> data_4:f2
data_3:f3 -> data_4:f3
data_3:f4 -> data_4:f4
}
```
從上述過程可以觀察到,一旦邏輯右移與 `m < 0` 同時成立,則 `fixu = 0xff..f` ;反之,若其中一個條件不成立,則 `fixu = 0x0` ,而接下來 fixu 的工作,就是補上右移後所失去的有號狀態,以目前的例子來說明, `-5` 右移 `1 bit` 後,最左邊的 `bit` 會被補上 `0` 導致輸出結果為正數, `fix ^ (fix >> n)` 可將失去的有號狀態補回,再藉由 `or` 運算修正回正常結果;若 `fix` 為 `0` ,則以上運算出來結果並不影響 `m >> n` 的輸出,意即 `result | 0x0 = result`
**fix ^ ( fix >> n ) 運算示意圖**
```graphviz
digraph
{
node[shape = record]
int[label = "fix", shape = plaintext]
data_1 [label = "<f0>1|<f1>1|<f2>...|<f3>1|<f4>1"]
{rank = same ; int data_1}
shift[label = "fix >> 1", shape = plaintext]
data_2 [label = "<f0>0|<f1>1|<f2>...|<f3>1|<f4>1"]
{rank = same ; shift data_2}
data_1:f0 -> data_2:f0
data_1:f1 -> data_2:f1
data_1:f2 -> data_2:f2
data_1:f3 -> data_2:f3
data_1:f4 -> data_2:f4
result[label = " fix ^ ( fix >> 1 )", shape = plaintext]
data_3 [label = "<f0>1|<f1>0|<f2>...|<f3>0|<f4>0"]
{rank = same ; result data_3}
data_2:f0 -> data_3:f0
data_2:f1 -> data_3:f1
data_2:f2 -> data_3:f2
data_2:f3 -> data_3:f3
data_2:f4 -> data_3:f4
}
```
**(m >> n) | (fix ^ (fix >> n)) 修正輸出示意圖**
```graphviz
digraph
{
node[shape = record]
fix[label = " fix ^ ( fix >> 1 )", shape = plaintext]
data_1 [label = "<f0>1|<f1>0|<f2>...|<f3>0|<f4>0"]
{rank = same ; fix data_1}
result [label = "-5 >> 1", shape = plaintext]
data_2 [label = "<f0>0|<f1>1|<f2>...|<f3>0|<f4>1"]
{rank = same ; result data_2}
data_1:f0 -> data_2:f0
fix_result [label = "(m >> n) | (fix ^ (fix >> n))", shape = plaintext]
data_3 [label = "<f0>1|<f1>1|<f2>...|<f3>0|<f4>1"]
{rank = same ; fix_result data_3}
data_2:f0 -> data_3:f0
}
```
## 測驗 2
### 題目:
>Built-in Function: `int __builtin_ctz (unsigned int x)`
>* Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined.
>
>Built-in Function: `int __builtin_clz (unsigned int x)`
>* Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
我們可用 `__builtin_ctz` 來實作 LeetCode [342. Power of Four](https://leetcode.com/problems/power-of-four/),假設 int 為 32 位元寬度。實作程式碼如下:
```cpp
bool isPowerOfFour(int num)
{
return num > 0 && (num & (num - 1))==0 &&
!(__builtin_ctz(num) OPQ);
}
```
請補完程式碼。
### 原理:
* 假設 n 為 2 的倍數,且 n > 0 ,從二進制角度觀察, n 的最右個 bit 為 1 ,其他值為 0 ,則 n & n-1 必為 0
* 但題目要求為 4 的倍數,因此需要考慮到 n=2 的情況,若 n=2 ,則 `(__builtin_ctz(num) OPQ)` 不為 0
* 透過 n=4 與 2 代入,只有在 `OPQ=&0x1` 時,符合題目要求
### 延伸問題:
1. 改寫上述程式碼,提供等價功能的不同實作,儘量降低 branch 的數量
* 若 n 為 2 的倍數,以二進制表示下,只有其中一值為 1 ,其他皆全為 0
* 若 n 為 4 的倍數,在二進制表示下,一次會向右位移 2 格,因此透過 `__builtin_ctz(num)` 計算的結果,會皆為偶數
* 透過 `__builtin_popcount(num)` ,可知 `1-bits` 的個數
>Built-in Function: int __builtin_popcount (unsigned int x)
>* Returns the number of 1-bits in x.
```cpp=
bool isPowerOfFour(int num)
{
return __builtin_popcount(num) == 1 && ( __builtin_ctz(num)%2) == 0 ;
}
```
2. LeetCode [1009. Complement of Base 10 Integer](https://leetcode.com/problems/complement-of-base-10-integer/)
* 透過 xor 運算,做 bit 翻轉
* 利用 `__builtin_clz(N)` ,計算需運算 xor 的有效長度
* 根據 `__builtin_clz(N)` 說明, `N=0 is undefined` ,因此需額外考慮 N=0 的狀況
```cpp=
int bitwiseComplement(int N)
{
if(N==0) return 1 ;
return N^(0xffffffff >> __builtin_clz(N)) ;
}
```
![](https://i.imgur.com/rz39jFm.png)
3. LeedCode [41. First Missing Positive](https://leetcode.com/problems/first-missing-positive/)
* 因為此題是為了找出陣列中,未出現的最小正整數,因此可將數值安插在所屬的陣列位置,例如 `1` 放在 `nums[0]` , 3 放在 `nums[2]` 這樣的手法,再透過對陣列內容尋訪,若該位置 `nums[i]` 沒有放入數值,那麼最小值就是 `i+1` ,若陣列中每個位置都有對應的值,則最小值為 `NumsSize+1`
* 排序過程所需時間複雜度為 $O(n)$ ,而在尋訪陣列尋找最小值時, `wosrt case` 為 $O(n)$ ,即陣列中每個位置都有數值,因此在 `worst case` 下,整體時間複雜度為 $O(2n)$
* 排序方法說明:若 `nums[i]` 不為正整數,就不在排列目標名單中,直接將其值設為 0 ;如果 `nums[i]` 未超出 numsSize (可被安排於陣列內),且目標位置內尚未存放目標值,則做交換的動作,交換過程需考慮到同值交換 ( 程式碼10~16行 ) 而導致的無窮迴圈
* 以陣列 `[4,1,3,5,6]` 為例,最後排列的結果會為 `[1,0,3,4,5]` ,因此輸出結果為 `2`
```cpp=
int firstMissingPositive(int* nums, int numsSize)
{
for(int i=0 ; i<numsSize ; i++)
{
if(nums[i] > 0)
{
if (nums[i] < (numsSize+1) && (i+1) != nums[i])
{
int tmp = nums[i] ;
if(nums[i] == nums[tmp-1]) nums[i] = 0 ;
else
{
nums[i] = nums[tmp-1] ;
nums[tmp-1] = tmp ;
i-- ;
}
}
else if (nums[i] > numsSize) nums[i] = 0 ;
}
else nums[i] = 0 ;
}
for(int i=0 ; i<numsSize ; i++)
{
if(nums[i] == 0) return i+1 ;
}
return numsSize+1 ;
}
```
![](https://i.imgur.com/bHi5qwj.png)
## 測驗 3
### 題目:
針對 [LeetCode 1342. Number of Steps to Reduce a Number to Zero](https://leetcode.com/problems/number-of-steps-to-reduce-a-number-to-zero/),我們可運用 gcc 內建函式實作,假設 int 寬度為 32 位元,程式碼列表如下:
```cpp
int numberOfSteps (int num)
{
return num ? AAA + 31 - BBB : 0;
}
```
請補完程式碼。
### 原理:
* 在二進制表示法中,每個 1 最後都要進行 -1 的動作,每向右挪 1 bit 都要進行除 2 的動作,直到最後最後結果剩下 1 ,再做最後的 -1 ,過程有點類似透過「短除法」將十進制轉為二進制
* 透過 `31 - __builtin_clz(num)` 計算需要往右挪多少次
* 再透過 `__builtin_popcount(num)` 計算需要 `-1` 多少次
### 延伸問題:
1. 改寫上述程式碼,提供等價功能的不同實作並解說
* 透過 `(num|0x1)` 的動作,避免 `__builtin_clz()` 發生輸入為 0 的狀況,且不影響整體執行結果
```cpp
int numberOfSteps(int num)
{
return 31 - __builtin_clz(num|0x1) + __builtin_popcount(num) ;
}
```
2. 避免用到編譯器的擴充功能,只用 C99 特徵及 bitwise 運算改寫 [LeetCode 1342. Number of Steps to Reduce a Number to Zero](https://leetcode.com/problems/number-of-steps-to-reduce-a-number-to-zero/),實作出 branchless 解法,儘量縮減程式碼行數和分支數量
* 引用題目所提供的 `popcnt_branchless` 實作方法來取代原本的 `__builtin_popcount`
* 引用 [Bit Twiddling Hacks](http://graphics.stanford.edu/~seander/bithacks.html) 所提供的 **Find the log base 2 of an N-bit integer in O(lg(N)) operations** 方法來找尋從右邊開始數的最高位有效 bit ,且允許 0 的情況輸入
```cpp=
int log2N(int v)
{
int r; // result of log2(v) will go here
int shift;
r = (v > 0xFFFF) << 4; v >>= r;
shift = (v > 0xFF ) << 3; v >>= shift; r |= shift;
shift = (v > 0xF ) << 2; v >>= shift; r |= shift;
shift = (v > 0x3 ) << 1; v >>= shift; r |= shift;
r |= (v >> 1);
return r ;
}
int popcnt_branchless(uint32_t v)
{
uint32_t n;
n = (v >> 1) & 0x77777777;
v -= n;
n = (n >> 1) & 0x77777777;
v -= n;
n = (n >> 1) & 0x77777777;
v -= n;
v = (v + (v >> 4)) & 0x0F0F0F0F;
v *= 0x01010101;
return v >> 24;
}
int numberOfSteps (int num)
{
return popcnt_branchless(num) + log2N(num) ;
}
```
![](https://i.imgur.com/8Ua9LQV.png)
## 測驗 4
### 題目:
考慮以下 64-bit GCD (greatest common divisor, 最大公因數) 求值函式:
```cpp
#include <stdint.h>
uint64_t gcd64(uint64_t u, uint64_t v) {
if (!u || !v) return u | v;
int shift;
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
while (!(u & 1))
u /= 2;
do {
while (!(v & 1))
v /= 2;
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (XXX);
return YYY;
}
```
請補完程式碼。
### 原理:
* 透過一開始的程式碼,了解輾轉相除法的整個過程:
* 根據輾轉相除法的概念,當過程執行到 u 、 v 其中一值為 0 ,則最大公因數為不為 0 的那個數,因此可以得知 $gcd(a,0)=a$ 的特性, `if (!u || !v) return u | v` 此部分就是在確認 u 、 v 是有 0 存在
* 再來輾轉相除法是依循著 $gcd(a,b)=gcd(b,a$ $mod$ $b)$ 這個概念在進行,整個 `while` 迴圈就是在做這樣的重複計算,直到 `v = 0` 為止
```cpp=
#include <stdint.h>
uint64_t gcd64(uint64_t u, uint64_t v) {
if (!u || !v) return u | v;
while (v) {
uint64_t t = v;
v = u % v;
u = t;
}
return u;
}
```
* 回到最大公因數的概念,如果兩數都可被 2 連除,則最大公因數必參雜著 $2^n$
* 透過 `(u | v) & 1` 這個計算動作,確認 u 、 v 兩者皆為偶數,若其中 1 值為奇數,與 1 做 & 計算後會有不為 0 的結果,並透過 shift 這個變數,紀錄總共右移多少次,來得到兩數皆為 0 或產生至少一個奇數的計算結果
* 因為奇數與偶數無法做到整除的動作,因此透過 while 迴圈將判斷 u 是否為偶數,若是的話就進行除 2 的動作
* 再比較 u 、 v 大小,透過相減的方式,使 v < u ,而 else 的部分,則是 v =< u 的狀況,將 u - v,然後再與 v 對調
* 而輾轉相除法的結束條件就是其中一值被減為 0 ,因此 `XXX = v`
* 輾轉相除法的最後結果就如同一開始所述,為 $gcd(a,0)=a$ ,但一開始有做對 2 連除的動作,因此這邊補上連除後的結果,故 `YYY = u << shift`
### 延伸問題:
1. 在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升
* 藉由 `__builtin_ctz( u|v )` ,可快速計算出shift的值
* 將原本透過 `while` 計算出的 `shift` 改由 `__builtin_ctz( u|v )` 來執行 ( 5~11 行)
```cpp=
uint64_t gcd64(uint64_t u, uint64_t v) {
if (!u || !v) return u | v;
int shift;
shift = __builtin_ctz( u|v ) ;
if(shift)
{
u >>= shift ;
v >>= shift ;
}
if (!(u & 1))
u >>= __builtin_ctz(u) ;
do {
if (!(v & 1))
v >>= __builtin_ctz(v) ;
if (u < v)
{
v -= u;
}
else
{
uint64_t t = u - v;
u = v;
v = t;
}
} while (v);
return u<<shift;
}
```
因為 `__builtin_ctz()` 主要是在對一開始判斷是否存在著因數 $2^n$ 進行優化,因此在測試設計上,針對 u 、 v 都為偶數的狀態去做測試,並透過 [perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) 進行效能分析:
```gcc
int main()
{
for(int i = 0 ; i < 0xffff ; i+=2)
{
for(int j = 0 ; j < 0xffff ; j+=2)
{
gcd64(i, j) ;
}
}
return 0 ;
}
```
* 以原本的方法進行測試:
```
Performance counter stats for './perf_test.o':
15,0863.80 msec task-clock # 1.000 CPUs utilized
624 context-switches # 0.004 K/sec
23 cpu-migrations # 0.000 K/sec
45 page-faults # 0.000 K/sec
6258,6631,7894 cycles # 4.149 GHz
3804,6978,9838 instructions # 0.61 insn per cycle
976,2472,2490 branches # 647.105 M/sec
141,9692,1133 branch-misses # 14.54% of all branches
150.882867503 seconds time elapsed
150.864725000 seconds user
0.000000000 seconds sys
```
* 透過 `__builtin_ctz()` 取代 while 迴圈的方法測試:
```
Performance counter stats for './perf_test.o':
8,4535.58 msec task-clock # 1.000 CPUs utilized
303 context-switches # 0.004 K/sec
10 cpu-migrations # 0.000 K/sec
45 page-faults # 0.001 K/sec
3499,8308,2866 cycles # 4.140 GHz
2541,5180,7243 instructions # 0.73 insn per cycle
552,3677,8057 branches # 653.415 M/sec
51,5058,8256 branch-misses # 9.32% of all branches
84.536884443 seconds time elapsed
84.536043000 seconds user
0.000000000 seconds sys
```
:::info
可以發現,透過 `while` 的方式計算 shift ,在 `branch` 使用上,比 `__builtin_ctz()` 多了將近一倍的數量,這也是導致效能低落的主要原因。
:::
## 測驗 5
### 題目:
在影像處理中,[bit array](https://en.wikipedia.org/wiki/Bit_array) (也稱 `bitset`) 廣泛使用,考慮以下程式碼:
```cpp
#include <stddef.h>
size_t naive(uint64_t *bitmap, size_t bitmapsize, uint32_t *out)
{
size_t pos = 0;
for (size_t k = 0; k < bitmapsize; ++k) {
uint64_t bitset = bitmap[k];
size_t p = k * 64;
for (int i = 0; i < 64; i++) {
if ((bitset >> i) & 0x1)
out[pos++] = p + i;
}
}
return pos;
}
```
可用 clz 改寫為效率更高且等價的程式碼:
```cpp
#include <stddef.h>
size_t improved(uint64_t *bitmap, size_t bitmapsize, uint32_t *out)
{
size_t pos = 0;
uint64_t bitset;
for (size_t k = 0; k < bitmapsize; ++k) {
bitset = bitmap[k];
while (bitset != 0) {
uint64_t t = KKK;
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
}
return pos;
}
```
請補完程式碼。
### 原理:
先理解這個程式的目標是在實作什麼功能,有一個存放 `64-bits` 的 `array ( bitmap )` ,裡面共可存放 `bitmapsize` 個變數,而 `array` 是透過連續記憶體分配的方法去實作,在 `bitmap` 中,每一個數字占用 `64-bits` 的空間, `bitmap[0]` 區段為 0-63 , `bitmap[1]` 區段為 64-127 以此類推,而整個程式的運作目的是透過 `out` 這個陣列,去依序儲存在這連續的記憶體空間裡面,出現 `bit 1` 的位置,而 `pos` 則是回傳出現 `bit 1` 的數量。
舉個例子來說,輸入 `bitmap = [0,1]` ,會得到 `pos = 1` ,`out[0] = 64` 這樣的結果,因為第一個數字為 0 ,在前 64 個 bit 中,沒有任何 1 出現,而第二個數字為 1 ,因此在第二個數字個第一個 bit 為 1 ,對應的連續位置為 64 。
```graphviz
digraph
{
node[shape = record]
p0 [label = "0", shape = plaintext]
p64 [label = "64", shape = plaintext]
p128 [label = "128", shape = plaintext]
{rank = same ; p0 p64 p128}
//bitmap[label = "bitmap", shape = plaintext]
data [label = "<h0>|<f0> bitmap[0] |<h1>|<f1> bitmap[1]|<h2>|<f2> bitmap[2]|<h3>|<f3> . . . . . . "]
//{rank = same ; bitmap data}
p0 -> data:h0
p64 -> data:h1
p128 -> data:h2
}
```
* `naive` 運作流程解釋:
* 透過 `bitset` 來記錄當前所讀到的數值
* 透過 `p = k*64` 來計算目前數值所對應到的連續位置起始值
* 透過迴圈逐一右移再與 `1` 做 `&` 計算,確認是否為 `bit 1`
* 如果是的話,則將該位置 `p + i` 儲存到 `out` 陣列中
* `improved` 運作流程解釋:
* 透過 `__builtin_ctzll` 來計算當前的 `bitset` 中 `bit 1` 的位置,計算完成後,將該位置的 `bit` 清為 0 ,再繼續判斷 `bitset` 中是否還有 `bit 1` 存在
* 在二進制系統中,負整數的表示方式是透過將正整數的每個 bit 做反轉的動作,最後再加上 1 ,且有進位的動作
* 透過 `bitset & -bitset` 這樣的方法,可以將從右數來的第一個 `bit 1` 設為 `1` ,而其他 `bit` 經過運算後會全為 `0` ,因為經過翻轉後,原本 `0` 的數字會被翻為 `1` ,而原本右邊數來的第一個 `1` 則會被翻轉為 `0` ,透過 `+1` 且進位的運算,最後就會不斷進位直到將那個 `0` 設為 `1` 為止,而將那個 `bit` 設為 `1` 後,後面的 `bit` 則跟原本的 `bit` 全部相反,因此做 `&` 運算後會全為 `0` ,透過這個方法得到可以將從右數來的第一個 `bit 1` 設為 `0` 的 `mask`
* 最後再透過 `bitset ^= t` 將 `ctz` 指出的` bit` 清為 0 ,得以繼續執行 bit 檢查
* 因此 `KKK = bitset & -bitset`
:::info
**[Bloom filter](https://hackmd.io/@sysprog/2020-dict#%E4%BD%95%E8%AC%82-Bloom-Filter)**
透過特定的 `hash function` ,將對應的 `index` 設為 `1` ,當要檢查某個字串是否可能存在,透過檢查 `hash table` 中,對應的 `index` 狀況,若其中一個 `index` 不為 `1` ,就能很快地確定這個字串不出現在這個資料庫中。
對於 `bloom filter` 實作的方法,其中之一就是特過 `bitset` ,透過只記錄 `true` 跟 `false` 的 `array` 來當作 `hash table` 的方法,更改對應的 `array bit` ,來達到 `bloom filter` 的目的。
:::
### 延伸問題:
* 設計實驗,檢驗 clz 改寫的程式碼相較原本的實作有多少改進?
* 在 `ctz` 的改良版中,是針對 `while` 尋找 `bit 1` 做改進
* 透過比較 `naive` 的 `worst case` ,也就是透過代入 `0x8..0` ,如此在二進制中,將只有第 `64 bit` 為 `1` ,其他都為 `0` ,並反覆測試 `300` 次,再透過 `perf` 去分析兩種方法的執行過程
==naive==
```
Performance counter stats for './a.o':
0.25 msec task-clock # 0.514 CPUs utilized
0 context-switches # 0.000 K/sec
0 cpu-migrations # 0.000 K/sec
48 page-faults # 0.191 M/sec
99,4935 cycles # 3.963 GHz
79,9795 instructions # 0.80 insn per cycle
15,3597 branches # 611.736 M/sec
4332 branch-misses # 2.82% of all branches
0.000488485 seconds time elapsed
0.000499000 seconds user
0.000000000 seconds sys
```
==improved==
```
Performance counter stats for './a.o':
0.21 msec task-clock # 0.499 CPUs utilized
0 context-switches # 0.000 K/sec
0 cpu-migrations # 0.000 K/sec
46 page-faults # 0.221 M/sec
84,4957 cycles # 4.055 GHz
59,3354 instructions # 0.70 insn per cycle
11,4697 branches # 550.454 M/sec
4004 branch-misses # 3.49% of all branches
0.000417697 seconds time elapsed
0.000438000 seconds user
0.000000000 seconds sys
```
從中不難發現,在 `improved` 版本中, `instructions` 的數量明顯低於 `naive` 版本,因為 `naive` 是透過對每個 `bit` 做 `& 0x1` 的運算來檢查是否為 `bit 1` ,而對比於 `improved` ,則是透過 `ctz` ,直接找到 `bit 1` 的位置,藉此降低在 `branch` 與 `instrcution` 上的使用。