56han
我的作業和你們相同,也是完成第 7 週測驗題測驗。
首先,謝謝你們「第 7 週測驗題測驗 - 第二題」對 m
有完整的數學證明,讓我更了解第二個判別式,我有引用數學證明到我的作業上,並貼上這篇作業的網址,若有不妥,我會再修改。
另外,我不理解第二個判別式 ___res
和 ___x
存在的意義,還有下方 if-else 的三個情境。
當初解決數學推導部分後,我並沒有繼續進一步研究該判別式的其餘部分。謝謝你提出這個問題。一旦我完全理解了這部分,能夠更系統性地進行描述運作原理,會立即更新於此處。
yy214123
yuyuan0625
請問第七周測驗題2 的判別式使用 __builtin_constant_p(__base)
有什麼用意?
這與 Constant folding 有關,可參考 你所不知道的 C 語言:編譯器和最佳化原理 (上)-[2:29:58],透過這個 builtin fuction ,去判斷除數是否為常數,若以組合語言序列的觀點來看,會有部分被 dead code elimination,故可以優化程式的執行時間。
yy214123
david965154
在 memchr
函式實作中建議可以加上 UNALIGNED
的判斷考量集及解釋,為何要先做這裡的判斷才透過 SWAR 特定字元。
感謝建議,已更新
UNALIGNED
的解釋。
yenshipotato
探討位元運算的應用
重新作答並涵蓋延伸問題
解釋上述程式碼運作原理
如果 lsb 被設置為 1,則其值必為奇數,反之。
而為減少運算量,將兩個 32 位元組合為一個 64 位元
y
= 0xAAAAAAAA
y
+ 0L = 0x00000000 00000000
= 0x00000000 AAAAAAAA
假設 x 的十六進位表示為 0x12345678
,則相加並左移後為:0x12345678 00000000
。
x
的型別是無號 32 位元整數,而 0L
是 64 位元,在 c99 有以下描述:
The rank of long long int shall be greater than the rank of long int, which shall be greater than the rank of int, which shall be greater than the rank of short int, which shall be greater than the rank of signed char.
〈 c99 6.3.1.1 Boolean, characters, and integers 〉
由此可知 long int 的 rank 比 int 高。
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.
〈 c99 - 6.3.1.8 Usual arithmetic conversionse 〉
所以 x
的型別會被轉為 64 位元的 long,將其與 0L
相加後向左移 32 位元,其結果如下:
假設 x 的十六進位表示為
0x12345678
,則相加並左移後為:0x12345678 00000000
。
假設 y 的十六進位表示為0x12345678
,則相加後0x00000000 12345678
。
而 (y + 0L
同理。
我對這部分有疑惑,為甚麼不直接:
如上方程式碼所示,去掉 (-1L >> 32)
效果不是相同嘛?那做這個 and 運算的原因為何?
設計對應測試程式如下,bit_compound
是原始包含 and 運算的,而 bit_compound_2
是對應我上方做的更動(即去掉 and 運算):
輸出結果:
根據該結果,我是不是能說 (-1L >> 32)
這個運算是多餘的?還是這其中有我沒考量到的地方?
Linux 核心中的 lib/string.c 有 memchr 實作:
這段程式會逐字元去檢查輸入的字串,直至與 c
相符。在規格書提到:
The result of the postfix ++ operator is the value of the operand. After the result is obtained, the value of the operand is incremented. (That is, the value 1 of the appropriate type is added to it.) See the discussions of additive operators and compound assignment for information on constraints, types, and conversions and the effects of operations on pointers.
〈 c99 - 6.5.2.4 Postfix increment and decrement operators 〉
所以在 while 迴圈中,會先比對 c
是否等於 *p
,接著將其增加 1(即指向下一個字元),故若有匹配,應將其指向上一個字元(p - 1
)。
memchr_opt
函式有三個輸入參數:
const void
*str
:輸入字串int
c
:欲從輸入字串中搜尋的字元size_t
len
:輸入字串的長度可以看到 *str
是 void
型別,用意是可以接受不同型別的輸入字串。接著看到:
而為了逐字元比對,所以先將其強制轉型成 unsigned char
,並指派給同為 unsigned char
型別的 src
指標變數。
首先看到第一個 while
迴圈:
while
的執行條件是 UNALIGNED
這個巨集,該巨集的輸入參數為指向輸入字串的指標變數 str
其位址。
起初不懂為何這樣 and 運算後可以判斷是否對齊,故我將變數中所存的值打印出來以便觀察:
輸出結果
經過進位轉換工具驗證後,確定了 140737488346096
是 0x7fffffffdbf0
的十進位表示,經此轉換後才可以進行對應的 bitwise 運算。
接著去思考與 (sizeof(long) - 1)
進行 and 運算的用意。規格書有關 sizeof
描述如下:
The sizeof operator yields the size (in bytes) of its operand, which may be an expression or the parenthesized name of a type. The size is determined from the type of the operand. The result is an integer. If the type of the operand is a variable length array type, the operand is evaluated; otherwise, the operand is not evaluated and the result is an integer constant.
〈 c99 - 6.5.3.4 The sizeof operator 〉
根據描述可以得知是以 bytes 為單位,而 long
是 8bytes,所以扣掉 1 後是 7,以二進位來看就是原先二的羃 bit 變為 0,其右邊 bits 皆為 1。若作為參數傳入其轉換為 long 型別後值為 8 之倍,與 7 進行 and 運算後必為 0,反之若非 8 之倍則進行 and 運算後必為 1。
回到 while
的部份,當 UNALIGNED(src)
為真時會進入迴圈中:
len
是否為 0,若為 0 則表所有字元已經掃描過一輪,而隨著每次檢查會將其值遞減,直至迴圈條件不成立。src
所指向字元與欲搜尋之字元 d
是否匹配。src
改指向下一個字元所在位址,直到位址值符合對齊條件。接著是 memchr_opt
的第二部份程式碼,即 if
判別式的部份:
此判別式的成立條件為 !TOO_SMALL(len)
,而傳入巨集的參數即 strlen(輸入字串)
,規格書有關 strlen
描述如下:
The strlen function returns the number of characters that precede the terminating null
character.
〈 c99 - 6.5.3.4 The sizeof operator 〉
故可知當輸入字串長度 \(\geq 8\) 時,判別式條件成立,而判別式的第一行程式碼將 src
轉型為 unsigned long
,這邊我不懂確切的原因,透過程式碼上下文我推測是跟後續的 mask
有關。
接著透過 or 及左移運算,使變數 mask
為欲搜尋字元的重覆擴展(擴展次數會因不同機器上的 long
型別位元數而有所不同)。
接著是 DETECT_CHAR
及 DETECT_NULL
這兩個巨集,首先先將輸入字串與 mask
進行 xor 運算,這邊在理解的過程中也出現了疑惑,我透過 vscode 偵錯模式去監看指標變數,轉為 long
型別的輸入字串會指向第一個字元,如此當初擴充 mask
的意義為何呢?不也是只比較最低的 byte 嘛? 後面的流程我可以理解,如果輸入字串的某個字元與 mask
有匹配,那兩者進行 xor 後會出現 00000000
,接著看到這個巨集:
這個巨集會根據 long
型別位元數之差異(32位元、64位元)進行對應的 bitwise 操作。
(X) - (0x01010101)
:
以 byte 的觀點出發,當
X
有任一個 byte 為0
,與0x01010101
相減後對應的 byte 會是0xFF
。
接著上一步結果與 ~X
進行 and 運算,當 X
有任一個 byte 為 0
,則 ~X
中對應的 byte 亦為 0xFF
。
舉兩個例子說明:
假設
A
、B
皆為 32 位元 long,其中A
為0x00FF00FF
、B
為0x12345678
。
首先看A
,特意將第一個 byte、第三個 byte 設為為 0:
A
-0x01010101
=0xFFFEFFFE
;0xFFFEFFFE
&~A
=0xFF00FF00
接著看B
,不具為 0 的 byte:
B
-0x01010101
=0x11335577
;0x11335577
&~B
=0x01030107
最後與 0x80808080
進行 and 運算,將 1byte 分為左右 4bits 來看,若原先具 0 byte,左邊的 4bits 其最高位必為 1
。示意圖如下:
結論來說,先透過 xor 運算來判斷欲搜尋字元是否存在,若存在必具某 byte 為 0,而此 byte 經運算後會使原先 if 判別式的條件成立,隨即 break(表示已找到),而另一方面:
這邊起初在看時不懂,想說為什麼遞增與遞減 LBLOCKSIZE
不太一致,後來我也去監看兩指標其遞增及遞減所造成的記憶體位址變化後,才解決了這方面的問題。
由於
arsc
型別是unsigned long
,其每次遞增會將記憶體位址增加sizeof(unsigned long)
這麼多。
比較 Linux 核心原本 (與平台無關) 的實作和 memchr_opt,設計實驗來觀察隨著字串長度和特定 pattern 變化的效能影響
先前提到 Linux 核心中的 lib/string.c 有 memchr 實作:
在先前 2024q1 Homework1 (lab0) 我在排序法效能比較的部分遇到了困難,當時遇到的問題是:
測試資料準備以及對應的統計分析,為什麼我設計的測試資料是 1000 筆?而每筆是由 1K 到 1000K 個長度介於 5 到 15 的隨機字串所組成,目前還是無法針對該部份提出對應的統計分析,會不會根本不需要這麼多?
趁著這次比較 memchr
與 memchr_opt
我想在這個問題點上有所突破,之前在 M01: lab0 亂數 + 論文閱讀 提到 Linux 中有工具 ent
可用於計算輸入字串的 entropy,char 的最大 entropy 為 8,所以要盡可能滿足這個理論值:
長度為 100 ,此時 entropy 離理論值有一段差距,但由卡方檢定所計算出的結果可知此時是符合 Uniform distrubution。
相較於長度 100,entropy 提高許多。
以上述結果來看,我認為字串長最小要從 10K 開始。
根據中央極限定理(CLT),當樣本數 \(\geq 30\) 時,樣本均值的分佈會接近常態分佈。然而,我在實驗設計中遇到了一些疑問。中央極限定理通常假設存在一個母體(例如,包含大量長度為 10K 的字串),我們每次從中隨機挑選 30 個字串來計算處理時間並取其平均值。經過多次挑選後,這些平均時間的分佈會趨近於常態分佈。
然而,這與我的實驗設計有所不同。我的實驗設計如下:
對照中央極限定理看下來,在我的實驗中並不是針對各長度字串有設計一個母體,那這樣我每個特定長度字串的樣本數量該如何訂定?是否有其他方法可用於解決這部份問題?
實驗設計:
研讀 2022 年學生報告,在 Linux 核心原始程式碼找出 x86_64 或 arm64 對應的最佳化實作,跟上述程式碼比較,並嘗試舉出其中的策略和分析
下一步:貢獻程式碼到 Linux 核心
Phoronix 報導
Linux 核心原始程式碼對應 x86_64 的實作在 linux/arch/x86/lib/string_32.c 中,程式碼如下:
試著去理解上方 inline aseembly 各指令的用途,所以我去參考了 Intel® 64 and IA-32 Architectures Software Developer Manuals。
這邊依程式上下文,我的解讀是輸出/入參數與 inline aseembly 間的傳遞,但我找不到 "a"
、"0"
、"1"
這些代表的意義為何?
repne
在手冊中的 Vol. 2B 4-555 有相關描述:
此為一種 prefix instruction,
重新作答並涵蓋延伸問題
2
解釋上述程式碼運作原理
div64
是一個接受兩個參數的巨集,用於計算被除數 n
與除數 base
相除後的商數。此巨集會根據不同的情況選擇較優的除法策略。
注意書寫規範:
已修正。
成立條件為除數是常數且是 2 的冪,若某數 為2的冪,其二進位表示法僅有一個 1,而將其 -1 後會使原先為 1 的位元變成 0,其右邊所有位元均變成 1,舉例如下:
\(2 = 00000010,2-1 = 00000001\)
\(4 = 00000100,4-1 = 00000011\)
\(8 = 00001000,8-1 = 00000111\)
再將被除數與 -1 後的除數做 and 運算,結果即為餘數。接著對除數取 log,以取 log 後的結果作為被除數 n
右移運算的次數,即可求出商數。舉例如下:
假設 \(n =13,base=8=2^{3}\),分別以二進位表示:
因為是除以 \(8\),以二進位除法的觀點來看就是會把 \(n\) 的 msb 消除,保留剩餘的 bit。
成立條件為除數是常數且除數不為 0
原先被除數為 64 位元,首先將其較低 32 位元 assign 給 __n_lo
這個無號 32 位元整數。
巨集 __div64_const32
將被除數與除數作為參數傳入此巨集,首先求出一個與除數 msb 相同的數,根據註解:
\(m =\frac{(p \times 2^{64} + b - 1)}{b}=\frac{(p \times 2^{64})}{b}+\frac{b-1}{b}\)
又 ~0ULL
為 64 位元全 1 的無號整數,相當於 \(2^{64}-1\),所以:
會近似於加號左半部的 \(\frac{p \times 2^{64}}{b}\)。
接續的下一行程式為
而 \(m =\frac{(p \times 2^{64})}{b}+\frac{b-1}{b}\),這邊我有疑惑,為何程式不是
這樣不是更符合加號的右半部?
問題已解決,可如下推導
根據除法公式:
\(a=bq+r\)
其中 \(a\) 為被除數、\(b\) 為除數、\(q\) 為商數、\(r\)為餘數。
\(\therefore a+1=bq+(r+1)\)令:
\(2^{64} - 1 = q \times b + r\)
\(2^{64} =q \times b + r+1\)根據註解可知:
\(m =\frac{(p \times 2^{64} + b - 1)}{b}=\frac{(p \times(q \times b + r+1)+b-1)}{b}\)展開可得:
\(m =\frac{pqb+pr+p+b-1}{b}=\frac{pqb}{b}+\frac{(r+1)p+b-1}{b}\)最後通分:
\(m= pq +\frac{(r+1)p+b-1}{b}\)
其中:
\(q\) = ~0ULL / \(b\)
\(r\) = ~0ULL % \(b\)
由上述推導過程,即可說明註解與程式等價。總結來說,bitwise 操作相當於整數除法,不會考慮到餘數的部分,如此會使數字有誤差,所以在 do while 中才會有如此的操作來彌補損失的誤差。
減少非必要的 :::warning
標示,務求清晰且明確的描述
這個成立條件代表 \(0\leq n \leq2^{32}-1\),所以 n 可用 32 位元表示,接著就是用傳統的運算方式去求得商及餘數。
若前三個判別式之條件均不滿足,則會呼叫 __div64_32
這個函式。
__div64_32
初始化:
rem
初始化為被除數的值。b
初始化為除數的值。rem
的較高 32 位元之值並將其 assign 給 high
。首先判斷 high
是否足夠大,若夠大求其除以 base
後之商值,求出商值後再左移使其恢復實際值,並將實際值 assign 給 res
。
相當於
res
的較高 32 位元為被除數較高 32 位元之商。
求算完較高 32 位元後,接著要處理較低的 32 位元,程式碼如下:
舉個例子來解釋,假設:
rem
= 0x0000 0007 0000 0000
base
= 3那麼 high
就會是 7
(rem
>> 32),接著求 \(7 \div 3\) 之商(即 2),接著將商值存回 high
中,最後將 2 左移 32 位元並 assign 給 res
。
res
=0x0000 0002 0000 0000
由前述可知,目前的 high
為商值,將其與 base
(即 3)相乘並左移 32 位元,此舉等同於求出被除數較高 32 位元除以 3 後的商,最後再以 rem
扣掉該值。
rem
=0x0000 0007 0000 0000
-0x0000 0006 0000 0000
=0x0000 0001 0000 0000
接著可以看到有兩階段的 while 迴圈:
注意書寫規範:
已修正。
在初始化階段,b
被賦予指派的值即為除數,所以在第一階段的 while 中,透過倍增的方式去找到最逼近 rem
的 b
。而對於另一個初始值為 1
的變數 d
,他也會以倍增的方式,記錄商值的倍數。
接著看到第二階段的 do-while 迴圈,在上一階段 res
紀錄著較高 32 位元除以 base
後的商值,所以在第二階段,透過判斷 rem
及 b
間的大小,將剩餘位元與 base
相除後之商值加入 res
,最後即可得最初被除數除以 base
之實際商值。
在 Linux 核心原始程式碼找到相關的程式碼並設計實驗 (使用 Linux 核心模組) 探討程式碼的效益和限制
研讀 Linux 核心程式碼:div4.c 的過程中,在 __div64_32
這個函式第 47 行~第 63 行的位置:
這部分程式碼的解釋可參考上方 第四個判別式。此處觀察到 while 迴圈中的變數 b
及 d
,兩變數倍增的行為是採用傳統的加法運算,而在 do while 迴圈部分是採 bitwise 操作來達到倍減,我的想法是可將 while 迴圈的程式修改如下:
與授課老師討論後,老師給予的建議是:
原先我天真地認為
調整加號前後的空白字元就可以嘗試提交 patch 了,經授課老師說明後,才知道這種沒有實質功能變更的 patch 很難被收錄。
授課老師提示用下方方式進行編譯並觀察。
查看編譯後的組合語言我所用的工具為先前一對一面談老師推薦的 godbolt,由於欲比較差異的程式碼位於第四個判別式,所以需設計不符合前三個判別式的參數。
後來沒有用 godbolt,因為顯示出來的組合語言序列並沒有變化。
所以我的用於測試的 main
函式如下:
原先 Linux 核心程式碼編譯後的結果:
而我將加法改為 bitwise 後的編譯結果:
兩者並無任何差異,觀察這部分的組合語言序列:
根據 wikipedia: Apple M1 的介紹,我這台電腦所使用的指令集架構為 ARMv8-A,參考 arm Developer 的描述,對於暫存器的部分有以下介紹:
使用 Graphivz 重新製作上圖。
我的理解為:
編譯器先將兩者相除後的餘數(即 6)存至 w8
暫存器。
在 main
函式中,被除數為 0x100000000
、除數為 10
,而被除數的 10 進位表示為 \(16^{8}= 4294967296\)。參考 ARM Compiler armasm Reference Guide Version 6.01 對於 movk 的說明後,第一行先將 10 進位的 39321 存到 w9
暫存器的較低 10 位元,接著再將 10 進位的 6553 存到 w9
暫存器的較高 16 位元。
試著計算 w9
暫存器中的值:
\(6553_{(10)} = 1100110011001_{(2)}\)
\(39321_{(10)} = 1001100110011001_{(2)}\)
所以 w9
= \(0001100110011001 1001100110011001_{(2)}\) =\(114631_{(10)}\)
跟預想的不太樣,我以為 w9
會是商值。我不懂這樣商值是怎麼求算出來的。
查閱 Arm 手冊,注意看指令描述
去降低編譯器最佳化的等級,原先是:
改為
再次進行觀察,發現編譯後的組合語言序列與原先 $ gcc -O2 -S
沒有任何差異。
回顧上方的比較實驗,由觀察到的組合語言序列可知,在使用編譯器最佳化 -O2
的情況下,我所做的改變並沒有帶來實際影響,這個學習過程是有趣的,但我目前還無法去解釋這個結果的主因。
接著出於好奇如果去變更原先為 bitwise 的 do-while 迴圈,將其改為傳統除法操作,那應該會有變化吧?變更如下所示:
一樣使用 gcc -O2 -S
進行編譯。
原先 Linux 核心程式碼編譯後的結果:
而我將 bitwise 改為除法運算後的編譯結果:
晴天霹靂,兩者仍無差異,我真的很好奇這部分又是為什麼?因為根據之前教材所學習的知識,透過 bitwise 來取代除法、乘法運算,進而提升效能,在這個實驗中理應要有變化才對,請問老師我是否忽略了什麼細節?
「透過 bitwise 來取代除法、乘法運算,進而提升效能」要看場景,若在 Linux 核心,因為無法使用 FPU,所以這策略有效,但你若在 Apple M1 這樣的晶片執行應用程式,就不是如此。
問題一:經過高等級編譯器的最佳化後,傳統的乘法和除法運算與 bitwise 操作無異,那為何不都使用高等級編譯器進行優化就好?
授課老師回覆:參見「你所不知道的 C 語言:編譯器和最佳化原理篇」的內容和解說錄影。編譯器最佳化常採取保守的策略,連 code motion 這樣單純的策略都很難全部適用。
問題二:是否有某些情境比較嚴苛,只能使用普通的編譯器來編譯?
授課老師回覆:有,例如要確保產生的機械碼符合預期 (形式化驗證),參見 形式化驗證 (Formal Verification)
重新作答並涵蓋延伸問題
在並行程式設計: Atomics 操作中提到 C11 規範了記憶體對齊的操作,在 stdalign.h
中定義了:
這邊我看不懂,以 alignas(16) int x
為例,教材中的描述是 x
的記憶體位址會對齊 16,是否可以理解為會是 16 的倍數?以及為何這樣操作可以避免 false sharing?
注意 L1 cacheline 的寬度
<stdatomic.h>
根據 C11-7.17 中描述,此標頭檔中定義了用於執行緒間共享資料 atomic 操作的巨集及函數(函式)。
區分「函數」和「函式」。
務必本課程教材規範的術語。
好的,現在開始會注意。
起初我不懂範例程式 atomic.c
的執行結果
在 main 中:
傳入了 10000 作為 nLoops
之值,裡頭的 for 迴圈迭代 10000 次後,atomic_float_obj.value
應該是 10000 才對,但執行結果實際上是 20000。
後來閱讀了並行程式設計: POSIX Thread 後,才知道我遺漏了主執行緒(main),實際上經由 pthread_create
所創建的子執行緒以及主執行緒均會執行 atomic_val_inc(10000)
,故最終的執行結果才會是 20000 而非原先預想的 10000。
釐清這邊的觀念後,才看懂教材中描述之
若將 while(atomic_flag_test_and_set(&atomic_float_obj.flag))
改為 while (0)
後會導致輸出結果不一的由來。
3
1.研讀程式碼註解提及的論文〈Cilk: An Efficient Multithreaded Runtime System〉,解釋程式碼運作原理
結構體:
教材中對個成員的描述如下:
code
:各執行緒要啟動該任務時所要執行的函式之指標,且輸入至該函式的參數是 work_t 本身的 referencejoin_count
:用來計算這個 work 還缺少了多少 arguments 才得以進行args
:即 work 執行所需要的 argumentsthread
初始化:
對照 main
函式及 init
函式會注意到下方程式片段:
由此可知每條執行緒一開始所維護的 deque 如下(size 為 8):
push
操作push
函式的功能為將新的工作放入當前執行緒所維護之 deque 的 buttom 端。
首先看到函式中的 if
,每條執行緒隨著新工作的加入,其維護的 deque 會逐漸變化,由初始的:
隨著新工作的加入,buttom 所指位置會遞增:
在這種狀態下,變數 t
= 0、變數 b
= 8,此時會大於 size - 1
(即 7),故代表 deque 已滿,需執行 resize
擴充,擴充後的 deque 其 size 會大兩倍:
接著是將新的工作放置 bottom 所指位置(即 A[8]),並將 bottom 指向下一個位置(即 A[9]):
故可知 DDDD
要填入 b + 1
,但這邊我有點問題:
為何新的工作要存入 &a->buffer[b % a->size]
?若改為&a->buffer[b]
是否也是相同效果?
搭配 ThreadSanitizer 確認
take
及 steal
操作:兩者相關性較高,我將其放在一起討論,根據教材所描述:
take
:執行緒從自己維護的 deque 之 bottom 端取工作來執行(即位於 bottom -1 的工作)。steal
:閒置的執行緒從其他執行緒維護的 deque 之 top 端取工作來執行。個別程式碼如下:
首先要先理解 atomic_compare_exchange_strong_explicit
的用法,以下是參考 c11:
_Bool atomic_compare_exchange_strong_explicit( volatile A *object, C *expected, C desired, memory_order success, memory_order failure);
〈 c11 - 7.17.7.4 The atomic_compare_exchange generic functions 〉
此巨集為布林型別,會去比較 object
與 expected
是否相等,若相等回傳 true 並將 desire
存入 object
中,反之若不相等回傳 false 且將object
存入 expected
。
教材提到是刻意從 top
的方向去取,亦即取完後得將 q->top + 1
,此提示顯然就是 AAAA
的相關資訊,示意圖如下:
上圖的情境為某執行緒其 deque 僅存最後一個元素,若此元素沒有被其他執行緒 steal
,那麼cmpxchg 成立,會將 AAAA
assign 給 &q->top
,故可知 AAAA
即為 t + 1
:
可運用上圖來解釋程式碼中 else
的部份(即 t > b),此時若再次執行 take
,示意圖如下:
此時 t > b
成立,即 deque 為空,應該要恢復 bottom
指向的位置,故 CCCC
應填入 b + 1
。
接著看相反的情況,回到 cmpxchg 的部份,若當前 deque 也僅剩最後一個元素:
這邊我不懂,我的想法是:
在這兩段程式碼期間發生了 work stealing,在 steal
函式可以看到其也具備相同的 cmpxchg 程式碼,對於如此的設定,我的想法是若 steal
成功執行,會將 t + 1
assign 給 &q->top
,而 take
中的 cmpxchg 就會出現 *object != *expected
的情況。
觀察工作佇列的任務數量變化。
與前面 push
一樣,也有 atomic_load_explicit
的疑問,為何需要 % a->size
?
本質上是 ring buffer
thread
這個函式整合了前面的take
及 steal
,各執行緒的 while 迴圈會去判斷其維護的 deque 是否為空,非空執行 take
,若為空則會依序的去巡察其他執行緒的 deque,若其他執行緒的 deque 有多餘的工作,則協助該工作的執行。
main
接著看到主程式的部份,前幾行的程式用於生成執行緒及各自維護的 deque,因為#define N_THREADS 24
,故總共會有 24 條執行緒。生成執行緒後,因 nprint
宣告為 10,可理解為每個 deque 初始狀態下會有 10 個工作。
而 donework->join_count
即為工作數的總和,經上述可知其值為 240(N_THREADS * nprint
),可留意下方兩個函式:
當 print_task
完成時,會呼叫下方的:
會將 join_count
這個紀錄工作總數的計數器減 1。
以目前的理解來看,整體架構上有兩種 work:
而每個普通的 work 其 join_count
恆為 0,且 args[1]
為 done_work
,這樣看下來,為何 work->join_count
還有存在的必要性?
為何不修改程式碼並充分實驗?不要「舉燭」。
按照 2024q1 第 9 週測驗題 進行編譯:
出現了下方的警告資訊:
目前還沒理解造成這些警告的原因。
注意
_Atomic
標示的方式
接著進行測試:
2.指出上述程式碼可改進之處並著手進行,如記憶體釋放和量化分析性能和延展性 (scalability)
工程人員說話要精準,避免說「覺得」。先推論後驗證。
好的,現在開始會注意。
目前我覺得 deque 的 size 有可改進之處,在 main
可以看到:
這代表每個 deque 其初始大小為 8,但實際上:
nprint
宣告為 10。
我在 main
中呼叫 push
函式前加入了下列程式碼:
如此可印出是第幾條執行緒的第幾個 work 的資訊。接著在 push
函式中的判別式加入下列程式碼:
一旦有發生須擴充 deque 大小的情況,也會印出相關資訊。
而後看到終端機的輸出結果:
我的想法是能不能改為
init(&thread_queues[i], nprint);
如此避免每個 deque 在最初就必須 resize 一次。
3.利用 work stealing 改寫第 6 次作業提及的並行化快速排序程式碼,在 Linux 使用者層級實作,並確保能充分運用硬體資源
4.研讀 Linux 核心的 CMWQ,討論其 work stealing 的實作手法
相關教材:
CMWQ 關鍵資料結構:
work_queue
我發現在 2024 年 Linux 核心設計/實作課程作業 —— integration(C) 所提及的:
其所在位置有變化,在比較早的 kernel 版本(V5),相關程式碼位於
workqueue.h ,而在較新版本(V6.8),相關程式碼位於 workqueue_types.h,而函式實作的相關程式碼位於 linux / kernel / workqueue.c
觀察上述結構體可知,在 work_struct 結構體中置入了 list_head 物件,此即作業一指定佇列操作所用的雙向環狀鍊結串列用以將 workqueue 中的 work item 連接起來,
而用於執行這些 work item 的 worker 定義在workqueue_internal.h,其結構體如下:
注意書寫規範:
已修正。
在註解中有提到,L、I、K 等等相關的細節可參考 workqueue.c
:
L: pool->lock protected. Access with pool->lock held.
K: Only modified by worker while holding pool->lock. Can be safely read by self, while holding pool->lock or from IRQ context if %current is the kworker.
S: Only modified by worker self.
A: wq_pool_attach_mutex protected.
I: Modifiable by initialization/destruction paths and read-only for everyone else.
workqueue_struct
此結構體程式碼可在 workqueue.c
中找到
worker_pool
struct list_head workers;
如教材所述,會管理所有的 workers,所以這個串列可以視為 workers 們的集合。
pool_workqueue
避免「舉燭」,明確說明 Linux 核心原始程式碼和上述論文行為的關聯。你可撰寫對應的 Linux 核心模組來測試。