---
title: '2020q3 Homework3 (quiz3)'
tags: linux2020
---
# 2020q3 Homework3 (quiz3)
contributed by < `ChongMingWei` >
## 開發環境
```shell
$ uname -a
Linux cmw-System-Product-Name 5.4.0-47-generic #51~18.04.1-Ubuntu SMP Sat Sep 5 14:35:50 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
$ gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```
---
## 測驗 1
In some environment, we don't have arithmetic right shift. That is, negative number after `>>` operation will be positive.
So, we have to set bits which become `0` after the `>>` operation.
```c=
int asr_i(signed int m, unsigned int n)
{
const int logical = (((int) -1) >> 1) > 0;//OP1
unsigned int fixu = -(logical & (m<0));//OP2
int fix = *(int *) &fixu;
return (m >> n) | (fix ^ (fix >> n));
}
```
Check if the compiler supports arithmetic right shift.
If yes, `logical = 0`. Otherwise, `logical = 1`
```c=3
const int logical = (((int) -1) ) > 0;//OP1
```
`fixu` will be assigned `0xffffffff` if `logical = 1` and `m<0` (The environment doesn't support arithmetic right shift.)
(`fixu = 4294967295`)
```c=4
unsigned int fixu = -(logical & (m<0));//OP2
```
`fix` will also be assigned `0xffffffff`.
(With the datatype `int`, `fix = -1`)
```c=5
int fix = *(int *) &fixu;
```
`m >> n` will get n `0` bits. (From left to right)
`fix >> n` will also get n `0` bits. (From left to right)
`fix ^ (fix >> n)` will reverse all bits. And we will get n `1` bits(From left to right) and 32 - n `0` bits.
```c=6
return (m >> n) | (fix ^ (fix >> n));
```
:::success
2.練習實作其他資料寬度的 ASR,可參照[ C 語言:前置處理器應用篇 ](https://hackmd.io/@sysprog/c-preprocessor)撰寫通用的巨集以強化程式碼的共用程度;
:::
將原本 `int asr_i` 裡面的程式碼都改寫成 macro 並且能給予不同資料型態。
```c=
#define get_logical(datatype) (((datatype) -1) >> 1) > 0
#define get_fixu(logical, m) -(logical & (m < 0))
#define get_fix(datatype, fixu) (*(datatype *) &(fixu))
#define get_result(fix, m, n) (m >> n) | (fix ^ (fix>>n))
```
## 測驗 2
```cpp
bool isPowerOfFour(int num)
{
return num > 0 && (num & (num - 1))==0 &&
!(__builtin_ctz(num) & 0x1); //OPQ
}
```
### Analysis on return value
- 考慮是否為正數
```cpp
num > 0
```
- 考慮是否有多個 set bits (應當只有一個),也就是確認是否為` power of 2`。
> 但此種寫法會把0也算進去,應替換成 `num && !(num & (num - 1))`
```cpp
num & (num - 1))==0
```
- 考慮 set bit 是否在正確的位置上(應當要在 bit(2n) 的位置上):
- $4^n = 2^{2n}$,$2n$ 即為 `__builtin_ctz(num)` 會返回的數字,可以發現,只要**返回的數字是大於等於0的偶數**,就會是我們要的答案。
- 因此只要**返回的數字是大於等於2的偶數**,就會是我們要的答案,用 `&0x1` 來做檢查即可。
```cpp
__builtin_ctz(num) & 0x1
```
>測驗時沒有看到 `!` 且只想到要把 bit0 轉成1 (但是前面的 bits 也可能會有值!!),所以挑了 (g) 選項
:::success
2.改寫上述程式碼,提供等價功能的不同實作,儘量降低 branch 的數量;
:::
- `&& __builtin_popcount(num)==1`: 確認只有一個 set bit。
- `(num&0x55555555)`: 確認 set bit 在正確的位置上。
```c=
bool isPowerOfFour_m(int num)
{
return num > 0 && __builtin_popcount(num)==1 && (num&0x55555555);
}
```
:::success
3.練習[ LeetCode 1009. Complement of Base 10 Integer ](https://leetcode.com/problems/complement-of-base-10-integer/)和[ 41. First Missing Positive](https://leetcode.com/problems/first-missing-positive/),應善用 clz;
:::
### 1009. Complement of Base 10 Integer
- 想法: 將原本的 input 做 bit inverse ,但使用的是 int 的資料型態,所以做完 inverse 之後,要將和原本 input 無關的 bits 都設回0。
- 如果是0就返回1
- 如果是其他數字,計算 mask: `~((0xfffffffe) << (31-__builtin_clz(N)))` ,
假設現在 N = 5,那麼 mask 會得到`0x000000007` ,
使用此 mask 和 `~N` 做 `AND` 操作,可以將和 input 無關的 bits 設回0。
```c=
int bitwiseComplement(int N){
return N==0?1:~N&~((0xfffffffe) << (31-__builtin_clz(N)));
}
```
### 41. First Missing Positive
> 參考 [LEETCODE 41. First Missing Positive 解题思路分析](https://leetcode.jp/leetcode-41-first-missing-positive-%E8%A7%A3%E9%A2%98%E6%80%9D%E8%B7%AF%E5%88%86%E6%9E%90/)
- 想法: 假設 ^1^ `nums` 裡面的數字是1- `numsSize` ,那麼要回傳 `numsSize+1`,否則^2^回傳的數字一定會落在[1, `numsSize`]的區間內。
- 第一個 for loop: 所以使用一個長度為 `numsSize` 的 array ,若 `index+1` 的數字在 `nums` 中有出現過,那就將 array[index] 設為1。
- 第二個 for loop: 檢查 array ,第一個出現不為1的 index ,將其+1後就是我們要求的數字,如果都有1的話,那就是前面提到的第一種情況,回傳 `numsSize+1`
```c=
int firstMissingPositive(int* nums, int numsSize){
int *tmp = malloc(sizeof(int)*numsSize);
for (int i = 0;i < numsSize; ++i){
if(nums[i]>0&&nums[i]<=numsSize)
tmp[nums[i]-1] = 1;
}
for (int i = 0;i < numsSize; ++i){
if(tmp[i]!=1)
return i+1;
}
return numsSize+1;
}
```
:::success
4.研讀[ 2017 年修課學生報告](https://hackmd.io/@3xOSPTI6QMGdj6jgMMe08w/Bk-uxCYxz),理解 clz 的實作方式,並舉出[ Exponential Golomb coding ](https://en.wikipedia.org/wiki/Exponential-Golomb_coding)的案例說明,需要有對應的 C 原始程式碼和測試報告;
x-compressor 是個以 Golomb-Rice coding 為基礎的資料壓縮器,實作中也用到 clz/ctz 指令,可參見[ Selecting the Golomb Parameter in Rice Coding](https://ipnpr.jpl.nasa.gov/progress_report/42-159/159E.pdf)。
> [x-compressor](https://github.com/xbarin02/x-compressor)
:::
## 測驗 3
- 如果是0,直接返回數值0
- 如果非0,那麼就要計算減1的次數和除以2的次數,其中前者和後者分別為:
- 1的總個數 `__builtin_popcount(num)`,以及
- MSB 之後的 bits 數量 `31 - __builtin_clz(num)`
```cpp
int numberOfSteps (int num)
{
return num ? __builtin_popcount(num) + 31 - __builtin_clz(num) : 0; //AAA BBB
}
```
:::success
2.改寫上述程式碼,提供等價功能的不同實作並解說;
提示:[ Bit Twiddling Hacks ](http://graphics.stanford.edu/~seander/bithacks.html)中提及許多[ bitwise operation ](https://hackmd.io/@sysprog/ByzoiggIb)的使用,如 bit inverse、abs 等等,是極好的參考材料。
:::
> 參考 [2017q3 Homework4 (改善 clz)](https://hackmd.io/@3xOSPTI6QMGdj6jgMMe08w/Bk-uxCYx)
```c=
int numberOfSteps (int num)
{
/* Compute population count */
uint32_t popcount = num;
uint32_t tmp;
tmp = (popcount>>1)&0x77777777;
popcount -= tmp;
tmp = (tmp>>1)&0x77777777;
popcount -= tmp;
tmp = (tmp>>1)&0x77777777;
popcount -= tmp;
popcount += (popcount >> 4);
popcount &= 0x0f0f0f0f;
popcount *= 0x01010101;
popcount >>= 24;
/* Compute clz */
uint32_t n = 32, c = 16, l, r = num;
do {
l = r >> c;
if (l) {
n -= c;
r = l;
}
c >>= 1;
} while (c);
return num ? popcount + 31 - (n - r) : 0; //AAA BBB
}
```
:::success
3.避免用到編譯器的擴充功能,只用 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 解法,儘量縮減程式碼行數和分支數量;
:::
```c=
int numberOfSteps (int num){
int numofminus=0;//要做-1的次數
int numofdiv=0;
while(num){
int isodd = num&0x1;//確認目前 bit0 是否為1
int iseven = (num&0x1)^0x1;//確認目前 bit0 是否為0
numofminus += isodd;
numofdiv += iseven;
num -= (isodd-iseven)>0;//是奇數做-1
num >>= (iseven-isodd)>0;//是偶數做/2
}
return numofminus+numofdiv;
}
```
## 測驗 4
考慮以下 64-bit GCD (greatest common divisor, 最大公因數) 求值函式:
```c=
#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;
}
```
改寫為以下等價實作:
```c=
#include <stdint.h>
uint64_t gcd64_modified(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 (v); //XXX
return u<<shift; //YYY
}
```
- 檢查 `u` 和 `v` 是否都是偶數,直到一方不為偶數為止,可以取得最後要**向左 shift 回來的次數**
> E.g.
> A = 2^n^\*a, B = 2^n+m^\*b, a and b are odd
> GCD(A, B) = GCD(a, 2^m^\*b) * 2^n^
> n即為此處計算的 **shift**
```cpp=5
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
```
- 如果 `u` 還是偶數,那就把他除以2直到他變奇數為止,因為當一數為奇數且另一數為偶數時,偶數中不管有幾個因數2,都不會成為兩數最大公因數的因數
> E.g.
> A = 2^n^\*a, B = b, a and b are odd
> GCD(A, B) = GCD(2^n^\*a, b) = GCD(a, b)
> n即為此處計算的 **shift**
```cpp=8
while (!(u & 1))
u /= 2;
```
- Line 11-12: 和上述 `u` 相同的操作,確認 `v` 為奇數
- Line 13-14: 使用了性質 `GCD(A, B) = GCD(A, B-A) (when B > A)`
> 證明參考:
> https://blog.csdn.net/CS171_chengl/article/details/104967588
> Proof 1:
> **If GCD(A, B) = 1, then GCD(A, B - A) = 1**
> Because A and B are coprime, B = q * A + r (r not equal to 0)
> So, B - A = (q - 1) * A + r
> Finally, GCD(A, B - A) = 1.
> Proof 2:
> **GCD(A, B) = GCD(A, B-A)**
> Suppose GCD(A, B) = d
> A = A' * d, B = B' * d, B-A = (B' - A') * d
> GCD(A, B-A) = GCD(A' * d, (B' - A') * d) = d * GCD(A', (B' - A'))
> Because **GCD(A', (B' - A')) = 1** (*by Proof 1*), d * GCD(A', (B' - A')) = d
> So GCD(A, B) = d = GCD(A, B-A)
- Line 15-18: 使用上述相同性質,只是我們需要 `v=0` 來做為迴圈跳出條件,所以要交換位置
```cpp=10
do {
while (!(v & 1))
v /= 2;
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (v);
```
- `u` 最後要再左移 `shift`
```cpp=21
return u<<shift;
```
:::success
在 x86_64 上透過 `__builtin_ctz` 改寫 GCD,分析對效能的提升;
:::
```c=
uint64_t gcd64_improved(uint64_t u, uint64_t v) {
if (!u || !v) return u | v;
int shift;
shift = __builtin_ctz(u) > __builtin_ctz(v)?__builtin_ctz(v):__builtin_ctz(u);
u >>= shift;
v >>= shift;
u >>= __builtin_ctz(u);
do {
v >>= __builtin_ctz(v);
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (v);
return u<<shift;
}
```
### 效能分析
實驗設計三種演算法: `gcd64`, `gcd64_modified`, `gcd64_improved` ,分別對相同的兩數做計算,兩數使用亂數產生,計算取 1000 次亂數後得到的平均執行時間如下:
| `gcd64` | `gcd64_modified` | `gcd64_improved` |
|:-------:|:----------------:|:----------------:|
| 1022 ns | 1727 ns | 915 ns |
## 測驗 5
訊號 or 影像處理會用到 (平行化?)
在影像處理中,[bit array](https://en.wikipedia.org/wiki/Bit_array) (也稱 bitset) 廣泛使用,考慮以下程式碼:
>此程式碼是用來計算,`bitmap` 中,set bit 的總個數
```c=
#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 = bitset & -bitset;//KKK
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
}
return pos;
}
```
原本的for 迴圈,需要遍歷64個 bit,修改為以下的while 迴圈之後,看`bitmap` 內set bit 有幾個,就跑幾次迴圈就好。
在 Line 9 的地方,我們使用變數`t` 來存放 LSB,經過 `bitset & -bitset` 的操作之後,只會留下 LSB ,其他的 bit 都會被設為 `0`,如此一來,就可以使用 `XOR` 的操作來將當前的 LSB 消除。 (Line 12)
```c=8
while (bitset != 0) {
uint64_t t = bitset & -bitset;//KKK
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
```
:::success
延伸問題:
2.設計實驗,檢驗 clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 [bitmap density](http://www.vki.com/2013/Support/docs/vgltools-3.html)
:::
### 實驗設計
- 根據不同的 bitmap density ,計算分別需要多少時間:
![](https://i.imgur.com/tOumcVZ.png)
這邊的密度是以32-bit 為一組單位,而我們使用的數字資料型態為 `uint64_t` ,測試的時候會重複兩次,換算成16進位數字如下:
| Bitmap density | Hex |
| -------- | -------- |
| `BITMAP_DOT` | `0xc0c0c0c0c0c0c0c0` |
| `BITMAP_DASH` | `0xf0f0f0f0f0f0f0f0` |
| `BITMAP_DOTDASH` | `0xf0c0f0c0f0c0f0c0` |
| `BITMAP_LDASH` | `0xfcfcfcfcfcfcfcfc` |
| `BITMAP_DOTLDASH` | `0xfc30fc30fc30fc30` |
| `BITMAP_DOTDOT` | `0xcccccccccccccccc` |
| `BITMAP_DOTDOTLDASH`| `0xfcccfcccfcccfccc` |
| `BITMAP_LLDASH` | `0xfff0fff0fff0fff0` |
- 執行時間結果如下:
| Bitmap density | Original (unit:ns) | Modified (unit:ns)|Speed up|
| -------- | -------- | -------- |--------|
| `BITMAP_DOT` | 1233 | 565 |2.18x|
| `BITMAP_DASH` | 1877 | 865 |2.17x|
| `BITMAP_DOTDASH` | 1651 | 736 |2.24x|
| `BITMAP_LDASH` | 1555 | 1066|1.16x|
| `BITMAP_DOTLDASH` | 1785 | 776 |2.3x |
| `BITMAP_DOTDOT` | 1083 | 556 |1.95x|
| `BITMAP_DOTDOTLDASH`| 932 | 767 |1.22x|
| `BITMAP_LLDASH` | 1240 | 880 |1.41x|
:::success
延伸問題:
3.思考進一步的改進空間;
:::
### 平行化設計
> 參考 [簡易 Pthreads 平行化範例與效能分析](https://tigercosmos.xyz/post/2020/07/simple-pthread-usage/)
以下程式碼,對 bitset 的部分做平行化。
```c=
//TODO 考慮bitset 數量不能被thread 數量整除的情況
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NUMTHRDS 1
#define MAGNIFICATION 300
//先考慮整除的情況,每個thread分配到的都是整數
uint64_t *a;
uint32_t *b;
typedef struct
{
int thread_id;
int start;
int *pos;
int bitmapsize;
} Arg; // 傳入 thread 的參數型別
pthread_t callThd[NUMTHRDS]; // 宣告建立 pthread
pthread_mutex_t mutexsum; // pthread 互斥鎖
// 每個 thread 要做的任務
void *people_count(void *arg)
{
Arg *data = (Arg *)arg;
int thread_id = data->thread_id;
int start = data->start;
int bitmapsize = data->bitmapsize;
int *pos = data->pos;
uint64_t bitset;
for (size_t k = start; k < bitmapsize; ++k) {
bitset = a[k];
while (bitset != 0) {
uint64_t t = bitset & -bitset;//KKK
int r = __builtin_ctzll(bitset);
pthread_mutex_lock(&mutexsum);
b[*pos] = k * 64 + r;
*pos=-(~*pos);//*pos = *pos + 1
pthread_mutex_unlock(&mutexsum);
bitset ^= t;
}
}
pthread_exit((void *)0);
}
int main()
{
a = malloc(sizeof(uint64_t)*MAGNIFICATION);
for(int i =0;i<MAGNIFICATION;++i)
a[i] = 2*i+5;
b = malloc(sizeof(uint32_t)*2048);
// 初始化互斥鎖
pthread_mutex_init(&mutexsum, NULL);
// 設定 pthread 性質是要能 join
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
// 每個 thread 都可以存取的 PI
// 因為不同 thread 都要能存取,故用指標
int *pos = malloc(sizeof(int));
*pos = 0;
int part = MAGNIFICATION / NUMTHRDS;
Arg arg[NUMTHRDS]; // 每個 thread 傳入的參數
for (int i = 0; i < NUMTHRDS; i++){
// 設定傳入參數
arg[i].thread_id = i;
arg[i].start = part * i;
arg[i].pos = pos; // PI 的指標,所有 thread 共用
arg[i].bitmapsize = part * i + part;
// 建立一個 thread,執行 people_count 任務,傳入 arg[i] 指標參數
int temp;
if((temp=pthread_create(&callThd[i], &attr, people_count, (void *)&arg[i]))!= 0) {
printf("can't create thread: %d\n",temp);
return 1;
}
}
// 回收性質設定
pthread_attr_destroy(&attr);
void *status;
for (int i = 0; i < NUMTHRDS; i++) {
// 等待每一個 thread 執行完畢
pthread_join(callThd[i], &status);
}
// 所有 thread 執行完畢,印出結果
printf("result = %d \n", *pos);
// 回收互斥鎖
pthread_mutex_destroy(&mutexsum);
// 離開
pthread_exit(NULL);
return 0;
}
```
> 參考 [Lock-free 程式設計議題](https://hackmd.io/@sysprog/lock-free-prog?fbclid=IwAR1qQIDI5er1cbHQfgGw13B1ozIxDHTahxWF3aHC-bPu4kaxpfv360Q-zaQ)
:::warning
不要忽視 mutex lock 的成本,你嘗試平行化之前,應該適度切割 (partitioning) 數值範圍。
:notes: jserv
:::