# 2022q1 Homework2 (quiz2) contributed by < [`meyr`](https://github.com/meyr) > 題目:[2022q1 quiz2](https://hackmd.io/@sysprog/linux2022-quiz2) 作業區 :[2022q1 homework](https://hackmd.io/@sysprog/linux2022-homework2) ## 測驗 1 ### 1. 解釋程式碼運作的原理 個無號整數取平均值的程式碼。 ```c= #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a + b) / 2; } ``` 觀察不同數值的結果,因為程式碼有除以2,==等於是無條件捨去。== | | floating | average() | | -------- | -------- | -------- | | $(2 + 3)/2$ | $2.5$ | $2$ | | $(2 + 4)/2$ | $3.0$ | $3$ | | $(3 + 3)/2$ | $3.0$ | $3$ | 所以我們改寫成以下等價的實作時, ```c= #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (EXP1); } ``` 因為 (a >> 1) 會損失LSB一個bit,所以我們必須對兩個數的LSB做判斷。 | | 判斷式 | | | -------- | -------- | -------- | | 無條件捨去 | (a & 1) && (b & 1) | 當a和b的最後一個bit都是1才進位 | | 無條件進位 | (a & 1) \|\| (b & 1)| 當a或b的最後一個bit是1才進位 | 四捨五入 | (a & 1) \|\| (b & 1)| 因為0.5就進位和無條件進位一樣 這邊使用無條件捨去,所以只需要判斷a和b的LSB是否同時為1。 ```c (a & 1) && (b & 1) ``` 可以簡化為 ```c a & b & 1 ``` :::info 如果是數值分析,或是其他對數值敏感的應用,因為這個程式碼是無條件捨去,等於是有個DC offset。多次使用會讓值域越來越小。 ::: 另外 a+b可以寫成下面的程式碼 ```c= (a & b) << 1 + (a ^ b) ``` 其中a & b為carry,當兩個數都為1才需要進位。 其中a ^ b為sum,當只有一個為1才是1,否則為0。 所以 ```c= ((a & b) << 1 + (a ^ b)) >> 1 ``` 可以簡化為 ```c= (a & b) + (a ^ b) >> 1 ``` ### 3.Exponentially weighted moving average (EWMA) 在kernel中有個header [include/linux/average.h](https://github.com/torvalds/linux/blob/master/include/linux/average.h)定義了EWMA。參考[wiki](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average)的解釋 ![EWMA](https://i.imgur.com/YGSGEBG.png) 當t = 0時,第一筆data為Y[0]即為輸出的S[0] = Y[0]。 當t = 1時,第二筆data為Y[1],則輸出為S[1] = a * Y[1] + (1 - a) * S[0]。 當t = 2時,第三筆data為Y[2],則輸出為S[2] = a * Y[2] + (1 - a) * S[1] 也就是當前的資料給一個weight alpha,其他資料就是給(1 - alpha)的weight,可以展開成以下的式子。 $$ S[N] = \alpha Y[N] + \alpha (1-\alpha)Y[N - 1] + \alpha(1 - \alpha)^2 * Y[N - 2] + ... $$ 當alpha小於1,則(1 - alpha) < 1,也就是越前面的資料影響越小。 #### Alpha的選擇 ref1 : [Exponential Moving Average](https://tttapa.github.io/Pages/Mathematics/Systems-and-Control-Theory/Digital-filters/Exponential%20Moving%20Average/Exponential-Moving-Average.html) ref2 : [Frequency Response of the Running Average Filter](https://ptolemy.berkeley.edu/eecs20/week12/freqResponseRA.html) 這個式子是一個==IIR的Low pass filter架構==。因為是lowpass filter,所以可以得出DC值,而DC值就是數值的平均值。 其中frequency response如下圖所示。可以看出確實是一個lowpass filter。 ![](https://tttapa.github.io/Pages/Mathematics/Systems-and-Control-Theory/Digital-filters/Exponential%20Moving%20Average/images/frequency-response.png) 另外不同的$\alpha$也會對frequency response產生影響,造成lowpass filter的passband的頻寬大小不一樣。如下圖所示。 ![](https://ptolemy.berkeley.edu/eecs20/week12/movingAvgMagResponse.png) | | $\alpha$數值 | 頻寬 | 取得正確平均的時間 | 受noise的影響 | | ------------ | ------------ | --- | ------------------ | ------------- | | $\alpha$越小 | $1\over64$ | 越小 | 越慢 | 越小 | | $\alpha$越大 | $1\over4$ | 越大 | 越快 | 越大 | #### 名詞解釋 | Funciton Name | Description | | ---------------------- | -------------------------- | | **BUILD_BUG_ON(cond)** | 停止compile如果cond是true. | |**BUILD_BUG_ON_NOT_POWER_OF_2(var)** | 停止compile如果var不是2的次方。 |**__builtin_constant_p(var)** | 判斷var是否為常數,返回1為常數,返回0則否。 |**READ_ONCE/WRITE_ONCE** | 參考[這裡](/YcjVoBx5R_mnKxX9-H6pEg#READ_ONCEWRITE_ONCE)</br>就是volatile的封裝。當需要用的時候把變數轉換成volatile型態,編譯器只是保證 2 個 volatile 變數之間讀取不會被亂序。 #### Source Code分析 從[LXR](https://elixir.bootlin.com/linux/latest/source)中找出使用EWMA的例子[sta_info.h](https://elixir.bootlin.com/linux/latest/source/net/mac80211/sta_info.h#L126), 其中第一個為name,第二個為pricision,第三個alpha值。 因為alpha為小於1的值,所以這個為$\alpha = 1 / 8$。 ```c= DECLARE_EWMA(avg_signal, 10, 8) ``` 另外一個很重要的變數==weight_rcp,就是把輸入的8取log2。== ```c= unsigned long weight_rcp = ilog2(_weight_rcp); ``` 根據ilog2的定義。[include/linux/log2.h](https://github.com/torvalds/linux/blob/master/include/linux/log2.h#L77),因為我們限制_weight_rcp一定要為2的次方,所以log2(_weight_rcp)就是==找出MSB的位數==。這樣我們做乘法或是除法的時候只需要用shift就可以達成。 例如: ilog2(8) = 3 ilog2(16) = 4 ilog2(32) = 5 重點是怎麼把val加進來,所以只分析這個function。 ```c= WRITE_ONCE(e->internal, internal ? (((internal << weight_rcp) - internal) + (val << precision)) >> weight_rcp : (val << precision)); ``` 根據上面的邏輯重新改寫。 ```c= if(internal) // 第一筆,直接放入 e->internal = val << precision; else { // 第二筆之後 unsigned long newdata = val << precision; unsigned long olddata = (internal << weight_rcp) - internal; e->internal = (olddata + newdata) >> weight_rcp; } ``` 原本的算式 $$ S[N] = \alpha*Y[n] + (1-\alpha)*S[N-1] $$ 把$\alpha$提出到式子外面。 $$ S[N] = \alpha(Y[N] + ({1\over\alpha} - 1)*S[N - 1]) $$ 最後等於下面的算式 $$ S[N] = \alpha(Y[N] + {1\over\alpha}*S[N-1] - S[N - 1]) $$ 因為$\alpha$小於1,所以**乘於$\alpha$等於向右shift(>> weight_rcp)** **乘於$1\over\alpha$等於向左shift(<< weight_rcp)。** #### 在 Linux 核心的應用 通常是計算平均成的場合使用。每一筆資料近來,我就可以算出平均值,而不用儲存以前的數值。參考以下的程式碼[drm_self_refresh_helper.c:275](https://elixir.bootlin.com/linux/latest/source/drivers/gpu/drm/drm_self_refresh_helper.c#L256) ==init之後,第一筆資料給有可能的平均值,可以快速得到準確的平均值,而不用從不正確的值慢慢收斂。== ```c= #define SELF_REFRESH_AVG_SEED_MS 200 DECLARE_EWMA(psr_time, 4, 4) ewma_psr_time_init(&sr_data->entry_avg_ms); ewma_psr_time_init(&sr_data->exit_avg_ms); /* * Seed the averages so they're non-zero (and sufficiently large * for even poorly performing panels). As time goes on, this will be * averaged out and the values will trend to their true value. */ ewma_psr_time_add(&sr_data->entry_avg_ms, SELF_REFRESH_AVG_SEED_MS); ewma_psr_time_add(&sr_data->exit_avg_ms, SELF_REFRESH_AVG_SEED_MS); ``` ## 測驗 2 ### 1.解釋程式碼運作的原理 實作branchless的max函數。 ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((EXP4) & -(EXP5)); } ``` 先觀察這個function,只知道a ^ (某個數)要得到我們的答案。 所以得到以下的table。 | input | expect | internal | | ----- | ------ | -------- | | a > b | a | a ^ 0 | | a < b | b | a ^ (a ^ b)| 另外2的補數特性 | val | -val(10進位) | -val(2進位) | | -------- | -------- | -------- | | 1(true) | -1 | 1111...1111(全部為1) | | 0(false) | 0 | 0000...0000(全部為0) | 所以 | expr | EXP3 == true | EXP3 == false| | -------- | -------- | -------- | | (EXP4) & -(EXP3) | EXP4 | 0 | 所以我們可以用以下來實現max ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((a ^ b) & -(a < b)); } ``` ### 3.在 Linux 核心原始程式碼中,找出更多類似的實作手法 ```c if (i & lsbit) i -= size; ``` 下面的程式碼是實作上面的branchless的等校程式碼。 ```c i -= size & -(i & lsbit); ``` 如同上面分析的, | expr | i & lsbit == 1 | i & lsbit == 0| | -------- | -------- | -------- | | (size) & -(i & lsbit) | size | 0 | ## 測驗7 FizzBuzz的題目為,當數字為3的倍數則印出"Fizz",當數字為5的倍數則印出"Buzz",如果同時是3和5的倍數則印出"FizzBuzz"。 老師給的程式碼如下: ```c #include <stdio.h> int main() { for (unsigned int i = 1; i < 100; i++) { if (i % 3 == 0) printf("Fizz"); if (i % 5 == 0) printf("Buzz"); if (i % 15 == 0) printf("FizzBuzz"); if ((i % 3) && (i % 5)) printf("%u", i); printf("\n"); } return 0; } ``` 這邊應該要把i % 15的順序排在最上面,不然沒機會印出"FizzBuzz",所以修改程式碼如下: ```c #include <stdio.h> int main() { for (unsigned int i = 1; i < 100; i++) { if ((i % 3 == 0) && (i % 5 == 0)) printf("FizzBuzz\n"); else if (i % 3 == 0) printf("Fizz\n"); else if (i % 5 == 0) printf("Buzz\n"); else printf("%u\n", i); } return 0; } ``` 另外列印字串的程式改用以下的方法 ```c char fmt[9]; strncpy(fmt, &"FizzBuzz%u"[start], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); ``` 其中length為需要列印字串的長度 | length | M5 == 0 | M5 == 1 | | -------- | -------- | -------- | | M3 == 0 | 2(%u) | 4(Buzz) | | M3 == 1 | 4(Fizz)| 8(FizzBuzz)| 所以 length可以用以下的方法算得 ```c unsigned int length = (2 << div3) << div5; ``` 另外字串是由"FizzBuzz%u"來取得,所以必須要有不同的offset。 | offset | M5 == 0 | M5 == 1 | | -------- | -------- | -------- | | M3 == 0 | 8(%u) | 4(Bizz) | | M3 == 1 | 0(Fizz)| 0(FizzBuzz)| 所以offset可以由以下的程式碼算得 ```c unsigned int offset = (8 >> div5) >> (div3 << 2); ``` 修正過的程式碼如下,可以正確的顯示FizzBuzz ```c= uint8_t div3 = is_divisible(i, M3); uint8_t div5 = is_divisible(i, M5); unsigned int length = (2 << div3) << div5; unsigned int offset = (8 >> div5) >> (div3 << 2); char fmt[9]; strncpy(fmt, &"FizzBuzz%u"[offset], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); ``` ###### tags: `linux2022`