# 2022q1 Homework2 (quiz2) contributed by < `happyjack91630` > >[作業需求](https://hackmd.io/@sysprog/linux2022-quiz2) ## 測驗 `1` :::success EXP1 = a & b EXP2 = a & b EXP3 = a ^ b ::: ```c= uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (EXP1); } ``` `a>>1`,就是floor(a/2)的動作,但是floor((a+b)/2)有時候並不會等於floor(a/2) + floor(b/2)。在a和b都是奇數的時候,就會出問題。比如: a=7,b=3,floor((7+3)/2)=5,但是floor(7/2)+floor(3/2)=4,會少一個單位。所以`EXP1`就是要在a和b都是奇數情況下把1加回去。`EXP1 = (a&1)&(b&1)`因為a如果是奇數,a&1就會是1,所以如果a和b都是奇數,1&1=1,就能把兩個奇數各別floor過後加起來少1的值加回去了。`(a&1)&(b&1)`能夠化簡成`a&b`。 ```c= uint32_t average(uint32_t a, uint32_t b) { return (EXP2) + ((EXP3) >> 1); } ``` 前面一題,是先分開除在相加,那這題則是**先相加在除**。位元相加可以用`(a ^ b)`代表加完後LSB的位元是甚麼,但是相加過後還要考慮進位的問題。進位的問題則是利用了`(a & b) <<1 `去處理。所以a + b 可以被表示為`(a & b) << 1 + (a ^ b)`,最後在做除二的動作,就是>>1。化簡就會變成`(a & b) + (a ^ b) >> 1`。 ## 測驗 `2` :::success EXP4 = a ^ b EXP5 = a < b ::: ```c= #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((EXP4) & -(EXP5)); } ``` 以下面case分析: | a = 3 b = 8 | a = 8 b = 3 | | -------- | -------- | | => 0011 ^ ((EXP4) & -(EXP5)) = 1000 | => 0011 ^ ((EXP4) & -(EXP5)) = 0011| | => ((EXP4) & -(EXP5)) = 1011 (a ^ b) | => ((EXP4) & -(EXP5)) = 0000 | | 若EXP4 = a ^ b| 若EXP4 = a ^ b | | => ( 1011 & -(EXP5)) = 1011 | => ( 1011 & -(EXP5)) = 0000 | | => -(EXP5) = 1111 (補數)| => -(EXP5) = 0000 | | => EXP5 = 0001 | => EXP5 = 0000 | 左邊的EXP5 == 0001,右邊EXP5 == 0000,可以想成右邊的EXP5為true,左邊的為EXP5為false,那能比較的就是a和b的大小了。所以`EXP5 = a < b`。如果a = b就會回傳a,但也沒關係。 ## 測驗 `3` :::success COND = v == 0 RET = u << shift ::: ```c= #include <stdint.h> uint64_t gcd64(uint64_t u, uint64_t v) { //若u or v 為0,則回傳不是0的數,若u,v皆為0則回傳0 if (!u || !v) return u | v; int shift; //以下for迴圈先計算u,v共同有幾個2次方的因數,結果存在shift裡 for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } //上述for迴圈跑完就會計算所有共同2次方的因數了,所以while可以把u裡2的因數都除掉 while (!(u & 1)) u /= 2; do { while (!(v & 1)) //跟上面for迴圈想法類似,把2的因數刪掉 v /= 2; if (u < v) { //因為等等要用減法算餘數,所以不能有負數情況出現 v -= u; } else { uint64_t t = u - v; //利用減法算餘數 u = v; //gcd u,v對調 v = t; //gcd u,v對調 } } while (COND); //gcd如果還找不到公因數,v != 0,還要再進while迴圈找 return RET; //u為公因數,但是不要忘記前面有先計算2的次方的公因數,所以要把它乘回來,所以最終公因數為 u << shift } ``` ### 在 x86_64 上透過 __builtin_ctz 改寫 GCD ```c= #include <stdint.h> //回傳min(a,b) uint64_t min(uint64_t a, uint64_t b) { uint64_t diff = (a - b); return b + (diff & (diff >> 63)); } //__builtin_ctz回傳尾巴有幾個0 uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; int shift = min(__builtin_ctz(u), __builtin_ctz(v)); 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 (COND); return RET; } ``` ## 測驗 `4` :::success EXP6 = bitset & -bitset ::: ```c= #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 = EXP6; //找出最低位元為1的 int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } } return pos; } ``` 題目有敘述,其中第 9 行的作用是找出目前最低位元的`1`,並紀錄到 t 變數。那`EXP6 = bitset & -bitset`。那第12行就能在使用XOR的方式將最低位元的1清掉為0,再繼續找下一個最低位元為1的地方。 ## 測驗 `6` >參考 kevinshieh0225 >[linux 内核container_of剖析](https://zhuanlan.zhihu.com/p/54932270) :::success M = _h X = 0 ::: ```c= /* * ALIGNOF - get the alignment of a type * @t: the type to test * * This returns a safe alignment for the given type. */ #define ALIGNOF(t) \ ((char *)(&((struct { char c; t _h; } *)0)->M) - (char *)X) ``` 在看懂上述程式碼之前,可以先去看`offestof`這個function。 ```c= #define offsetof(s,memb) \ ((size_t)((char *)&((s *)0)->memb-(char *)0)) ``` 讀懂順序: 1. `((s *)0` => 先將`0`轉型成`s`這個結構體,這麼做是在給定一個起始地址在0的結構,以便後續算出`memb`的offset。 2. `&((s *)0)->memb` => 取得`memb`地址。 3. `(char *)&((s *)0)->memb` =>將地址轉型成(char *)來看,這樣就能1byte,1byte來算了。 4. `((char *)&((s *)0)->memb-(char *)0))` => 用`memb`的地址減掉`0`的地址就能算出offset了。 5. `(size_t((char *)&((s *)0)->memb-(char *)0)))` => 將算出來的`offset`轉型成`size_t`型態。 依照`offsetof`的邏輯,`ALIGNOF`的答案就顯而易見了。