contributed by < yy214123
>
執行可以看到這個專案是 的井字遊戲,其勝負的判斷規則在 game.c
中的:
並在 main 函式中透過 while 迴圈,來不斷檢查當前遊戲的狀況。
勝利的條件有四種,分別是:
此處要先了解關鍵的結構體定義:
搭配相關的結構體宣告:
最外層的迴圈每次會針對一種勝利條件來對整個盤面進行檢查,內層迴圈會對特定勝利條件去檢查是否符合條件。
這邊我將迴圈中的 i 及 j 印出,觀察每次的檢查對象:
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | 0,0 | 0,1 | 0,2 | 0,3 |
1 | 1,0 | 1,1 | 1,2 | 1,3 |
2 | 2,0 | 2,1 | 2,2 | 2,3 |
3 | 3,0 | 3,1 | 3,2 | 3,3 |
初始化會將(0,0)~(3,3)設為
' '
。
從此處可以發現,實際上的註解有誤,常見的 column 及 row 的定義應該如下:
於是著手進行修改:
並將此修改提交 pull request 至原始專案中。
過程中出現了意外的小插曲,由於自己的疏忽誤將 ROW 打成 RAW,經由 reviewer 提醒須重新修改。
我沒有相關經驗,所以我最一開始的作法在原先的 commit 之上,再新增一個新的 Commit 8af6a76,但 reviewer 指出:
這意味著我需要:
我進行了以下的操作:
git push --force
即可。算是蠻意外的收穫,所以不要怕做錯事,沒有這次的疏忽也不會有更進一步了解相關 git 運作的經驗。
基於上一個 pull request 可以注意到,每下一步棋,第一步是基於 column major 掃描整個棋面是否滿足勝利條件。
實際上 4 * 4 的二維棋面就是 1 * 16 的陣列,是連續的記憶體配置,我參考了:
我認為既然兩種勝利的機率是等價的,那可以先 ROW 的判定再進行 COLUMN 的判定,如此可以充分運用 temporal locality 的特性。
因此我對程式做了以下修改:
將兩個敘述對調。
嘗試提交 pull request 到原始專案中,目前還未被合併,過程中獲得了一些啟發:
最初的提交是 commit ddf7e45,reviewer 同樣是授課老師有邀請回來演講的 linux 核心維護者,對於我這次的提交,他留下這樣的評論:
Looks good.
However, I would appreciate more explanation on why prioritizing row-wise checks improves spatial locality.
Also, I'm curious if there's any observable performance gain or data showing how much faster this change might be.
讓我印象深刻的是,在當時的演講中,他提到了貢獻程式碼到 linux 核心很重要的環節:
顯然我在沒有做效能評比的狀況下,這次的 commit message 相當沒有說服力,但當下我沒有什麼想法,所以我將上面所執行的結果,以及我參考的資料附於評論中。並依據其建議修改了 commit message,新的提交為 commit c77acdb。
後來此專案的 owner(即授課老師)也參與了評論,owner 指出:
Instead of appending the references supporting this proposed change, show experimental evidences.
從這邊我也意識到,貢獻開源專案沒有一定的 SOP,reviewer 並沒有強烈要求我要進行效能評比,但 owner 要求我進行效能評比,我得出的結論是以後還是乖乖做效能分析!畢竟數字才能真正的說服人。
目前的想法是,我直接產生所有會勝利的棋盤,並針對這些勝利棋盤運行 check_win 函式。
在 lab0 的基礎上,將 Fisher–Yates shuffle 演算法及 do_time 引入我的測試中。
測試程式碼如下:
兩者本質上沒有差異,僅是掃描的順序不同。
在此實驗中,生成的測試資料保證每一組棋盤皆存在一組勝利條件,並且均勻涵蓋各種勝利情況。
在 10 × 10 的棋盤下:
在 50 × 50 的棋盤下:
在 100 × 100 的棋盤下:
由於在資料生成過程中會包含所有可能的勝利條件,對於使用 row-major 的程式來說,掃描 column 勝利條件會產生額外記憶體訪問成本;反之,column-major 亦然。
在相同為 100 × 100 的棋盤條件下,進一步調整測試資料生成策略,使其中 row 方向的勝利樣本佔比提高。(Row:Col=2:1)
在此「不公平」條件下,row-major 儲存方式展現出明顯的優勢,並與 column-major 拉開了更大的性能差距。
使用 perf 進行分析:
指標 | column-major | row-major |
---|---|---|
Instructions | 1,585,771,268,229 | 1,426,533,169,134 |
CPU cycles | 427,169,360,881 | 390,890,530,455 |
IPC (insn/cycle) | 3.71 | 3.65 |
Cache references | 3,505,765,178 | 3,411,224,518 |
Cache misses | 2,549,701,006 | 2,526,058,997 |
Cache miss rate | 72.73% | 74.05% |
Time elapsed (s) | 85.00 s | 77.88 s |
受 reviewer 提醒,我上面的實驗並不公平,應該設計對稱的實驗比較:
Perf analysis summary:
Metric | column-major | row-major |
---|---|---|
Instructions | 1,430,153,246,436 | 1,585,481,506,424 |
CPU cycles | 390,527,903,361 | 423,555,794,132 |
IPC (insn/cycle) | 3.66 | 3.74 |
Cache references | 3,416,572,300 | 3,496,775,391 |
Cache misses | 2,515,143,524 | 2,549,693,611 |
Cache miss rate | 73.62% | 72.92% |
Time elapsed (s) | 77.70 s | 84.30 s |
前述兩個實驗皆為「保證勝利」的條件下進行,與真實下棋過程不同。在實際情況中,棋局常需多次判斷後才出現勝利條件。舉例如下:
此類情況下,若從左上開始掃描,column-major 可能需檢查多次才能辨識出最左下角的 X(假設五子棋)。
為了公平比較,我生成兩組互為轉置的 5 × 5 棋盤:
這兩者皆需進行多次無效檢查,之後才發現勝利條件,模擬「邊緣判斷」的效能情況。
在執行 1,000K 次 的 perf 性能監控後,結果如下:
指標 | column_major | row_major |
---|---|---|
Instructions | 20,042,179,354 | 9,528,171,690 |
CPU cycles | 4,974,042,956 | 3,064,415,787 |
IPC (insn/cycle) | 4.03 | 3.11 |
Cache references | 205,075 | 115,548 |
Cache misses | 28,503 | 20,176 |
Cache miss rate | 13.90% | 17.46% |
Time elapsed (s) | 0.971 | 0.597 |
在這種接近實戰的條件下,row-major 方式的效能優勢更加明顯,幾乎在所有指標上都優於 column-major。儘管其 cache miss rate 較高,但這是由於其總 cache reference 數量減少近一半所致,整體表現仍然更為優秀。
綜合實驗二及實驗三,row-major 在實作上具有更佳的空間區域性(spatial locality),尤其當勝利條件以橫向居多或需多次判斷時,表現優勢更加顯著。
因此,未來若設計電腦下棋演算法時能引導 AI 傾向以 row 為主進行布局,不僅在演算法判斷上較為直接,亦可獲得更好的效能表現,尤其在大型棋盤或頻繁檢查勝利條件的場景中,將有顯著助益。
目前雖然比上次修課更懂如何進行實驗了,但很多面向我還是沒有顧及到,就以實驗二來說,我的初始規劃並不公平。
另外是英文書寫的部份,我還有很大的改善空間,在 comment 中錯誤的描述會產生誤會。要提醒自己要多檢查才送出。
sysprog21 #11
此更新同 jserv #42,一樣也是對錯誤的註解進行修正。
syspro21 #20
優先檢查 row。
首先先編譯並掛載 kxo
,接著執行下列命令即可看到電腦對奕:
接著開啟另一個終端機視窗執行下列命令:
此時可以監看最新棋盤的動態,倘若此時輸入 ctrl+p 就會看到原先執行 $ sudo cat /dev/kxo
的終端機與當前的終端機皆停止。
popcount 對應到 C 程式的實作:
簡單的用一個二進位當作輸入範例,假設 v
為 0b11100011
,當 v
非 0 就會進入 while 迴圈:
v
與 v - 1
進行 and 運算。此時
v
變成 0b11100010`,接著將計數 +1。
後面也是延續上述步驟。而上述程式可如下優化:
這邊數學公式沒看懂。
先以簡單的範例進行,5 % 3 = 2 這顯而易見,而 5 的 2 進位表示為 101,相當於 4 + 1,所以:
那由過程我們可以寫出下列公式:
而 非奇即偶,所以 ,這個通式由下列兩個所組成:
將此結合,可以得到:
這就相當於「位於偶數位的 1 加起來」減去「位於奇數位的 1 加起來」。將 popcount 引入即: