# 2022q1 Homework3 (quiz3) contributed by < `shawn5141` > ## [測驗 `1`](https://hackmd.io/@sysprog/linux2022-quiz3#%E6%B8%AC%E9%A9%97-1) :::success 在 Linux 核心原始程式碼,[include/linux/bitfield.h](https://github.com/torvalds/linux/blob/master/include/linux/bitfield.h) 提及一個巨集 `GENMASK`,其作用是依據給定的範圍,產生連續的 [bitmask](https://en.wikipedia.org/wiki/Mask_(computing)),例如: * `GENMASK(6, 4)` 產生 01110000<sub>2</sub> * `GENMASK(39, 21)` 產生 0x000000ffffe00000 (64 位元) 已知我們使用的微處理器架構為 64 位元,且 `unsigned long` 為 8 位元組寬度 (符合 [LP64](https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models) 資料模型),以下是可能的 `GENMASK` 巨集的定義: ```cpp #define GENMASK(h, l) \ (((~0UL) >> (LEFT)) & ((~0UL) >> (l) << (RIGHT))) ``` 請補完,使其行為符合預期。作答規範: 1. `LEFT` 和 `RIGHT` 應以最簡潔的形式撰寫,且符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範 (近似 Linux 核心程式碼排版風格) 2. `LEFT` 和 `RIGHT` 皆為表示式,可能包含常數 3. `LEFT` 和 `RIGHT` 皆不該包含小括號 (即 `(` 和 `)`) 作答區 LEFT = ? RIGHT = ? ::: ### 1. 解釋上述程式碼運作原理 我們可以從`GENMASK(6, 4)`產生 01110000<sub>2</sub>來分析。 - `(((~0UL) >> (LEFT)) & ((~0UL) >> (l) << (RIGHT)))`可以當成 `(A) & (B))`,在只有一個`&`且只有左移跟右移的情況,可以合理的懷疑A和B是由`11110000`跟`01111111`組成。 >因為是unsigned long,所以Left shift 和 right shift 都會補上0。 >`~0UL` 是`0xffffffffffffffff` ```cpp= 11110000 & 01111111 ------------ 01110000 ``` - A因為只有右移,所以其LSB是會有0。這邊發現A只有右移1個。考慮這個例子只有8bits,所以可以假設A=(8-(h+1))= (8-6-1) = 1。 同理,我們可發現B左移了4,但這邊我們看到B先右移了`l`,然後才左移Right。 在我們的例子中,l=4,所以變成00001111,然後再右移4變成11110000。 按照上面的假設,我們可以得到公式 ```cpp= (((~0UL) >> (63-h)) & ((~0UL) >> (l) << (l))) ``` 我們把這個公式帶到第二個範例中可以得到`0x000000ffffe00000` ### 2. 比較 Linux 核心 GENMASK 巨集的實作,闡述其額外的考量 有三個地方皆有定義 GENMASK。這邊就以[include/linux/bits.h](https://elixir.bootlin.com/linux/latest/source/include/linux/bits.h#:~:text=%23define%20GENMASK(h%2C%20l)%20%5C%0A%09(GENMASK_INPUT_CHECK(h%2C%20l)%20%2B%20__GENMASK(h%2C%20l)))來討論。 ```cpp= #define GENMASK(h, l) \ (GENMASK_INPUT_CHECK(h, l) + __GENMASK(h, l)) ``` 呼叫 `GENMASK_INPUT_CHECK` 跟 `__GENMASK`,我們先探討前者。 ```cpp= #if !defined(__ASSEMBLY__) #include <linux/build_bug.h> #define GENMASK_INPUT_CHECK(h, l) \ (BUILD_BUG_ON_ZERO(__builtin_choose_expr( \ __is_constexpr((l) > (h)), (l) > (h), 0))) #else /* * BUILD_BUG_ON_ZERO is not available in h files included from asm files, * disable the input check if that is the case. */ #define GENMASK_INPUT_CHECK(h, l) 0 #endif ``` 從comments中發現如果`BUILD_BUG_ON_ZERO`的話,就把GENMASK_INPUT_CHECK設為0。否則的話,就判斷 ```cpp= (BUILD_BUG_ON_ZERO(__builtin_choose_expr( \ __is_constexpr((l) > (h)), (l) > (h), 0))) ``` <details> <summary>__is_constexpr判斷(l) > (h)是不是integer const expression,如果是的話就用回傳1 </summary> - [__is_constexpr](https://stackoverflow.com/questions/49481217/linux-kernels-is-constexpr-macro#:~:text=24-,Linux%20Kernel%27s%20__is_constexpr%20macro,Introduction,-The%20__is_constexpr(x))不錯的文章 :::success The `__is_constexpr(x)` macro can be found in Linux Kernel's [include/kernel/kernel.h](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/kernel.h?id=3c8ba0d61d04ced9f8d9ff93977995a9e4e96e91#n814): The macro is notable for taking advantage of subtle details of the C standard: the conditional operator's rules for determining its returned type (6.5.15.6) and the definition of a null pointer constant (6.3.2.3.3). In summary, the __is_constexpr(x) macro returns an integer constant expression of value 1 if the argument is an integer constant expression. Otherwise, it returns an integer constant expression of value 0. ::: ```cpp= /* * This returns a constant expression while determining if an argument is * a constant expression, most importantly without evaluating the argument. * Glory to Martin Uecker <Martin.Uecker@med.uni-goettingen.de> */ #define __is_constexpr(x) \ (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8))) ``` `8 ? ((void *)((long)(x) * 0l)) : (int *)8)`基本上就是去考慮 ``` sizeof(int) == sizeof(*((int *) (NULL))) // if `x` was an integer constant expression sizeof(int) == sizeof(*((void *)(....))) // otherwise ``` >According to the GNU C extension, sizeof(void) == 1. Therefore, if x was an integer constant expression, the result of the macro is 1; otherwise, 0. ![](https://i.imgur.com/SPuENoO.png) (圖擷取自[Linux內核中max()宏的奧妙何在?](https://www.twblogs.net/a/5d8c9b15bd9eee5327001eb0)) </details> <details> <summary>__builtin_choose_expr在編譯的時候做ternary operator 判斷,如果__is_constexpr((l) > (h))回傳1就用(l)>(h),不然就回傳0</summary> TYPE __builtin_choose_expr(CONST_EXP, EXP1, EXP2):同CONST_EXP?EXP1:EXP2的概念,但是這個寫法會在編譯時就決定結果。 </details> <details> <summary>BUILD_BUG_ON_ZERO 如果回傳是0就沒事,但如果(l)>(h)是true的話,就沒辦法編譯通過。因為H必須要大於L</summary> ```cpp= #define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); })) ``` [參照此篇文章](https://www.programminghunter.com/article/68671244579/) 1. (e): 表达式e的声明 2. !!(e): 对e的结果进行两次求非。即如果e开始是0的话,结果就是0;如果e不为0,则结果为1。 3. –!!(e): 再乘以-1。如果第2步结果为0,则仍为0;否则结果为-1。 4. struct { int : –!!(0); } --> struct { int : 0; } : 如果e的结果为0,则我们声明一个结构体拥有一个int型的数据域,并且规定它所占的位的个数为0。这没有任何问题,我们认为一切正常。 5. struct { int : –!!(1); } --> struct { int : –1; } : 如果e的结果非0,结构体的int型数据域的位域将变为一个负数,将位域声明为负数这是一个语法的错误。 6. 现在要么结果为声明了一个位域为0的结构体,要么出现位域为负数编译出错;如果能正确编译,然后我们对该结构体进行sizeof操作,得到一个类型为size_t的结果,值为0。 </details> :::info 所以這邊就是在檢查`h`和`l`的大小合不合規定 ::: #### __GENMASK ```cpp= #define __GENMASK(h, l) \ (((~UL(0)) - (UL(1) << (l)) + 1) & \ (~UL(0) >> (BITS_PER_LONG - 1 - (h)))) ``` <details> <summary>BITS_PER_LONG 在64bit machine下是64,不然就是32</summary> ```cpp #ifdef CONFIG_64BIT #define BITS_PER_LONG 64 #else #define BITS_PER_LONG 32 #endif /* CONFIG_64BIT */ ``` </details> [隔壁同學不錯的例子](https://hackmd.io/@jim12312321/linux2022-quiz3#:~:text=%E6%88%91%E5%80%91%E4%B8%80%E6%A8%A3%E4%BE%86%E8%88%89%E5%80%8B%E4%BE%8B%E5%AD%90%E8%AA%AA%E6%98%8E%EF%BC%88%E4%B8%8D%E8%88%89%E4%BE%8B%E7%B5%A6%E8%87%AA%E5%B7%B1%E7%9C%8B%E6%88%91%E7%9C%9F%E7%9A%84%E8%85%A6%E8%A2%8B%E5%8D%A1%E4%BD%8F%EF%BC%89) ### 3. 舉出 Linux 核心原始程式碼中二處 GENMASK 巨集和 include/linux/bitfield.h 的應用案例 1. [include/asm-generic/bitops/find.h](https://elixir.bootlin.com/linux/latest/source/include/asm-generic/bitops/find.h#L23) ```cpp= #ifndef find_last_bit /** * find_last_bit - find the last set bit in a memory region * @addr: The address to start the search at * @size: The number of bits to search * * Returns the bit number of the last set bit, or size. */ static inline unsigned long find_last_bit(const unsigned long *addr, unsigned long size) { if (small_const_nbits(size)) { unsigned long val = *addr & GENMASK(size - 1, 0); return val ? __fls(val) : size; } return _find_last_bit(addr, size); } #endif ``` 然後`find_last_bit`有被用在[/mm/percpu.c](https://elixir.bootlin.com/linux/latest/source/mm/percpu.c#:~:text=l_bit%20%3D%20find_last_bit(pcpu_index_alloc_map(chunk%2C%20s_index)%2C%20s_off)%3B) ```cpp= /* * pcpu_block_update_scan - update a block given a free area from a scan * @chunk: chunk of interest * @bit_off: chunk offset * @bits: size of free area * * Finding the final allocation spot first goes through pcpu_find_block_fit() * to find a block that can hold the allocation and then pcpu_alloc_area() * where a scan is used. When allocations require specific alignments, * we can inadvertently create holes which will not be seen in the alloc * or free paths. * * This takes a given free area hole and updates a block as it may change the * scan_hint. We need to scan backwards to ensure we don't miss free bits * from alignment. */ static void pcpu_block_update_scan(struct pcpu_chunk *chunk, int bit_off, int bits) { int s_off = pcpu_off_to_block_off(bit_off); int e_off = s_off + bits; int s_index, l_bit; struct pcpu_block_md *block; if (e_off > PCPU_BITMAP_BLOCK_BITS) return; s_index = pcpu_off_to_block_index(bit_off); block = chunk->md_blocks + s_index; /* scan backwards in case of alignment skipping free bits */ l_bit = find_last_bit(pcpu_index_alloc_map(chunk, s_index), s_off); s_off = (s_off == l_bit) ? 0 : l_bit + 1; pcpu_block_update(block, s_off, e_off); } ``` --- # [測驗 2](https://hackmd.io/@sysprog/linux2022-quiz3#%E6%B8%AC%E9%A9%97-2) :::success 考慮以下程式碼: ```cpp struct foo; struct fd { struct foo *foo; unsigned int flags; }; enum { FOO_DEFAULT = 0, FOO_ACTION, FOO_UNLOCK, } FOO_FLAGS; static inline struct fd to_fd(unsigned long v) { return (struct fd){EXP1, v & 3}; } ``` 函式 `fo_fd` 接受指定的地址 `v`,填入一個 `fd` 結構體,並確保第一個成員的地址得以依據〈[你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory)〉描述,對 4 個位元組進行向下對齊 (即 C++ Boost 描述的 [align_down](https://www.boost.org/doc/libs/1_78_0/doc/html/align/reference.html#align.reference.functions.align_down))。其中 `struct foo;` 運用〈[你所不知道的 C 語言:指標篇](https://hackmd.io/@sysprog/c-pointer)〉提及的 forward declaration 技巧,可在實作階段才提供具體 `struct foo` 的定義。 請補完,使其行為符合預期。作答規範: 1. `EXP1` 應以最簡潔的 C99 形式撰寫,符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範,且不該觸發編譯器警告訊息 2. `EXP1` 不得使用 `%` (modulo) 運算子 3. 當變數和常數進行運算時,變數應該出現前於常數,例如 `v | 1` <mark>作答區</mark> * `EXP1` = ? ::: 延伸問題: ### 1. 解釋上述程式碼運作原理 在linux kernel 中已經有定義了`#define ALIGN_DOWN(addr, size) (addr)&(~((size)-1))` 當中size 是4,`(addr)&(~((size)-1))`變成`(addr)&(~(3))`= `(addr)&(0xFFFFFFFFFFFFFFFC)`。最後的C代表1100<sub>2</sub>,也就是說會強制把最後的兩位變成零,造成align_down 的效果。 這邊因為是使用Compound Literals來初始化fd,所以EPX1會把第一個元素變成struct foo。EXP1 = `(struct foo)(v & ~3)` ### 2. 在 Linux 核心原始程式碼找出類似技巧的實作 (例如檔案系統) 並解說 在[arch/arm/mm/init.c](https://elixir.bootlin.com/linux/latest/source/arch/arm/mm/init.c#:~:text=ALIGN_DOWN(addr%2C%20pageblock_size)%2C)有使用`ALIGN_DOWN`的技巧。 ```cpp= #ifdef CONFIG_HAVE_ARCH_PFN_VALID int pfn_valid(unsigned long pfn) { phys_addr_t addr = __pfn_to_phys(pfn); unsigned long pageblock_size = PAGE_SIZE * pageblock_nr_pages; if (__phys_to_pfn(addr) != pfn) return 0; /* * If address less than pageblock_size bytes away from a present * memory chunk there still will be a memory map entry for it * because we round freed memory map to the pageblock boundaries. */ if (memblock_overlaps_region(&memblock.memory, ALIGN_DOWN(addr, pageblock_size), pageblock_size)) return 1; return 0; } EXPORT_SYMBOL(pfn_valid); #endif ``` - pfn stands for page-frame number. ![](https://i.imgur.com/DrlLtwG.png) <details> <summary>從這個commit我們得知 pfn_valid 是用來判斷memblock中有沒有hole的</summary> Platforms like arm and arm64 have redefined pfn_valid() because their early memory sections might have contained memmap holes caused by memblock areas tagged with MEMBLOCK_NOMAP, which should be skipped while validating a pfn for struct page backing. This scenario could be captured with a new option CONFIG_HAVE_EARLY_SECTION_MEMMAP_HOLES and then generic pfn_valid() can be improved to accommodate such platforms. This reduces overall code footprint and also improves maintainability. [link](https://lore.kernel.org/linux-arm-kernel/2541b182-1f1c-c1ed-c15c-9d71160eb6fe@redhat.com/T/) </details>,而memblock_overlaps_region則是在這個commit: [[PATCH 3/3] arm: extend pfn_valid to take into accound freed memory map alignment](https://www.spinics.net/lists/arm-kernel/msg906173.html) 被引入 ## [測驗3](https://hackmd.io/@sysprog/linux2022-quiz3#%E6%B8%AC%E9%A9%97-3) :::success 考慮以下程式碼,能將輸入的 8 位元無號整數逐一位元反轉,如同 [LeetCode 190. Reverse Bits](https://leetcode.com/problems/reverse-bits/) 的描述。 ```cpp= #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; } ``` 請補完,使其行為符合預期。作答規範: EXP2 和 EXP3 應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息 當變數和常數進行運算時,變數應該出現前於常數,例如 v | 1 作答區 EXP2 = ? EXP3 = ? ::: ### 延伸問題: #### 1. 解釋上述程式碼運作原理 不懂就用例子來解,假設`x = 0b01101100`,翻轉後應該變成`reverse_x = 0b00110110`。放進rev8中, ##### Step 1. x = x>>4 | x<<4 ```cpp= 0b00000110 | 0b11000000 --------------- x= 0b11000110 (翻轉前後4bits) ``` ##### Step 2. ((x & 0xCC) >> 2) | (EXP2); ```cpp= 0b11000110 (x) & 0b11001100 (0xCC) ---------------------- 0b11000100 (x') >> 2 ---------------------- 0b00110001 (x'') ``` > 這邊先跟0xCC 做AND Op,再右移2可以想像成保留4bits的前兩個bits。然後移位到後兩個bits。 因為無法從step3 `x = ((x & 0xAA) >> 1) | (EXP3)`回推,所以我們先猜想step2跟step1的目的是類似的,也就是翻轉前後2bits,看得到的答案最後能不能演化成reverse bit要的結果。所以經過| (EXP2)後應該會變成 0x00111001。 ```cpp= 0b00110001 (x'') | EXP2 ---------------------- 0b00111001 (x''') 所以可以知道 EXP2 = 0b00??100? (?可是1或0)就先當作0吧 ``` 所以要想辦法把 `x''(0b11000110)`變成`EXP2(0b00001000)`,主要思考的點是要讓`EXP2(0x00001000)`從右邊數來第四個bit是1的話,要怎麼使用左移或右移的情形產生。所以我們這邊先假設使用0x33對x''進行mask。 > 會使用0x33,是因為想要保留4bits中的後兩位。之後在left shift 2。模仿(x & 0xCC) >> 2 ```cpp= 0b11000110 (x) & 0b00110011 (0x33) --------------------- 0b00000010 (y') << 2 (在left shift 2) --------------------- 0b00001000 (EXP2) ``` 這樣我們就可以得到`EXP2= (x & 0x33) << 2` #### step3 ((x & 0xAA) >> 1) | (EXP3) 接著我們來思考`step3`。跟上面的情況類似,x在這邊是`0b00111001 (x''')`,兩個兩個bits交換後會得到0b00110110,ˊˋˊ就是我們想要得到的最終答案。也就是說前面step2的推理有可能是正確的。這邊先推理左半邊 `((x & 0xAA) >> 1)`。 ```cpp= 0b00111001 (x''') & 0b10101010 (0xAA) --------------------- 0b00101000 >>1 --------------------- 0b00010100 (x'''') ``` 再來思考右半邊,如果也是做類似的動作(masking掉其中一個bit,然後shift) 我們可以用`0b01010101`(0x55),然後在left shift 1。 > 所以得知EXP3 = `(x & 0x55) << 1` ```cpp= 0b00111001 (x''') & 0b01010101 (0x55) --------------------- 0b00010001 <<1 --------------------- 0b00100010 (y) ``` 把 `0b00010100 (x'''')`跟 `0b00100010 (y)`做 OR Operation。 ```cpp= 0b00010100 (x'''') | 0b00100010 (y) --------------------- 0b00110110 (final) ``` 如果還是看不懂的話,可以參照此[文章](https://hackmd.io/@sysprog/bitwise-reverse#%E8%AA%B2%E5%89%8D%E6%B8%AC%E9%A9%97%E5%8F%83%E8%80%83%E8%A7%A3%E7%AD%94-Q1),有精美圖片 #### 2. 在 Linux 核心原始程式碼找出類似技巧的實作並解說其應用場景 參見[linux/lib/packing.c](https://github.com/torvalds/linux/blob/9bc62afe03afdf33904f5e784e1ad68c50ff00bb/lib/packing.c#L32) ```cpp= static u64 bit_reverse(u64 val, unsigned int width) { u64 new_val = 0; unsigned int bit; unsigned int i; for (i = 0; i < width; i++) { bit = (val & (1 << i)) != 0; new_val |= (bit << (width - i - 1)); } return new_val; } ``` 使用場景,同個[linux/lib/packing.c](https://github.com/torvalds/linux/blob/9bc62afe03afdf33904f5e784e1ad68c50ff00bb/lib/packing.c#L52)文件下。 ```cpp= static void adjust_for_msb_right_quirk(u64 *to_write, int *box_start_bit, int *box_end_bit, u8 *box_mask) { int box_bit_width = *box_start_bit - *box_end_bit + 1; int new_box_start_bit, new_box_end_bit; *to_write >>= *box_end_bit; *to_write = bit_reverse(*to_write, box_bit_width); *to_write <<= *box_end_bit; new_box_end_bit = box_bit_width - *box_start_bit - 1; new_box_start_bit = box_bit_width - *box_end_bit - 1; *box_mask = GENMASK_ULL(new_box_start_bit, new_box_end_bit); *box_start_bit = new_box_start_bit; *box_end_bit = new_box_end_bit; } ``` ## [測驗4](https://hackmd.io/@sysprog/linux2022-quiz3#%E6%B8%AC%E9%A9%97-4) :::success 延伸〈[你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor)〉,我們嘗試將 [foreach 巨集](https://en.wikipedia.org/wiki/Foreach_loop) 予以擴充,使得支援以下寫法: ```cpp 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); } ``` 預期輸出如下: ``` i is 0 i is -1 i is 100 i is 0 i is -99 p is Hello p is world ``` 對應的巨集定義: ```cpp #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__}))) ``` 請補完,使其行為符合預期。作答規範: 1. `EXP4` 和 `EXP5` 應以最簡潔的 C99 形式撰寫,符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範,且不該觸發編譯器警告訊息 2. 不該出現小括號 (即 `(` 和 `)`) <mark>作答區</mark> * `EXP4` = ? * `EXP5` = ? ::: ### 1. 解釋上述程式碼運作原理 ```cpp= #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]) ``` #### foreach_int for loop 可以分為 ##### 1. 起始條件: `unsigned _foreach_i = (((i) = ((int[]){__VA_ARGS__})[0]), 0)` - `(int[]){__VA_ARGS__})`: 轉成integer array - `(int[]){__VA_ARGS__})[0]`:拿到第一個integer - `((i) = ((int[]){__VA_ARGS__})[0])`: 把第一個integer assign給 i - 把第二個`0`,指派給`_foreach_i` ##### 2. 終止條件: `_foreach_i < sizeof((int[]){__VA_ARGS__}) / sizeof(int)` - 當_foreach_i < `sizeof(integer array) / sizeof(int)`,就是一般的找integer array的長度。 ##### 3. 更新條件: `(i) = ((int[]){__VA_ARGS__, 0})[EXP4]` - 已知_foreach_i 作為index,所以要透過先做increment再取值來更新i。 > 所以`EXP4 = ++_foreach_i` #### foreach_ptr ##### 1. 起始條件: `unsigned _foreach_i = (((i) = (void *) ((typeof(i)[]){__VA_ARGS__})[0]), 0)` - 跟上面的分析一樣,只是這邊變成先把variable array變成(void*) (typeof(i)[]),然後在assign給i。而_foreach_i 也還是從0開始。 ##### 2. 終止條件: `(i)` - 判斷是不是null pointer ##### 3. 更新條件: ```cpp= (i) = (void *) ((typeof(i)[]){__VA_ARGS__, NULL})[EXP5], \ _foreach_no_nullval(_foreach_i, i,((const void *[]) {__VA_ARGS__}))) ``` 我們先來看第二個function: _foreach_no_nullval ```cpp= #define _foreach_no_nullval(i, p, arr) \ assert((i) >= sizeof(arr) / sizeof(arr[0]) || (p)) ``` - 這邊是要判斷傳進去的index(_foreach_i)在小於array 的memory boundary的時候pointer 的值存不存在。(在大於等於memory boundary的時候assert值會自動唯一)否則的話就會assert fail。 >EXP5跟上面一樣,都是使用++當_foreach_i ### 2. 在 Linux 核心原始程式碼找出類似技巧的實作並解說其應用場景 在[linux/tools/perf/util/debug.h](https://github.com/torvalds/linux/blob/27151f177827d478508e756c7657273261aaf8a9/tools/perf/util/debug.h#L21)蠻多使用`__VA_ARGS__`的macro function,提供不同的debug print。 <details> <summary> 看更多 </summary> ```cpp= /* SPDX-License-Identifier: GPL-2.0 */ /* For debugging general purposes */ #ifndef __PERF_DEBUG_H #define __PERF_DEBUG_H #include <stdarg.h> #include <stdbool.h> #include <stdio.h> #include <linux/compiler.h> extern int verbose; extern int debug_peo_args; extern bool quiet, dump_trace; extern int debug_ordered_events; extern int debug_data_convert; #ifndef pr_fmt #define pr_fmt(fmt) fmt #endif #define pr_err(fmt, ...) \ eprintf(0, verbose, pr_fmt(fmt), ##__VA_ARGS__) #define pr_warning(fmt, ...) \ eprintf(0, verbose, pr_fmt(fmt), ##__VA_ARGS__) #define pr_warning_once(fmt, ...) ({ \ static int __warned; \ if (unlikely(!__warned)) { \ pr_warning(fmt, ##__VA_ARGS__); \ __warned = 1; \ } \ }) #define pr_info(fmt, ...) \ eprintf(0, verbose, pr_fmt(fmt), ##__VA_ARGS__) #define pr_debug(fmt, ...) \ eprintf(1, verbose, pr_fmt(fmt), ##__VA_ARGS__) #define pr_debugN(n, fmt, ...) \ eprintf(n, verbose, pr_fmt(fmt), ##__VA_ARGS__) #define pr_debug2(fmt, ...) pr_debugN(2, pr_fmt(fmt), ##__VA_ARGS__) #define pr_debug3(fmt, ...) pr_debugN(3, pr_fmt(fmt), ##__VA_ARGS__) #define pr_debug4(fmt, ...) pr_debugN(4, pr_fmt(fmt), ##__VA_ARGS__) /* Special macro to print perf_event_open arguments/return value. */ #define pr_debug2_peo(fmt, ...) { \ if (debug_peo_args) \ pr_debugN(0, pr_fmt(fmt), ##__VA_ARGS__); \ else \ pr_debugN(2, pr_fmt(fmt), ##__VA_ARGS__); \ } #define pr_time_N(n, var, t, fmt, ...) \ eprintf_time(n, var, t, fmt, ##__VA_ARGS__) #define pr_oe_time(t, fmt, ...) pr_time_N(1, debug_ordered_events, t, pr_fmt(fmt), ##__VA_ARGS__) #define pr_oe_time2(t, fmt, ...) pr_time_N(2, debug_ordered_events, t, pr_fmt(fmt), ##__VA_ARGS__) #define STRERR_BUFSIZE 128 /* For the buffer size of str_error_r */ union perf_event; int dump_printf(const char *fmt, ...) __printf(1, 2); void trace_event(union perf_event *event); int ui__error(const char *format, ...) __printf(1, 2); int ui__warning(const char *format, ...) __printf(1, 2); #define ui__warning_once(format, ...) ({ \ static int __warned; \ if (unlikely(!__warned)) { \ ui__warning(format, ##__VA_ARGS__); \ __warned = 1; \ } \ }) void pr_stat(const char *fmt, ...); int eprintf(int level, int var, const char *fmt, ...) __printf(3, 4); int eprintf_time(int level, int var, u64 t, const char *fmt, ...) __printf(4, 5); int veprintf(int level, int var, const char *fmt, va_list args); int perf_debug_option(const char *str); void debug_set_file(FILE *file); void debug_set_display_time(bool set); void perf_debug_setup(void); int perf_quiet_option(void); void dump_stack(void); void sighandler_dump_stack(int sig); #endif /* __PERF_DEBUG_H */ ``` </details> ## [測驗 5](https://hackmd.io/@sysprog/linux2022-quiz3#%E6%B8%AC%E9%A9%97-5) :::success 針對 LeetCode [29\. Divide Two Integers](https://leetcode.com/problems/divide-two-integers/),以下是可能的實作: ```cpp= #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; } ``` 請補完,使其行為符合預期。作答規範: 1. `EXP6` 和 `EXP7` 應以最簡潔的 C99 形式撰寫,符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範,且不該觸發編譯器警告訊息 2. 不該出現小括號 (即 `(` 和 `)`) 作答區 - `EXP6` = ? - `EXP7` = ? ::: ### 1. 解釋上述程式碼運作原理,指出可改進之處,並予以實作 我們依然使用例子丟進去判斷。`dividend = 10 (0b1010)` 與 `divisor = 3 (0b0011)`。答案應該是3.33 (0b11.0101 0101 0101 0011 0010 0110) - 因為本題類似於二進位的除法。`EXP6 = dvs << shift` - 可以發現 res 很像是除法得商數。 (在例子中是0b11),所以EXP7就像是餘數,dvd -= dvs << shift 優化的話可以使用`uint64_t shift = __builtin_clzll(dvs) - __builtin_clzll(dvd) + 1;`,效果比單使用while loop的情況好一些。 ![](https://i.imgur.com/Aa7fo6H.png) ```cpp= uint64_t divide_clzll(uint64_t dividend, uint64_t divisor) { uint64_t signal = 1; unsigned int dvd = dividend; if (dividend < 0) { signal *= -1; dvd = ~dvd + 1; } uint64_t dvs = divisor; if (divisor < 0) { signal *= -1; dvs = ~dvs + 1; } uint64_t shift = __builtin_clzll(dvs) - __builtin_clzll(dvd) + 1; uint64_t res = 0; while (dvd >= dvs) { while (dvd < (dvs << shift)){ shift--; } res |= (uint64_t) 1 << shift; dvd -= dvs << shift; } if (signal == 1 && res >= INT_MAX) return INT_MAX; return res * signal; } ``` ### 2. 在 Linux 核心原始程式碼中找出針對整數/浮點數/定點數除法特別撰寫的程式碼,並予以討論 ## [測驗6](https://hackmd.io/@sysprog/linux2022-quiz3#%E6%B8%AC%E9%A9%97-6) :::success 針對 LeetCode [149. Max Points on a Line](https://leetcode.com/problems/max-points-on-a-line/),以下是可能的實作: ```cpp= #include <stdbool.h> #include "list.h" struct Point { int x, y; }; struct point_node { int p1, p2; struct list_head link; }; static bool can_insert(struct list_head *head, int p1, int p2) { struct point_node *pn; list_for_each_entry (pn, head, link) return EXP8; return true; } static int gcd(int x, int y) { while (y) { int tmp = y; y = x % y; x = tmp; } return x; } static int maxPoints(struct Point *points, int pointsSize) { if (pointsSize <= 2) return pointsSize; int i, j, slope_size = pointsSize * pointsSize / 2 + 133; int *dup_cnts = malloc(pointsSize * sizeof(int)); int *hori_cnts = malloc(pointsSize * sizeof(int)); int *vert_cnts = malloc(pointsSize * sizeof(int)); int *slope_cnts = malloc(slope_size * sizeof(int)); memset(hori_cnts, 0, pointsSize * sizeof(int)); memset(vert_cnts, 0, pointsSize * sizeof(int)); memset(slope_cnts, 0, slope_size * sizeof(int)); for (i = 0; i < pointsSize; i++) dup_cnts[i] = 1; struct list_head *heads = malloc(slope_size * sizeof(*heads)); for (i = 0; i < slope_size; i++) INIT_LIST_HEAD(&heads[i]); for (i = 0; i < pointsSize; i++) { for (j = i + 1; j < pointsSize; j++) { if (points[i].x == points[j].x) hori_cnts[i]++, hori_cnts[j]++; if (points[i].y == points[j].y) vert_cnts[i]++, vert_cnts[j]++; if (points[i].x == points[j].x && points[i].y == points[j].y) dup_cnts[i]++, dup_cnts[j]++; if (points[i].x != points[j].x && points[i].y != points[j].y) { int dx = points[j].x - points[i].x; int dy = points[j].y - points[i].y; int tmp = gcd(dx, dy); dx /= tmp; dy /= tmp; int hash = dx * dy - 1333 * (dx + dy); if (hash < 0) hash = -hash; hash %= slope_size; if (can_insert(&heads[hash], i, j)) { struct point_node *pn = malloc(sizeof(*pn)); pn->p1 = i; pn->p2 = j; EXP9; } } } } for (i = 0; i < slope_size; i++) { int index = -1; struct point_node *pn; list_for_each_entry (pn, &heads[i], link) { index = pn->p1; slope_cnts[i]++; } if (index >= 0) slope_cnts[i] += dup_cnts[index]; } int max_num = 0; for (i = 0; i < pointsSize; i++) { if (hori_cnts[i] + 1 > max_num) max_num = hori_cnts[i] + 1; if (vert_cnts[i] + 1 > max_num) max_num = vert_cnts[i] + 1; } for (i = 0; i < slope_size; i++) { if (slope_cnts[i] > max_num) max_num = slope_cnts[i]; } return max_num; } ``` 請補完,使其行為符合預期。作答規範: 1. `EXP8` 和 `EXP9` 應以最簡潔的 C99 形式撰寫,符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範,且不該觸發編譯器警告訊息 2. `EXP8` 不該出現小括號 (即 `(` 和 `)`) 3. `EXP9` 為包含 `list_` 開頭巨集的單一敘述 <mark>作答區</mark> * `EXP8` = ? * `EXP9` = ? ::: ### 解釋上述程式碼運作原理,指出可改進之處,並予以實作 - #37-#43 Initialization of dup_cnts, hori_cnts, vert_cnts. - #48-#50 Initialization of heads。heads是長度為slope_size的array。之後會用作hash table,他的index是由 `hash = dx * dy - 1333 * (dx + dy)`組成。 - #54-#61 把垂直, 平行跟重複的點都存起來。 - #63-#72 算出剩下的兩點的hash number - #73 can_insert function - 需要確定放進來的兩點跟之前的點有重複?所以設計`EXP8=pn->p1 == p1` - #77 `EXP9`應該會跟linux list_head api有關。list_add_tail(pn->link,&heads[hash]) - #85-90 把重複的點也計算到slope_cnt中。 - #94-104 去比較說是平行的點多,垂直的多,還是有斜率的點多。把最大值記錄下來。 [隔壁同學很不錯的解析](https://hackmd.io/@at0mCe11/linux2022-quiz3#%E6%B8%AC%E9%A9%97-6) ### 擴充 LeetCode 題目,考慮更多座標點的輸入 (例如超過 10 萬個) 時,設計有效的資料結構以降低記憶體開銷,並確保快速的執行 ## [測驗7](https://hackmd.io/@sysprog/linux2022-quiz3#%E6%B8%AC%E9%A9%97-7) :::success 考慮 `ilog32` 函式可針對給定的 32 位元無號數,計算扣除開頭的 `0`,最少需要多少位元才可儲存 (the minimum number of bits required to store an unsigned 32-bit value without any leading zero bits),以下是一個可能的實作: ```cpp= int ilog32(uint32_t v) { int ret = v > 0; int m = (v > 0xFFFFU) << 4; 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; } ``` 請補完,使其行為符合預期。作答規範: 1. `EXP10` 和 `EXP11` 應以最簡潔的 C99 形式撰寫,符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範,且不該觸發編譯器警告訊息 2. `EXP11` 不該出現小括號 (即 `(` 和 `)`) 3. `EXP10` 和 `EXP11` 皆包含 `>` 運算子 作答區 - `EXP10` = ? - `EXP11` = ? ::: ### 解釋上述程式碼運作原理 #### 可以把程式分成#3-6, #7-9, #10-12, #13-15。 #3-6 `0xFFFFU` 是 `0b1111 1111 1111 1111`,所以比`0xFFFFU`大就代表MSB是介於16-31。也就是說當 m= (v > 0xFFFFU) << 4。也就說要 m=0x10,v>>m 就會變成右移16 bits。然後ret|=m是把ret變成'm=0x10'假設`v`>`0xFFFFU`的情況。在右移16bits後,v 的 MSB變成位於 0-15bits間。 #7-9 因為目前v的MSB位置是 `0 <= MSB <= 15`,跟`0xFFU`是 `0b1111 1111`比較是要找出v是不是介於9-15bit,如果是的話則右移8(`m = (v > 0xFFU)<<3`)。讓MSB介於0-7。然後再把`m`的值存到ret。 #10-12 目前 MSB位於: `0 <= MSB <= 7`,跟`0xFU`比較是要看v是介於0-3 還是4-7。如果是後者的話,就右移4 (`m = (v>0xFU)<<2`)。再把m記錄下來。 #13-15 同理,目前 MSB位於: `0 <= MSB <= 3`。要判斷是不是介於0-1還是2-3。所以EXP10是 `m = (v>0x3U)<<1`。位移完會把MSB的範圍變成0<=MSB<=1。 #16 最後利用 `ret |= v > 0x1`去判斷是否大於1,如果是的話,則直接存起來。 ### 在 Linux 核心原始程式碼找出類似實作並解說其應用場景 ### 研讀論文《[Using de Bruijn Sequences to Index a 1 in a Computer Word](http://supertech.csail.mit.edu/papers/debruijn.pdf)》,探討缺乏硬體 ctz/clz 指令的微處理器上,如何實作 branchless 的 `ilog` ### 運用〈[你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor)〉和〈[你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics)〉提及的技巧,實作編譯時期 (compile-time) 的 `ilog2` 實作 ``` ``` ## [測驗8](https://hackmd.io/@sysprog/linux2022-quiz3#%E6%B8%AC%E9%A9%97-8) :::success 考慮以下 C++ 二元搜尋樹的程式: ``` typedef struct tnode *tree; struct tnode { int data; tnode *left; tnode *right; tnode(int d) { data = d; left = right = 0; } }; void remove_data(tree &t, int d) { tnode *p = t; tnode *q; while (p != 0 && p->data != d) { if (d < p->data) { q = p; p = p->left; } else { q = p; p = p->right; } } if (p != 0) { if (p->left == 0) if (p == t) t = p->right; else if (p == q->left) q->left = p->right; else q->right = p->right; else if (p->right == 0) if (p == t) t = p->left; else if (p == q->left) q->left = p->left; else q->right = p->left; else { tnode *r = p->right; while (r->left) { q = r; r = r->left; } p->data = r->data; if (r == p->right) p->right = r->right; else q->left = r->right; p = r; } delete p; } } ``` 函式 `remove_data` 嘗試將指定的資料,自給定的二元搜尋樹移除,但原本的 `remove_data` 過於冗長,於是我們可善用 indirect pointer 改寫為以下: ``` void remove_data(tree &t, int d) { tnode **p = &t; while (*p != 0 && (*p)->data != d) { if (d < (*p)->data) EXP12; else EXP13; } tnode *q = *p; if (!q) return; if (!q->left) *p = q->right; else if (!q->right) *p = q->left; else { tnode **r = &q->right; while ((*r)->left) r = EXP14; q->data = (*r)->data; q = *r; *r = q->right; } delete q; } ``` 請補完,使其行為符合預期。作答規範: 1. `EXP12`, `EXP13` 和 `EXP14` 應以最簡潔的 C99 形式撰寫,符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範,且不該觸發編譯器警告訊息,並善用 indirect pointer 的技巧來撰寫 2. `EXP12`, `EXP13` 和 `EXP14` 皆不該出現 `;` 作答區 - `EXP12` = ? - `EXP13` = ? - `EXP14` = ? ::: ```cpp= if (d < p->data) { q = p; p = p->left; } else { q = p; p = p->right; } ``` 應該跟 ```cpp= if (d < (*p)->data) p = &(*p)->left; else p = &(*p)->right; ``` 等價