contributed by < NOVBobLee
>
對兩個無號 32 位元整數取平均值。
方法一會有 overflow 的問題,我們將 a
, b
都代入 ( 0xFFFF'FFFF
),結果會得到 ,是代入值的一半,顯然跟我們期望的平均值不同(期望輸出為 ),以下用 uint8_t
為例。
uint8_t |
二進位 | 備註 |
---|---|---|
a |
1111'1111 |
|
b |
1111'1111 |
|
a + b |
(1)'1111'1111 |
溢位 |
(a + b) / 2 |
0111'1111 |
無號整數的溢位會發生在相加超過上限值或者相減變為負數的時候,若我們知道誰的值比較大,那可以使用方法二,避免以上兩者的發生。
但需要事先知道誰大誰小,而方法三就避免掉減法,也就不需要比大小了。在整數上做除法,得到的是商數,所以還需要對兩者的餘數做處裡,從真值表可以看出該處理 EXP1 應該為 a & b & 1
(用 &1
限定 位置的位元)。
a & 1 |
b & 1 |
(a & 1 + b & 1) / 2 |
---|---|---|
0 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
0 |
1 |
1 |
1 |
那有沒有可能把方法三再縮短,可以觀察 a + b
的位元計算,將 a
自己拆開成與 b
相同的部份跟不同的部份,同樣對 b
做拆開。
然後相加 a
和 b
,可以看到相同的部份就是兩倍,而不同的部份則剛好是互補。
相加可以看到左移一位有溢位的可能,不過我們還會再做一個除以 的右移,就避開了溢位的危險,而方法四即使用此運算方式。
延伸問題:
移動平均(Moving average),又稱滾動平均值、滑動平均,在統計學中是種藉由建立整個資料集合中不同子集的一系列平均數,來分析資料點的計算方法。
移動平均通常與時間序列資料一起使用,以消除短期波動,突出長期趨勢或周期。短期和長期之間的閾值取決於應用,移動平均的參數將相應地設置。例如,它通常用於對財務數據進行技術分析,如股票價格、收益率或交易量。它也用於經濟學中研究國內生產總值、就業或其他宏觀經濟時間序列。
以下為比較有開最佳化和沒開的編譯組合語言結果,使用 AT&T 語法。
沒有開啟最佳化結果:
endbr64
是什麼?%rbp
, %rsp
是什麼?
Intel® 64 and IA-32 Architectures Software Developer’s Manual
If the nesting level is 0, the processor pushes the frame pointer from the BP/EBP/RBP register onto the stack, copies the current stack pointer from the SP/ESP/RSP register into the BP/EBP/RBP register, and loads the SP/ESP/RSP register with the current stack-pointer value minus the value in the size operand. For nesting levels of 1 or greater, the processor pushes additional frame pointers on the stack before adjusting the stack pointer. These additional frame pointers provide the called procedure with access points to other nested frames on the stack.
Releases the stack frame set up by an earlier ENTER instruction. The LEAVE instruction copies the frame pointer (in the EBP register) into the stack pointer register (ESP), which releases the stack space allocated to the stack frame. The old frame pointer (the frame pointer for the calling procedure that was saved by the ENTER instruction) is then popped from the stack into the EBP register, restoring the calling procedure’s stack frame.
.cfi_*
是什麼?開啟最佳化結果(之後都將 .cfi_*
, endbr64
去除),其結果 -O0
, -O1
, -O2
皆相同:
從兩者可以看出指令數的差別,還有就是開了最佳化後,連 stack 都不見了XD 沒開最佳化的時候,他會先把引數存進 stack 裡,再從 stack 讀回到 register 裡面,不知道是不是因為 argument register 不能拿來使用才這樣做?(可以使用,見方法三使用最佳化編譯結果)
這邊有點疑問,在 GCC Optimize Options 是說預設就是 -O0
,但 -O0
結果跟沒加 flag 的結果不同。
沒開最佳化,可以看到第 7 到 10 行,剛開始使用 %eax
計算,最後又換到 %edx
上,其實可以直接使用 %edx
即可。
開最佳化之後( -O0
, -O1
, -O2
三者結果相同), stack 也不見了。
沒開最佳化一樣會使用 stack ,跟開最佳化的結果比較後, stack 還是多餘的動作。
開最佳化後( -O0
, -O1
, -O2
結果一樣),沒有使用 stack ,而 register 的使用上比沒開最佳化的更靈活,像是開最佳化後的第 3 到 7 行對比沒開的第 7 至 12 行,還有開最佳化的第 8 到 10 行對比沒開的第 13 到 16 行。
沒開最佳化有使用 stack ,在 register 的選擇上會優先使用 %eax
,所以出現第 9 行的交換。
這次開啟最佳化 -O2
有些變化,與 -O0
, -O1
不一樣,兩個都沒有使用 stack 。
雖然做的事跟 -O0
, -O1
一樣,不過 -O2
在 register 上的使用上更靈活,第 4 行將 argument register %edi
的值複製進 %eax
之後就拿來存 a & b
的結果了,換句話說,運用 register 的手段比之前的多。
看完之後做比較,方法一二的使用指令數比較少,不過有 overflow 的可能或是需要 a, b 的大小關係。而方法三四以比較 general 的方法,且方法四的使用指令數比較少。
(TODO: 測試)
EWMA (Exponentially Weighted Moving Average) 是一種特殊的取平均方式,在一資料集中不同的子集合各取其平均,從名稱中可以看到移動,意指不同的子集,而其平均方式會使用指數權重。給一資料集合序列 ,而其 EWMA 計算方式如下:
其計算非常簡單,最初的 EWMA 為資料本身 ,因為在之前還沒有計算 EWMA 。而之後每增加一筆新的資料 會乘上一個權重 ,再加上之前計算的 EWMA 乘上 ,亦即以前的資料也會影響到現在的 EWMA ,可參考下面的圖例( )。
其中 又稱 smoothing factor ,取值介於 之間,越低則舊的資料的影響越大,相反地,越高則是新加入的資料影響越大。通常會取比較低的值,使得新加入的資料不會讓現有的變化趨勢波動過大,有 smoothing 的作用。
以下為 Linux 的 EWMA 實作,位置於 include/linux/average.h 。要使用 EWMA 需要先用此 macro 將 EWMA 專用的結構和函式建立起來,不同名稱的 EWMA 會有不同的對應結構名稱與函式。第一個引數 name
為 EWMA 名稱,用來識別不同的 EWMA 。第二個參數 _precision
為 EWMA 的精密度是在小數第幾位(二進位)。第三個參數 _weight_rcp
是權重的倒數,而且要為 的次方,直接看數學式 ,使用倒數的原因是權重 範圍可以被限定在 之間了。
所有建立的函式名稱和結構名稱都會接上 name
這個名稱,用以區分不同的 EWMA 。最開始會先宣告 struct ewma_name
(之後用 name 代表接上的部份),裡面只有一個成員 internal
,紀錄我們的 EWMA 。再來是自己宣告結構變數,然後使用 ewma_name_init
初始化該變數。
在函式中可以看到很多 BUILD_BUG_ON
,那是 Linux 設計在 compile-time 檢查錯誤的 macro 。這邊會檢查 _precision
, _weight_rcp
是否為常數表示式, _precision
是否在 以內, _weight_rcp
是否為 的冪(且不可為零),若沒有符合,在編譯的時候就會跳出錯誤。
這邊我有個問題,為何要三個函式都重複檢查,何不在其中一個函數裡作所有檢查就好?我目前猜測是跳出錯誤訊息的時候,可以明確指出哪些函式會使用到這些含有錯誤的表示式,在除錯時可以比較全盤的掌握錯誤所在。
剩下兩個函式一個拿來看現在 EWMA ,另一個拿來加入新資料更新 EWMA 用的。 ewma_name_add
會加入一筆新資料,更新我們的 EWMA ,可以看到 _weight_rcp
用 的冪是為了使用位元的左移右移,會經過 ilog2
轉換成次方數 weight_rcp
。
而精密度的實作是將每一筆資料加入的時候都預先左移,這樣紀錄在 internal
裡的 EWMA 其實是實際值的 倍,這樣多出來的右側位數就可以拿來計算小數的部份,也因為如此,查看 EWMA 的時候,使用 ewma_name_read
裡會做一個右移,將 internal
還原成實際的 EWMA 。
在 Linux 裡很多網路相關的 driver 有使用 EWMA ,有算晶片溫度、資料吞吐量、訊號強度 (rssi) 等平均,而其他像是網路協議和 GPU 的驅動也有在使用。從搜尋結果來看,這巨集設計也是不錯,可以從 name
看出他的作用為何。
以無分支方式求出最大值。
先來看看 XOR 運算的其中一些特性:
(a ^ b) ^ a = (b ^ a) ^ a = b ^ (a ^ a) = b
(a ^ b) ^ b = a ^ (b ^ b) = a
a ^ 0 = a
以上特性在 max
的引數裡兩者選其一可以使用,當要替換 a
變成 b
,那麼 a
後面可以接 a ^ b
,而不想替換的話則是接 0
,這樣看起來 &
後面接一個 bitmask 可以辦到。所以當 max 為 a
時需要 mask 為 0x0
,而當 max 為 b
時則需要 mask 為 0xFFFF'FFFF
,而題目有提示 EXP5 要用 relational operator ,那應該填 a > b
。當 a > b
為真,則 (a ^ b) & (-1)
變成 (a ^ b) & 0xFFFF'FFFF
,得到 (a ^ b)
,最後 a ^ (a ^ b)
等於 a
。當 a <= b
同理。其中有用到 integer promotions 。
先看 relational operator 結果的型態,是 int
。
N1256 (6.5.8-6)
Each of the operators < (less than), > (greater than), <= (less than or equal to), and >= (greater than or equal to) shall yield 1 if the specified relation is true and 0 if it is false. The result has type int.
再來看 integer promotions ,由於 (a ^ b)
結果是 unsigned
,而 -1
是 int
,根據 integer promotions 的規則, -1
會改變型態成 unsigned
,而其值則為 0xFFFF'FFFF
,沒有改變。
N1256 (8.3.1.8-1)
[…] Otherwise, the integer promotions are performed on both operands. Then the following rules are applied to the promoted operands:If both operands have the same type, then no further conversion is needed.
Otherwise, if both operands have signed integer types or both have unsigned integer types, the operand with the type of lesser integer conversion rank is converted to the type of the operand with greater rank.
Otherwise, if the operand that has unsigned integer type has rank greater or equal to the rank of the type of the other operand, then the operand with signed integer type is converted to the type of the operand with unsigned integer type.
Otherwise, if the type of the operand with signed integer type can represent all of the values of the type of the operand with unsigned integer type, then the operand with unsigned integer type is converted to the type of the operand with signed integer type.
Otherwise, both operands are converted to the unsigned integer type corresponding to the type of the operand with signed integer type.
N1256 (6.3.1.1-1)
Every integer type has an integer conversion rank defined as follows:
— No two signed integer types shall have the same rank, even if they have the same representation.
— The rank of a signed integer type shall be greater than the rank of any signed integer type with less precision.
— The rank oflong long int
shall be greater than the rank oflong int
, which shall be greater than the rank ofint
, which shall be greater than the rank ofshort int
, which shall be greater than the rank ofsigned char
.
— The rank of any unsigned integer type shall equal the rank of the corresponding signed integer type, if any.
— The rank of any standard integer type shall be greater than the rank of any extended integer type with the same width.
— The rank ofchar
shall equal the rank ofsigned char
andunsigned char
.
— The rank of_Bool
shall be less than the rank of all other standard integer types.
— The rank of any enumerated type shall equal the rank of the compatible integer type.
— The rank of any extended signed integer type relative to another extended signed integer type with the same precision is implementation-defined, but still subject to the other rules for determining the integer conversion rank.
— For all integer typesT1
,T2
, andT3
, ifT1
has greater rank thanT2
andT2
has greater rank thanT3
, thenT1
has greater rank thanT3
.
延伸問題:
git log
檢索。程式碼 uint32_t
也可以換成 int32_t
,其無號有號都適用。若要算 min
則將 bitmask 的生成條件改變即可,即 a < b
。
使用 Euclidean algorithm 或稱輾轉相除法,求出 64 位元 GCD (greatest common divisor)。
GCD 演算法(又稱 binary GCD alogrithm):
以下用可位元操作的改寫。可以看出下面改寫比原算法多了 v/=2
, u/=2
的動作,其對應的是 GCD 演算法的第 3 到 5 點,相較於使用比較費力的取模操作,改使用右移與減法更適當。
在 do-while 的區塊,其本質跟原算法相同,即計算餘數,再拿餘數當除數,換句話說就是輾轉相除法,停止條件跟原算法相同,條件為整除,餘數為零的時候,所以 COND 應填為 v
。而回傳值應該也跟原算法回傳商數一樣,不過我們有先做 GCD 演算法第 3 點,先提出公因數 ,所以要再乘回到我們的商數 u
上,即 RET 應為 u << shift
。雖說是輾轉相除,但實作是使用反覆的減法代替除法(意義上跟原本的輾轉相除法的商數、餘數相同)。
延伸問題:
__builtin_ctz
改寫 GCD,分析對效能的提升;使用 GCC built-in functions 的__builtin_ctz
作改寫。
Built-in Function: int __builtin_ctz (unsigned int x)
Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined.
在 Linux 內建的 GCD 實作使用大量 bitwise 操作,有分使用 __ffs
的版本跟沒有使用的版本,有使用 __ffs
的版本會比 even/odd algorithm (binary GCD algorithm) 的速度更快。
第一個是使用 __ffs
的版本, ffs (find first set) 是在找 LSB (least significant bit) 的位數,有分硬體支援和函式庫支援,這邊指的是硬體。
方法本質也是輾轉相除法,不過有些改良,第 14 行一樣是 GCD 演算法第 1, 2 點,不過這個 r
就有意思了, r & -r
會得到 LSB ,在我們的算法中,剛開始有一步是算公因數 ,這邊使用 r & -r
代替掉我們的迴圈。
第 16 行是 GCD 演算法的第 4(5) 點(第 3 點因為用 r
來算了,所以主要動機應該是第 4(5) 點),將 b
裡 的因數都提出,假如可以被 整除(第 17 行),則回傳 r & -r
,這是因為 b
只有 這個因數,也就不需要進入輾轉相除的階段。
再來進入迴圈準備輾轉相除,第 22 行跟第 16 行一樣,假如把 的因數提光之後互質,則回傳 r & -r
,同時也是輾轉相除法停止的條件之一。下一行 a == b
也是停止條件之一,即商數等於餘數,回傳商數乘以 的公因數。
在沒有使用 ffs 的版本裡,可以看到使用迴圈取代 ffs 。最初第 38, 39 行一樣是 GCD 演算法第 1, 2 點。然後將 r
取只剩 LSB 一個位元,意思是 r
是 a
, b
兩者最大的 的公因數,即 GCD 演算法第 3 點。
從註解中可以看到 normalization ,在這裡的 norm 是 -adic norm (-adic absolute value) ,特別指 -adic norm 。簡單說 -adic order 就是一整數 所擁有 的因數最高次方數,而 的 -adic norm 則是 的 -adic order 次方之後取倒數,然後整數的長度和兩整數之間的距離可以由 -adic norm 來定義。
回到註釋, normalization 就是以 r
為基準,將 a
, b
的 -adic norm 變成跟 r
一樣,所以就是將 a
, b
右移到三者的 LBS 位置一樣,此時的 a
, b
, r
三者的 -adic norm 相同,最大的 的公因數也相同。
Let be a prime number.
For example, gives and .
(待續)
以下是 bit array (bitset) 的應用。
bitmap
是一個 64 位元無號整數陣列,而每個整數都代表著一個 bitset
,使用 p + i
來計算 bit 的 index ,是將 bitmap
所有 bits 都攤平,所以 p
會是每加一次就是加 64 ,代表一個整數有 64 位元,然後加上在 bitset
裡的位移 i
,傳到 out
裡面。
以下使用 GNU extension 的 __builtin_ctzll
作等價改寫。先觀察程式碼,改變的地方從 p = k * 64
開始,變成 while (bitset != 0)
,然後計算 trailing zero 的數量,將之與 k * 64
取和放進 out
,應該就是對應上面程式碼的 index 計算,然後做 XOR 運算,繼續下一個迴圈。所以推測應該每次迴圈就是計算一個 index ,放進 out
之後就要將那個 bit 給反轉,所以每次 r
都可以更新到左邊一個 bit 的位置,進而到 bitset
變為 0
,算完所有的 bits ,結束迴圈。
這樣 EXP6 就要填那個準備反轉的 bit ,利用 bitset & -bitset
可以得到 LSB 位置,即該準備反轉的 bit 。
延伸問題:
新算法可以跳過 0
-bit ,只對 1
作計算。根據不同的 bit density ,密度越高對新算法越不利,因每次迴圈的計算量較多。
(檢驗待測)
LeetCode - Fraction to Recurring Decimal ,給一分數的分子與分母,回傳其以字串表示的小數。若有循環小數,則將循環部份用括號括起來。
這裡對循環小數的對策是使用 hash table ,將遇到的餘數儲存,每算個新的位數,就與之前儲存的餘數作比對,檢查是否出現計算重複的餘數,即出現循環小數。
剛開始先檢查有沒有 trivial solution ,是否有分子為 或分母為 的情況。再來做正負號檢測,儲存在 sign
中。在第 35 行做了無謂的檢查,因為 n
, p
在第 24, 26 行調整為正數,所以 division
一定是正數,不需再做檢查,即該行應改為 sprintf(p, "%ld", (long)division);
較佳。
看完前半部份再來到第 54 行的迴圈,第一次迴圈 hash table 還是空的,所以 pos
會是 -1
,再來將儲存餘數進 hash table ,這邊注意到 key
是 int
,而 remainder
是 long long
,所以存進去的餘數有可能存不完整, find
在比對 key
(truncated remainder
) 與 node->key
(truncated remainder
) 是否相同時會有問題,應將 key
改為 long long
。之後將位數 i
存進 index
,插入 hash table ,所以 MMM 應該為 list_add
,而位置要跟 find
裡的 hash function 對應,所以 EEE 應該為 &heads[remainder % size]
。最後將該位數的商算出,放進 decimal
,然後更新 remainder
,進入下一個迴圈。
回到剛跳過的第 78 行區塊,在若 find
發現該餘數已經出現過了,則回傳前一次出現的位數 pos
,這時我們知道有循環小數,準備將剛才放在 decimal
的小數外圍用括號括起來,放到我們要回傳的 result
。第 80 行將 decimal
前面部份複製到 result
上,然後複製完後給一個括號,所以推測停止條件裡的 PPP 為 pos--
。之後將循環部份複製,最後補個下括號和 null terminator 就可以回傳了。
若沒有遇到循環小數,也就是在整除的時候,結束迴圈,將 decimal
複製到 result
回傳結束。
延伸問題:
例如,判斷負號只要寫作
bool isNegative = numerator < 0 ^ denominator < 0
;
搭配研讀 The simple math behind decimal-binary conversion algorithms
mm/
目錄 (即記憶體管理) 中,找到特定整數比值 (即 fraction) 和 bitwise 操作的程式碼案例,予以探討和解說其應用場景由於要從記憶體上快速找到某資料位置,一種加速方式是對資料做對齊,讓現代的 CPU 讀寫時更有效率。而使用 __alignof__
可以得知某資料的對齊大小,進而讓我們對資料位置的控制更方便。
以下 t
用 int
代入為例,中間的 anonymous structure 第一個成員為 c
是 char
型態,第二個成員為 _h
是 int
型態,有 4 bytes 。由對齊的特性, char
的地址會在 1
的倍數上,換句話說,任意地址皆可,反而 int
是 4 bytes ,所以地址會在 4
的倍數上。將這個 anonymous structure 放在 0
上, 0
為任意數的倍數,用第一個成員 c
佔據,然後以 char
這個最小單位給 _h
做位移,強制我們要計算對齊的對象 _h
的地址在 1
以後(含)。
這時有趣的事發生了,假如 t
的對齊是 1
的倍數,則此時 _h
就會剛好靠在 char
的旁邊,也就是地址 1
上,但現在我們 t
的對齊為 4
的倍數,所以 _h
不會在地址 1
上,而是出現在下一個 4
的倍數地址上,即地址 4
,而中間空的部份稱為 padding 。
根據以上的例子,對齊可以由 _h
與 0
的位移量得知,所以 M 應該指 _h
這個成員,得到前者 _h
的地址,而後者 X 對應 anonymous structure 的起始位置 0
當基準,計算位移量。
這邊有兩個問題,為何要轉換成 (char *)
?還有為何要相減,直接拿 _h
的地址不就好了。第一個問題,因為要做指標的減法,所以需要兩個指標的型態相同。而對於前者的對齊可能很多種,不如直接選 (char *)
,因為任何數都是 1
的倍數,這樣可以避免某些錯誤和編譯器的警告。
第二個問題目前只是猜測,根據 N1256 (6.5.6-9) ,兩者相減得到的型態不會是 (char *)
,而是 ptrdiff_t
,也就是會有轉換型態發生。用數學舉例,在二維空間中,兩點的座標相減得到是向量,而向量跟座標是不一樣的概念。
N1256 (6.5.6-7)
For the purposes of these operators, a pointer to an object that is not an element of an array behaves the same as a pointer to the first element of an array of length one with the type of the object as its element type.
N1256 (6.5.6-9)
When two pointers are subtracted, both shall point to elements of the same array object, or one past the last element of the array object; the result is the difference of the subscripts of the two array elements. The size of the result is implementation-defined, and its type (a signed integer type) isptrdiff_t
defined in the<stddef.h>
header. […]
N1256 (6.2.5-27)
A pointer to void shall have the same representation and alignment requirements as a pointer to a character type. Similarly, pointers to qualified or unqualified versions of compatible types shall have the same representation and alignment requirements. All pointers to structure types shall have the same representation and alignment requirements as each other . All pointers to union types shall have the same representation and alignment requirements as each other. Pointers to other types need not have the same representation or alignment requirements.
延伸問題:
__alignof__
的使用案例 2 則,並針對其場景進行解說ALIGN
, ALIGN_DOWN
, ALIGN_UP
等巨集,探討其實作機制和用途,並舉例探討 (可和上述第二點的案例重複)。思索能否對 Linux 核心提交貢獻,儘量共用相同的巨集FizzBuzz 是一個數學遊戲,剛開始由 開始往上數,每當數字可以被 整除,這時玩家不該喊數字,而是代替那數字喊 Fizz 。同樣地,數字可以被 整除,則是喊 Buzz 。當可以被 , 同時整除,則是喊 FizzBuzz 兩個同時。
上面為直覺的作法,當數字 i
可以被 整除就印出 Fizz ,當數字可以被 整除則印出 Buzz ,當可以同時被兩者整除時,則在兩個 if 檢查的時候就會依序印出。最後都不能整除的時候,則是印出該數字 i
。
觀察上面同時整除的時候,依序印出就像是兩個字接在一起,換個方式想,那可以控制字串的起始位置和長度,來選擇印出的字串為何,就像上面的 "FizzBuzz%u"
那樣,選擇 offset 和字串長度來控制要印出的格式。
先跳過 is_divisible
函式,到後面再來分析,從名字可以看出就是判斷 n
是否能被 bitmask M
對應的整數整除。那開始看主函式,使用我們剛才的想法,需要知道要印出的字串起始位置和長度,有的資訊則是可否被 , 整除,下面用表格表示相對關係。
dev3 |
dev5 |
start index | length |
---|---|---|---|
0 |
0 |
8 |
2 |
0 |
1 |
4 |
4 |
1 |
0 |
0 |
4 |
1 |
1 |
0 |
8 |
從此表可以看出 length
是每多一個可整除的因數( or )就多增加一倍,所以可以直接 KK1 填 dev3
, KK2 填 dev5
(可以互換)。
而起始位置原為 &"FizzBuzz%u"[(9 >> div5) >> (KK3)]
裡的 9
應改為 8
才對,不然不符合 KK3 的填空要求。從表格觀察,若 div5
為 0
,則起始位置為 8
,沒有右移,若 div5
為 1
,則起始位置為 4
,這兩個事件在 (8 >> div5)
就已經寫好了。那麼再來是 dev3
,若為 0
則保持原狀,若為 1
則歸零,所以 KK3 應該為右移位數為零或者右移位數大於 的一個表示式(可以將 8
歸零),所以可以填 dev3 << 2
。
延伸問題:
naive.c
和 bitwise.c
效能落差
printf
更換為 sprintf
bitwise.c
程式碼,試圖運用更少的指令來實作出 branchless;
kernel/time/
目錄的標頭檔和程式碼)
過程中,你可能會發現可貢獻到 Linux 核心的空間,請充分討論