2020q3 Homework3 (quiz3)
===
contribute by `Liao712148`
* 1.從新回答[第3周測驗題](https://hackmd.io/@sysprog/2020-quiz3)
###### tags: `進階電腦系統理論與實作`
### 測驗1
```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));
}
```
>“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==.”
因為在 C 語言中,對有號型態的物件進行算術右移,歸屬於==unspecified behavior==所以要先判斷,此環境在對有號數作算是右移時,是會根據`most significant bit`補充,還是會補0。
* 若是會根據`most significant bit`補充,則`m>>n`即為答案。
* 若是僅會補充0,
* 如果`m>0`則`m>>n`即為答案。
* 如果`m<0`則要將`m>>n`補充的0改為`1`才行
由第`3`行可以確認,此環境對於有號數算數右移是否會根據`most significant bit`補充
`logical=1`表示僅會補充0,`logical=0`表示會根據`most significant bit`補充
所以`OP1`要選擇`>>1`
如果`m>0`則不用考慮有號數為移的==undefined havior==
如果`m<0`且算數右移僅會補充0,則要對補充的0轉換成1
所以`OP2`要選擇`m<0`
所以在需要做修正的時候,`fixu`會是0x11111111,否則為0x00000000,再透過```| (fix ^ (fix >> n))```,就可以把右移的位元都補成`1`,以符合所求。
#### 延伸問題
* 練習實作其他資料寬度的 `ASR`,可參照 [C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor) 撰寫通用的巨集以強化程式碼的共用程度;
參考[RinHizakura](https://hackmd.io/@RinHizakura/SkjPS8vBP#%E6%B8%AC%E9%A9%97-1)
C11 提供了 _Generic 選擇,用來模擬泛型程式,其本質是類似 switch 的選擇陳述,不過是編譯時期根據型態來選擇展開的對象。並且減少相似的程式
```cpp=
#define asr_i(m,n) \
_Generic((m), \
int8_t : asr_i8, \
int16_t : asr_i16, \
int32_t : asr_i32, \
int64_t : asr_i64 \
)(m,n)
#define asr(type) \
const type logical = (((type) -1) >> 1) > 0; \
u##type fixu = -(logical & (m < 0)); \
type fix = *(type *) &fixu; \
return (m >> n) | ((fix >> n) ^ fix)
int8_t asr_i8(int8_t m, unsigned int n)
{
asr(int8_t);
}
int16_t asr_i16(int16_t m, unsigned int n)
{
asr(int16_t);
}
int32_t asr_i32(int32_t m, unsigned int n)
{
asr(int32_t);
}
int64_t asr_i64(int64_t m, unsigned int n)
{
asr(int64_t);
}
```
### 測驗2
```cpp=
bool isPowerOfFour(int num)
{
return num > 0 && (num & (num - 1))==0 &&
!(__builtin_ctz(num) OPQ);
}
```
此題要判斷輸入的數是否是4的冪次($4^n$)可以看成$2^{2n}$,其中==n為正整數==,所以我們先從數字是不是2^{2n}下手,透過觀察可以發現$2^{2n}$在其數值的most significant bit position之後必須全為才有可能會是$2^{2n}$。
所以透過``(num & (num - 1))==0``可以判斷是我們上述的假設,舉例來說:
若``num=16``
$num=0010\ 0000_2$ $num-1=0001\ 1111_2$兩者作`&`可以滿足上述的判斷式
若``num=17``
$num=0010\ 0001_2$ $num-1=0010\ 0000_2$兩者作`&`不會滿足上述的判斷式
現在已經確定數字會是2的冪次,再來判斷是否是4的冪次,由數學式可以得知$2n$必須要是$>0$的偶數。利用`__builtin_ctz(num)`算出數字尾端0的個數如果是奇數則不會是4的冪次,例如``num=8``
`__builtin_ctz(num)會回傳3`
如果是偶數則會是4的冪次`num=16`則`__builtin_ctz(num)會回傳4`
所以``OPQ``要選可以判斷奇偶數的`&0x1`
#### 延伸問題
* 改寫上述程式碼,提供等價功能的不同實作,盡量降低branch的數量
在return的表示式,要先判斷`num`是否大於0,以及`(num & (num - 1))==0`是否成立,如果可以利用`int __builtin_popcount (unsigned int x)`來判斷`1`的個數,則可以透過以下的方式將上面檢查2個條件的判斷式縮減為以下
```cpp=
bool isPowerOfFour(int num)
{
return __builtin_popcount(num) == 1 &&
!(__builtin_ctz(num) & 01);
}
```
這樣就只要判斷2個條件即可。
* 練習 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](https://leetcode.com/problems/complement-of-base-10-integer/)
我的想法是利用`XOR`將數字0,1進行翻轉,舉例如下:
以`int`8位元舉例,`N=5`轉成2進位後
`00000101`和`11111111`作`XOR`可以得到`11111010`,再對`11111000`作`XOR`將bit轉成0即可
```cpp=
int bitwiseComplement(int N){
return N ? ((1 << (32 - __builtin_clz(N))) - 1) ^ N : 1;
}
```
因為當`N=0`時`__builtin_clz(N)`是`undefined`所以將`N=0`獨立出來處理
>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==.
*針對[41. First Missing Positive](https://leetcode.com/problems/first-missing-positive/)
以下提供一個簡單粗暴的方法,我們可以宣告一個陣列`array`,來記錄`nums`中存在哪一些元素,將`nums`中的正數,依照其數值大小存放入`array`陣列中的相應為位子,並且將`nums`中大於`numsSize`的正數捨棄,因為比`numsSize`大的正數一定不會是我們要找的`nums`中缺失的最小正數。`array`完成後就可以快速地找出`nums`中缺失的最小正數。
```cpp=
int firstMissingPositive(int* nums, int numsSize){
int array[300] = {0};
for (int i = 0; i < numsSize; i++) {
if (nums[i] > 0 && nums[i] <= numsSize)
array[nums[i] - 1] = 1;
}
int i;
for (i = 0; i < numsSize; i++) {
if (array[i] == 0)
break;
}
return ++i;
}
```
附上在 leetcode 中執行正確且通過的證明:
![](https://i.imgur.com/w1jrtO0.png)
:::warning
需要設法減少`array`的儲存空間
:::
---
### 測驗3
```cpp=
int numberOfSteps (int num)
{
return num ? AAA + 31 - BBB : 0;
}
```
以下是參考`Noahnut`同學的想法
根據題目的規定,我們可以發現將一個不為0的偶數不斷作算數右移,最後
`least siginificant bit oisition`會是`1`(奇數),所以要多作一個`-1`的動作,使得該數值變成`0`則運算結束,或是不為0的偶數則回到上一步驟。
所以一個數字要完成題目的規定,可以簡化成`1`的個數(可以計算出要作幾次`-1`的動作)+將最高位的`1`作運算右移,推至`least significant bit position`所需要的次數。
因為題目規定`num`不為負數,所以`num`>`0x80000000`,可以將最高位的`1`作運算右移,推至`least significant bit position`所需要的次數,改成`31-__builtin_clz(num)`
`AAA`選擇`__builtin_popcount(num)`
`BBB`選擇`__builtin_clz(num)`
#### 延伸問題
* 改寫上述程式碼,提供等價功能的不同實作並解說
* 避免用到編譯器的擴充功能,只用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`解法,盡量縮減程式碼行數和分支數量
利用測驗3提供的方法可以避免使用編譯器擴充功能,來計算數值在二進位制中`1`的個數。
```cpp=
unsigned popcnt_branchless(unsigned v) {
unsigned 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 naive(unsigned v) {
int i = 0;
while (v >> i)
++i;
return i;
}
int numberOfSteps (int num) {
int temp = popcnt_branchless(num);
temp += naive(num);
return num ? --temp : 0;
}
```
附上在 leetcode 中執行正確且通過的證明:
![](https://i.imgur.com/aFjONCZ.png)
---
### 測驗4
```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;
}
```
* **Binary GCD**
>binary gcd 的精神就是,當兩數是偶數時,必有2的因子
$gcd(x,y)=2\times\gcd(\dfrac{x}{2},\dfrac{y}{2})$
且一數為奇數,一數為偶數($y$),則無2因子
$gcd(x,y)=2\times\gcd(x,\dfrac{y}{2})$
如果兩數皆是奇數則
$gcd(x,y)=2\times\gcd(\dfrac{|x-y|}{2},y)$
直到$x=y$或$x=0$,此時GCD為$2^k\times y$, k為兩個偶數出現的次數
將觀念帶入此題,因為如果兩個數都是偶數,則他們會有2的因式,所以透過`5,6,7`行,將兩個數2的因式取出。執行完`5,6,7`行後會是一奇一偶,或兩個奇數,因為一奇一偶不會有2的因式所以要將偶數2的因數除掉。
`10~20`行是輾轉相除法,將大數不斷減去小數,直到有一個數先為`0`則停止。所以`XXX`選`v`
,再將兩奇數的因式提出,並乘上`5,6,7`行算出的2的因式,即為所求。所以`YYY`選`u<<shift`。
#### 延伸問題
* 在`x86_64`上透過`__builtin_ctz`改寫`GCD`,分析對效能的提升
---
### 測驗5
```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;
}
```
由`8,9,10`行的expression可以知道,我們要把bitset中數值為1的位子找到
```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;
}
```
此題`8~13`實現上面`8,9,10`的功能,所以我們利用不斷的將最靠近`least significant bit`的$1$消掉,這樣就可以透過`__builtin_ctzll`算出下一個$1$的位子。
根據[statck overflow](https://stackoverflow.com/questions/12818978/meaning-of-number-number)
可以知道
>Assuming $2's$ complement (or that $i$ is unsigned), $-i$ is equal to $~i+1$. $\ i$ & $\ (~i + 1)$is a trick to extract the lowest set bit of $i$.
所以`KKK`要選擇`bitset&-bitset`
#### 延伸問題
* 設計實驗,檢驗`clz`改寫的程式碼相較原本的實作有多少改進?應考慮到不同的[bitmap density](http://www.vki.com/2013/Support/docs/vgltools-3.html)
* 思考進一步的改進空間