# 2022q1 Homework2 (quiz2)
contributed by < [`Pepitaw`](https://github.com/Pepitaw) >
###### tags: `linux2022`
:::danger
注意書寫規範!
:notes: jserv
已修正
:::
## 測驗 1
### 解釋上述程式碼運作的原理
```clike
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a + b) / 2;
}
```
- a + b 可能會有 overflow ,超出 uint32_t 可表示的範圍
```clike
#include <stdint.h>
uint32_t average(uint32_t low, uint32_t high)
{
return low + (high - low) / 2;
}
```
- 取平均可以視為將較小的加上兩數差取平均
```clike
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (a & b & 1);
}
```
- a 除以 2 , b 除以 2 相加,再加上補償
- 此補償為判斷 a 與 b 的 lsb 是否皆為 1 ,若是就要加 1 ,反之則加 0
- 解答為 a & b & 1
```clike
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) >> 1);
}
```
- a & b 為 a 跟 b 相加完會進位的,再加上進位後再右移(除以二)bit 不需要再改變位置
- a ^ b 要將不會進位的 bit 做補償,因此還要再右移(除以二)
### 組合語言輸出與解讀
```clike
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (a & b & 1);
}
```
:::spoiler 用 godbolt 觀察的非編譯器最佳化
```
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov eax, DWORD PTR [rbp-4]
shr eax
mov edx, eax
mov eax, DWORD PTR [rbp-8]
shr eax
add edx, eax
mov eax, DWORD PTR [rbp-4]
and eax, DWORD PTR [rbp-8]
and eax, 1
add eax, edx
pop rbp
ret
```
```
逐行翻
- 將 register rbp 放到 stack
- 將 rsp assign 給 rbp
- 將 edi 放入 rbp pointer 裡,記憶體位置是 rbp - 4
- 將 esi 放入 rbp pointer 裡,記憶體位置是 rbp - 8
- 將 rbp - 4 的記憶體位置 assign 給 eax
- 將 eax 右移一個 bit
- 將 eax assign 給 edx
- 將 rbp - 8 的記憶體位置 assign 給 eax
- 將 eax 右移一個 bit
- eax 與 edx 相加存入 edx
- 將記憶體位置是 rbp - 4 、記憶體位置是 rbp - 8 與 1 做 and 存入 eax
- 將右移好的 edx 與 做完 and 的 eax 相加
- 將 register rbp 移出 stack
- 返回
```
- 將記憶體位置 rbp - 4 的 register 做右移
- 再將記憶體位置 rbp - 8 的 register 做右移,並相加存入 edx
- 將 rbp - 4 、 rbp - 8 的 register 與 1 做 and 存入 eax
- 最後 eax 與 edx 相加
:::
用 `gcc -S -O3 *.c` 達到編譯器最佳化
```
movl %edi, %eax
movl %esi, %edx
andl %esi, %edi
shrl %eax
shrl %edx
andl $1, %edi
addl %edx, %eax
addl %edi, %eax
ret
```
- 將 edi 與 esi 做 and 存入 edi
- 再將 edx 與 esx 分別做右移
- edi 與 1 做 and
- 最後將 edx, eax, edi 相加
```clike
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) >> 1);
}
```
```
movl %edi, %eax
andl %esi, %edi
xorl %esi, %eax
shrl %eax
addl %edi, %eax
ret
```
- 將 edi 與 esi 做 and 存入 edi
- 將 esi 與 eax 做 xor 再做右移
- 再全部加起來
#### 差異
- 此兩種寫法,後者用到的 operator 較少,所需的動作較少
- 另外後者用 mov 的次數也較少,不會像前者需要足夠的 register 做運算而需要將值 mov 到其他 register 暫放
---
## 測驗 2
### 解釋上述程式碼運作的原理
```clike
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
- 若 a 小於 b 就是 a ^ a ^ b ,即為 0 ^ b , b 為最大值
- 反之則 a ^ 0 , a 即為最大值
### 針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
#### a, b皆為有號數
```clike
#include <stdint.h>
int32_t max(int32_t a, int32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
#### 其一為有號,另一為無號
```clike
int32_t max(int32_t a, uint32_t b)
{
return a ^ ((a ^ b) & (-(a < b) | (a >> 31)));
}
```
- 因為有號數與無號數做比較運算子,會將有號數轉成無號數
- 多加 a >> 31 當判斷,若 a 為負則 a >> 31 為 0xFFFF ,結果是 a ^ a ^ b ,輸出 b
- 反之則 a >> 31 為 0x0000 ,用 -(a < b) 來決定輸出 a 或 b
---
## 測驗 3
### 解釋上述程式運作原理
```clike
#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 (v);
return u << shift;
}
```
- 此為輾轉相除法的延伸,利用不斷互減仍是最大公因數之倍數的方式,一次次貼近最大公因數
- 看 u 和 v 是否為 0
- 連續除以二,直到其中一數不是二的倍數為止,並用 shift 做公因數中有幾個 2 的紀錄
- 為了減少兩數的大小,分別對兩數做連續除以二
- 接著就是一直用兩數之差與較小的數連續相減,一路比兩數相等,也就是 u - v = 0 (兩數之差會放在 v )
- 最後回傳公因數 u 乘上 shift 所記錄的幾個公因數 2
### 在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升
```clike
#include <stdint.h>
#define min(a,b) ((a) < (b) ? (a) : (b))
uint64_t gcd64(uint64_t u, uint64_t v)
{
if (!u || !v)
return u | v;
int a = __builtin_ctz(u);
int shift = min(a, __builtin_ctz(v));
u >>= a;
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;
}
```
---
## 測驗 4
### 解釋上述程式運作原理與舉出運用的案例
```clike
#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;
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
}
return pos;
}
```
- 把 bitmap 中的每一個位元存到 out 裡面,最後回傳 bitmap 中所存的 bit 總數
---
## 測驗 5
### 解釋上述程式運作原理,指出其中不足,並予以改進
```clike
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "list.h"
struct rem_node {
int key;
int index;
struct list_head link;
};
static int find(struct list_head *heads, int size, int key)
{
struct rem_node *node;
int hash = key % size;
list_for_each_entry (node, &heads[hash], link) {
if (key == node->key)
return node->index;
}
return -1;
}
char *fractionToDecimal(int numerator, int denominator)
{
int size = 1024;
char *result = malloc(size);
char *p = result;
if (denominator == 0) {
result[0] = '\0';
return result;
}
if (numerator == 0) {
result[0] = '0';
result[1] = '\0';
return result;
}
/* using long long type make sure there has no integer overflow */
long long n = numerator;
long long d = denominator;
/* deal with negtive cases */
if (n < 0)
n = -n;
if (d < 0)
d = -d;
bool sign = (float) numerator / denominator >= 0;
if (!sign)
*p++ = '-';
long long remainder = n % d;
long long division = n / d;
sprintf(p, "%ld", division > 0 ? (long) division : (long) -division);
if (remainder == 0)
return result;
p = result + strlen(result);
*p++ = '.';
/* Using a map to record all of reminders and their position.
* if the reminder appeared before, which means the repeated loop begin,
*/
char *decimal = malloc(size);
memset(decimal, 0, size);
char *q = decimal;
size = 1333;
struct list_head *heads = malloc(size * sizeof(*heads));
for (int i = 0; i < size; i++)
INIT_LIST_HEAD(&heads[i]);
for (int i = 0; remainder; i++) {
int pos = find(heads, size, remainder);
if (pos >= 0) {
while (pos-- > 0)
*p++ = *decimal++;
*p++ = '(';
while (*decimal != '\0')
*p++ = *decimal++;
*p++ = ')';
*p = '\0';
return result;
}
struct rem_node *node = malloc(sizeof(*node));
node->key = remainder;
node->index = i;
list_add(&node->link, &heads[remainder % size]);
*q++ = (remainder * 10) / d + '0';
remainder = (remainder * 10) % d;
}
strcpy(p, decimal);
return result;
}
```
從 find 看起,
- 用 key % size 對應要儲存 hash 的位置
- 若從 hash 中找到一樣的值就代表小數循環,回傳儲存在 hash 的位置
- 反之則回傳 -1
從 fractionToDecimal 看起,
- 若 denominator 或 numerator 為 0 ,就分別回傳空字串與 0
- 將 denominator 與 numerator 轉成正整數,存取 sign 到 result
- 利用 sprint 將 long 轉 char ,若商為正,將除數放入 result
- 若整除就回傳 result
- 將 p 指向 result 的字尾,加入小數點
- 宣告與配置記憶體給 decimal ,並初始化值為 0
- 宣告並初始化 1333 個 linked list
- 利用 find 確認是否有循環小數
- 若 pos 是正數或 0 就找到答案
- 反之,利用 list_add 加入新的 node 在 remainder % size ,其值為餘數
- 利用餘數乘十除以除數,將商儲存在 decimal ,並找下一輪餘數
指出其中不足
- 當值要放入的記憶體位置大於 1024 所 malloc 的記憶體時,要重新 realloc p ,要不然就會有隨機 assign 的記憶體位置給 p 形成 memory leak
- 配置記憶體時是用 size 配置,可能會有 buffer overflow 與沒使用到的記憶體浪費。可以紀錄當前能使用的空間,若需要更大空間再 realloc 就好
- 由於前面的判斷,商部會出現 0 或負值,可直接改為下行
```clike
sprintf(p, "%ld", division > 0 ? (long) division : (long) -division);
sprintf(p, "%ld", (long) division);
```
- 若輸入為無限小數時,此程式會陷入無限迴圈
```clike
for (int i = 0; remainder; i++) //遇到無限小數時, remainder 部會為 0
```
---
## 測驗 6
### 解釋上述程式運作原理
```clike
#define ALIGNOF(t) \
((char *)(&((struct { char c; t _h; } *)0)->_h) - (char *)0)
((struct { char c; t _h; } *)0)
//定義一個指向記憶體位置 0 的 struct , size 是 8
(&((struct { char c; t _h; } *)0)->_h)
//指向 _h 所在的記憶體位置,代表也就是 t 資料型別所在的記憶體位置
```
- 指向 t 所代表的記憶體位置,再扣掉一開始 alignment 的位置 0 ,即為 t 資料型別所 align 的記憶體位置
```clike
#include <stdio.h>
#define ALIGNOF(t) \
(&((struct { char c[5];} *)0)->c[t])
int main()
{ //註解為 output
printf("%d\n", ALIGNOF(0)); // 0
printf("%d\n", ALIGNOF(1)); // 1
printf("%d\n", ALIGNOF(2)); // 2
printf("%d\n", ALIGNOF(3)); // 3
printf("%d\n", ALIGNOF(4)); // 4
printf("%d\n", sizeof(ALIGNOF(0))); // 8
}
```
- 看到每個 char[i] 所 alignment 的記憶體位置
```clike
#include <stdio.h>
#define ALIGNOF(t) \
((int *)(&((struct { char c; t _h; } *)5)->_h))
int main()
{
printf("%d\n", ALIGNOF(int)); // 9
printf("%d\n", (char *)5); // 5
}
```
- 可知 5 就是 struct 所對齊的記憶體位置
---
## 測驗 7
### 解釋上述程式運作原理
```clike
static inline bool is_divisible(uint32_t n, uint64_t M)
{
return n * M <= M - 1;
}
static uint64_t M3 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1;
static uint64_t M5 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 5 + 1;
int main(int argc, char **argv)
{
for (size_t i = 1; i <= 100; i++) {
uint8_t div3 = is_divisible(i, M3);
uint8_t div5 = is_divisible(i, M5);
unsigned int length = (2 << div3) << div5;
char fmt[9];
strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (div3 << 2)], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
}
return 0;
}
```
從 is_divisible 看
$n/d = n * (2^N/d) / (2^N)$ ... $n - (n/d) * d$
從餘數推導
$n - (n/d) * d = n - n * (2^N/d) / (2^N) * d$
$= n - n * 2^N / 2^N$
- 當 $n * 2^N$ 比 $2^N$ 大時,可知有餘數
```clike
n * M <= M - 1
```
:::warning
問題:
為什麼不像下行這樣寫就好?
```clike
n * M < M
```
:::
從 main 來看
- M3 與 M5 分別為在 64 位元下能被 3 與被 5 整除的最大無號整數數量
- div3 與 div5 表示 i 是否能被 3 或被 5 整除
- 藉由 FizzBuzz 為 8 個 char 的特性,若可被任一整除,就左移 length ,使 strncpy 拿的字串長度為 2 (%u) 、 4 (Fizz or Buzz) 或 8 (FizzBuzz)
```clike
以這行來討論 [(9 >> div5) >> (div3 << 2)]
```
- 只要 div3 = 1 ,運算完要是 0
- 只有 div5 = 1 ,運算完要是 4
- 都為 0 ,運算完要是 8
:::danger
該題目有誤:
```clike
strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (div3 << 2)], length);
```
9 應改成 8
```clike
strncpy(fmt, &"FizzBuzz%u"[(8 >> div5) >> (div3 << 2)], length);
```
:::