# 2022q1 Homework3 (quiz3) contributed by < `happyjack91630` > > [作業需求](https://hackmd.io/@sysprog/BJJMuNRlq) > [測驗題目](https://hackmd.io/@sysprog/linux2022-quiz3) ## 測驗 `1` :GENMASK :::success EXP1 = 63 - h EXP2 = l ::: 產生連續的 bitmask。 ```c #define GENMASK(h, l) \ (((~0UL) >> (LEFT)) & ((~0UL) >> (l) << (RIGHT))) //(~0UL = 1111...111) ``` 例如: * `GENMASK(6, 4)` 產生 01110000 * `GENMASK(39, 21)` 產生 0x000000ffffe00000 (64 位元) `GENMASK(h, l)` 可以看出就是在h ~ l + 1 位元以外的地方都是0。 `((~0UL) >> (LEFT)`的作用是將63 ~ h位元設成0,所以`LEFT = 63 - h`。 `(~0UL) >> (l) << (RIGHT))`的作用是將 l - 1 ~ 0位元設成0,所以`RIGHT = l`。 `((~0UL) >> (63 - h)` 跟 `(~0UL) >> (l) << (l))`做 and 運算,就能將h ~ l + 1,l - 1 ~ 0 位元清除成0,保留其他位元為1。 ## 測驗 `2` :align down >參考 `kevinshieh0225` :::success EXP1 = (struct foo *)(v & -3) ::: ```c= struct foo; struct fd { struct foo *foo; unsigned int flags; }; static inline struct fd to_fd(unsigned long v) { return (struct fd){EXP1, v & 3}; //foo = exp1, flags = v & 3; } ``` >函式 `to_fd` 接受指定的地址 `v`,填入一個 `fd` 結構體,並確保第一個成員的地址得以依據〈你所不知道的 C 語言:記憶體管理、對齊及硬體特性〉描述,對 4 個位元組進行向下對齊 (即 C++ Boost 描述的 align_down)。其中 `struct foo`; 運用〈你所不知道的 C 語言:指標篇〉提及的 forward declaration 技巧,可在實作階段才提供具體 `struct foo` 的定義。 我們希望將`to_fd`指派在地址`v`且向下對齊的位置上。舉例來說,v = 7,向下對齊要變成4,v = 15,向下對齊要變成12,所以比v小的最大4的倍數。(v & ~3)就能取到比v小且為4的倍數。但是`struct fd`裡第一個存的是`struct foo *foo`,所以要將`v`轉型成`struct foo`的型態。 ## 測驗 `3` :Reverse Bits :::success EXP2 = (x & 0x33) << 2 EXP3 = (x & 0x55) << 1 ::: ```c #include <stdint.h> uint8_t rev8(uint8_t x) { x = (x >> 4) | (x << 4); x = ((x & 0xCC) >> 2) | (EXP2); x = ((x & 0xAA) >> 1) | (EXP3); return x; } ``` 這裡我用 `x = 12345678`當作以下解釋程式碼的範例。 ```c #include <stdint.h> //每次都取一半來做反轉 uint8_t rev8(uint8_t x) { x = (x >> 4) | (x << 4); // (12345678 >> 4) | (12345678 << 4) = 56781234 x = ((x & 0xCC) >> 2) | (EXP2); // ((56781234) & 11001100) >> 2) | ((56781234) & 00110011)) << 2 = 78563412 x = ((x & 0xAA) >> 1) | (EXP3); // ((78563412) & 10101010) >> 1 | ((78563412) & 01010101) << 1 = 87564321 return x; } ``` :::warning 避免說「一半」這種詞彙,請將自己設想在「科技公司面試場合」,用字要精準,無論你如何位移,資料寬度仍是一致,因此跟「一半」的概念不同。 :notes: jserv ::: ## 測驗 `4` : foreach 巨集 >參考`hsuedw` :::success EXP4 = ++_foreach_i EXP5 = ++_foreach_i ::: > 延伸〈你所不知道的 C 語言:前置處理器應用篇〉,我們嘗試將 foreach 巨集 予以擴充,使得支援以下寫法: ```c= int i; foreach_int(i, 0, -1, 100, 0, -99) { printf("i is %i\n", i); } const char *p; foreach_ptr(p, "Hello", "world") { printf("p is %s\n", p); } ``` 對應的巨集定義: ```c= #include <assert.h> #define _foreach_no_nullval(i, p, arr) \ assert((i) >= sizeof(arr) / sizeof(arr[0]) || (p)) #define foreach_int(i, ...) \ for (unsigned _foreach_i = (((i) = ((int[]){__VA_ARGS__})[0]), 0); \ _foreach_i < sizeof((int[]){__VA_ARGS__}) / sizeof(int); \ (i) = ((int[]){__VA_ARGS__, 0})[EXP4]) #define foreach_ptr(i, ...) \ for (unsigned _foreach_i = \ (((i) = (void *) ((typeof(i)[]){__VA_ARGS__})[0]), 0); \ (i); (i) = (void *) ((typeof(i)[]){__VA_ARGS__, \ NULL})[EXP5], \ _foreach_no_nullval(_foreach_i, i, \ ((const void *[]){__VA_ARGS__}))) ``` ```c= int i; foreach_int(i, 0, -1, 100, 0, -99) { printf("i is %i\n", i); } ``` 先針對上述的`foreach_int(i, 0, -1, 100, 0, -99)`做解析,利用相關巨集定義做展開,對展開一項一項做解析。 ```c= int i; for (unsigned _foreach_i = (((i) = ((int[]){0, -1, 100, 0, -99})[0]), 0); _foreach_i < sizeof((int[]){0, -1, 100, 0, -99}) / sizeof(int); (i) = ((int[]){0, -1, 100, 0, -99, 0})[++_foreach_i]) { printf("i is %i\n", i); } ``` * `(int[]){0, -1, 100, 0, -99})` 表示宣告一個int array,裡面包了這5個元素。 * `((int[]){0, -1, 100, 0, -99})[0]` 表示取這個array的第0個index的值,也就是0。 * `((i) = ((int[]){0, -1, 100, 0, -99})[0])` 表示將這個array的第0個index的值,assign給i。 * `unsigned _foreach_i = (((i) = ((int[]){0, -1, 100, 0, -99})[0]), 0)` ,這段程式碼可以看做成`unsigned _foreach_i = (0, 0)`,第一個0的由來是取第0個index的值,第二個0可以看成是將`_foreach_i`的初始值設為0。 * `_foreach_i < sizeof((int[]){0, -1, 100, 0, -99}) / sizeof(int);` 這個為for loop的中止條件,_foreach_i(index)要小於array的長度。 * `(i) = ((int[]){0, -1, 100, 0, -99,0})[EXP4]` for迴圈有了初始條件,有了終止條件,剩下的就是要做`_foreach_i(index)`的狀態變化。那這題是要將`_foreach_i`做遞增,讓array跑完。`EXP4 = ++_foreach_i`。 那第二個巨集`foreach_ptr`就跟上述想法一致,所以`EXP5 = ++_foreach_i` ## 測驗 `5` :LeetCode 29. Divide Two Integers :::success EXP6 = dvs << shift EXP7 = dvd -= dvs << shift ::: 針對 LeetCode 29. Divide Two Integers,以下是可能的實作: ```c= #include <limits.h> int divide(int dividend, int divisor) { int signal = 1; unsigned int dvd = dividend; if (dividend < 0) { signal *= -1; dvd = ~dvd + 1; } unsigned int dvs = divisor; if (divisor < 0) { signal *= -1; dvs = ~dvs + 1; } int shift = 0; while (dvd > (EXP6)) shift++; unsigned int res = 0; while (dvd >= dvs) { while (dvd < (dvs << shift)) shift--; res |= (unsigned int) 1 << shift; EXP7; } if (signal == 1 && res >= INT_MAX) return INT_MAX; return res * signal; } ``` * 第4行到第15行的程式碼要先辨識出最後的答案是正數還是負數。 * 第17行到第27行的程式碼就是在做除法運算,res會是商,那EXP7則是算餘數是多少。 * 第29行到第32行則是再把結果,變成正數or負數。 ![](https://i.imgur.com/pQ3RLvd.png) ## 測驗 `7` :ilog32 考慮 `ilog32` 函式可針對給定的 32 位元無號數,計算扣除開頭的 `0`,最少需要多少位元才可儲存 (the minimum number of bits required to store an unsigned 32-bit value without any leading zero bits),以下是一個可能的實作: ```c= int ilog32(uint32_t v) { int ret = v > 0; int m = (v > 0xFFFFU) << 4; //0xFFFFU = 0x0000FFFF v >>= m; ret |= m; m = (v > 0xFFU) << 3; v >>= m; ret |= m; m = (v > 0xFU) << 2; v >>= m; ret |= m; m = EXP10; v >>= m; ret |= m; EXP11; return ret; } ``` 這題換句話說,就是要取2進位,最高位元的1在甚麼地方?這邊用的方法,類似於binary search的方法,每次都找**一半**。 ![](https://i.imgur.com/4JydSZr.png) ## 測驗 `9` :ROUND_UP_TO_ALIGNMENT_SIZE :::success MMM = 1 NNN = MAX_ALIGNMENT - 1 ::: 考慮以下進行記憶體地址對齊 (alignment) 的巨集: ```c= /* maximum alignment needed for any type on this platform, rounded up to a power of two */ #define MAX_ALIGNMENT 16 /* Given a size, round up to the next multiple of sizeof(void *) */ #define ROUND_UP_TO_ALIGNMENT_SIZE(x) \ (((x) + MAX_ALIGNMENT - MMM) & ~(NNN)) ``` 這題是要考記憶體對齊,對齊的單位為16,也就是要補滿到最近的16倍數,例如 7 => 16,15 => 16,29 => 32,換句話說,就是要無條件進位到16的倍數。要將 x 進位到 16 倍數的想法就是,先 x + 15 再將後面3個bit(bit 0 ~ 3)清成0。 ![](https://i.imgur.com/JsIAep7.png) >上述方法可以應用在各種 2 的冪進位系統上。 ## 測驗 `10` :DIVIDE_ROUND_CLOSEST :::success RRR = __x + (__d >> 1) SSS = __x - (__d >> 1) ::: 此題實作整數除法取近似整數。 ```c #define DIVIDE_ROUND_CLOSEST(x, divisor) \ ({ \ typeof(x) __x = x; \ typeof(divisor) __d = divisor; \ (((typeof(x)) -1) > 0 || ((typeof(divisor)) -1) > 0 || \ (((__x) > 0) == ((__d) > 0))) \ ? ((RRR) / (__d)) \ : ((SSS) / (__d)); \ }) ``` 原本C的整數除法是**無條件捨去法**,這題的函示要做的除法是**四捨五入**。 ```c= typeof(x) __x = x typeof(divisor) __d = divisor; ``` 上述宣告方式是要避免巨集展開時,導致多次的evalution,所以才先將x跟divisor的值放入另一個參數中,防止變數被更改到(之所以要檢查是否為 unsigned ,是因為跟 unsigned 運算的 signed 數值也會變 unsigned ,因此恆大於等於 0)。 ```c= (((typeof(x)) -1) > 0 || ((typeof(divisor)) -1) > 0 || \ (((__x) > 0) == ((__d) > 0))) ``` >參考`jim12312321` 這條件式主要是先判斷結果有沒有可能是負數,是結果採用**正數除法**還是**負數除法**。x 如果為int,代表typeof((x) - 1 > 0)不成立,那如果 x 為unsigned int ,代表typeof((x) - 1 > 0)成立。 ```c= ((RRR) / (__d)) ((SSS) / (__d)); ``` 先撇開三元條件式,上述程式碼是要去計算除法且**四捨五入後**的結果。 ![](https://i.imgur.com/U181hTf.png) :::danger 改用 Graphviz 或 HackMD 支援的標記語法來製圖 :notes: jserv ::: 根據上述的範例得知,可以先讓被**除數(x)先加除數(divisor)的一半**,再去做C的整數除法,就能做到四捨五入了。但是上述想法只用於正數除法,**負數除法則是要先減再除**,答案才會對。