contributed by < sammer1107
>
Interpreter | Time |
---|---|
Default | 3.063 s |
w/ computed gotos | 1.771 s |
USE_COMPUTED_GOTOS
: src/vm/vm.c若要模擬一個 RISC-V 處理器,我們勢必要有一個資料結構來儲存機器的狀態,這個最重要的狀態儲存在 riscv_private.h
的 struct riscv_t
當中。各欄位的意義如下:
欄位 |
作用 |
---|---|
bool halt |
機器是否還在運行,若 true 則停止模擬 |
riscv_word_t X[RV_NUM_REGS] |
用來代表 32(RV_NUM_REGS) 個 32 bit 的 register (因為模擬的是 RV32I) |
riscv_word_t PC; |
用來紀錄 program counter ,也就是要抓取下一個指令的位置 |
riscv_user_t userdata |
1. 用來紀錄 CPU 之外的環境的狀態,包含了記憶體(儲存了程式與變數與常數)等等的資訊。 2. riscv_user_t 其實是 void* ,也就是說 userdata 是什麼很彈性,依據我們想要提供的執行環境 (EEI, execution environment interface) 可以再作調整。3. 當前的實作使用 state_t 作為 userdata 的結構,還儲存了 stdio 讓模擬的程式可以透過 system call 輸出結果 |
struct riscv_io_t io |
userdata 決定了模擬記憶體 / IO設備的資料結構,io 則是儲存了許多函式介面,規範了 RISC-V CPU 如何真的從模擬的記憶體中讀寫資料,以及如何作 system call。 |
uint32(64)_t csr_... |
這些為 Control and Status Registers,有各種特殊功能,其中包含紀錄 cycle 數的 csr_cycle ,剩下的 CSR 與 interrupt 與 Exception handling 相關,皆為 machine-mode CSR,因此以 m 開頭。 |
再上一部份提到 riscv_t
中的 io
成員作為 riscv_t
與 memory 溝通的介面,以下為 io 的定義:
riscv_t
的實作與剩下的執行環境實作分開,只要透過 io
的介面接和起來就行了。有了介面之後,當然還要有對應的實作。 IO 介面的實作要與 userdata
的設計結合,而目前的 userdata
設計為 state_t
:
io.h
中的定義如下:
整個 32bit 的 address space 被分成 64k 個大小為 64k 的 chunk。syscall.c
中。memory_delete
中有這一段 FIXME,說著 for 迴圈的範圍需要修正。m->chunks
是 array of chunk_t*
,所以這部份的範圍應該給成:我們先看一下目前的 rv_step 實作原理:
rv_step 會一次總共執行 cycles
個指令才會回傳,在達到目標的指令數以前,上面的程式碼就是不斷的:
opcodes
table,拿到對應的函式位置並且呼叫完成指令執行這樣的跳躍機制會會讓 CPU 的 branch prediction 機制無法有效執運作,原因為 op(rv, inst)
這個呼叫對應到一個跳躍指令,但往往跳躍到不同的位置,會讓 CPU 分支預測器頻繁猜錯。
解決辦法是將每個 op 的呼叫分開,並在每個呼叫後放置一個獨立的跳躍指令,如下:
以往寫程式注重的是程式的重複利用,但在這裡我們可以透過產生重複的跳躍指令來更好的發揮 CPU 的 branch prediction 機制。
這樣做的原理是由於 CPU 是透過地址來區分每個跳躍指令,而現在每個 op handler 之後都有了獨立的跳躍指令,所以 CPU 相當於多了一個資訊(上一個執行的指令)來判斷下一個跳躍位置。
舉例來說,假設我們使用一個非常簡單的 branch prediction,總是預測跳躍與上次一樣。而且,op1 之後常常接著 op2。
direct threading 可能會比 indirect threading 來得快,因為更少的記憶體存取,但在 x86_64 上,若每個 opcode 都要對應 64-bit 指標操作,會是可觀的開銷
啟發: RISC-V 指令之間是否存在相關順序?也就是說,例如
lw
指令之後會不會伴隨著add
呢?是否我們可先把指令的實際執行頻率統計出來,再分析相鄰的指令,最後將若干個指令對應到特定的程式碼
Makefile
加入 CFLAGS += -fno-gcse -fno-crossjumping
,如此才能讓 gcc 產生更好的程式碼179d878a4f5180ce5d03732e05e223db57b06418
已加入 build/coremark.elf
,可執行 build/rv32emu build/coremark.elf
(需要等待),預期可見以下輸出:
詳閱 CoreMark 以得知其具體意義。
#ifdef ENABLE_COMPUTED_GOTO
的檢查。若沒有開啟 computed goto,程式相當於以前的實作。若開啟了則會使用新的實作。
TABLE_TYPE
macro 來根據情形設定型別。OP(...)
macro 來包裝,如此可以根據開啟 computed goto 與否,來決定要變成 label address 或是 function。NULL
,如此在開啟 computed goto 的情況下經過 OP(...)
展開後變成不合法的 &&NULL
。為了更好的處理無實作的指令以及可能會開關的 extension,我將未實作的指令定義為 OP_UNIMP。如此像是 OP(msub)
這個未實作的指令會以這樣的展開順序被導向專門處理未實作指令的 handler: OP(msub)
&&op_msub
&&OP_UNIMP
&&op_unimp
。NULL
的地方也都填滿了 OP(unimp)
,如此當程式真的出現這樣的指令時,也不會讓程式直接跳到 NULL
去而直接死掉。TARGET
macro 來產生對應每個 opcode 的 goto label,每個 goto label 底下都有 EXEC
來直接呼叫對應的 handler(而不是以前用 function pointer 呼叫的方式)。在每一個 goto 區塊的尾巴,用 DISPATCH
macro 來跳躍到下一個指令的 label。cycles
個指令。TODO: 考慮到多樣的編譯器支援,前述 computed-goto 僅在 gcc/clang/icc 存在,像是 Microsoft Visual C++ Compiler 就缺乏該特徵,因此最好透過條件編譯 (即引入 #if defined(ENABLE_COMPUTED_GOTO
一類的敘述) 區隔新加入的程式碼,並確保原本 switch-case 的敘述可編譯。
可定義新的巨集,例如 OP
,這樣就可依據是否 ENABLE_COMPUTED_GOTO
,產生對應的程式碼:
同理,可針對 switch-case 撰寫對應的 DISPATCH
巨集。
-fno-gcse
這個選項來加優化效能。我想原因是我們透過 macro 刻意產生了重複的程式碼,但是編譯器可能會想把重複的程式法優化掉,因此可以加入這個選項來避免編譯器搗亂。另外 -fno-crossjumping
的作用也類似。在這裡我以 puzzle.elf 作為比較標準。
可以看到 branch-misses 的數目少了至少一半,而執行時間則有了 23% 的提升。
雖然預期加入 -fno-gcse -fno-crossjumping
之後會有比較好的效能,可是不知道為何在 riscv.c
的編譯加入這兩個選項後,效能不僅沒有提升,好像還弱了一點。執行時間平均起來有稍慢,且 branch-miss 數量也有增加。
因為 coremark 似乎會自動調整 iteration 次數,我發現跑無 computed goto 時 coremark 會跑 3000 個 iteration,跑有 computed goto 時 coremark 會跑 4000 個 iteration,所以無法只用執行時間比較。
因為 iteration 次數不一定,所以比較的重點在於 branch-miss
數目以及 CoreMark 的 Iterations/Sec
。
no computed goto | computed goto | |
---|---|---|
branch-misses | 272,290,570 | 179,106,693 |
Iterations/Sec | 230 | 266 |
如果以 CoreMark 分數計算,效能提升為 15%。此外 compiler flag 對效能則沒有顯著的影響,所以沒列出來。 |
"jump" 的繁體中文翻譯是「跳躍」,而非「跳轉」。請愛用教育部重編國語辭典修訂本
整合的時候剛好遇到 riscv-compliance 專案大更新,所以以下是整合 2.0 的版本。
Riscv-Compliance 為了要讓 test case 可以被各種 riscv 實作執行,在每一個 test case 內都有一些 macro 可以根據不同的測試對象去定義。
而新的 riscv-compliance 如果要整合新的測試對像,只需要在 model_test.h
內定義一些必要的 macro 即可。
--test filename
的執行選項,可以透過這個選項指定測試輸出檔案
--test
換為 --compliance
,對應的說明訊息是 "Generate a compliance signature"修改 main.c
,在 { run(rv); }
之後補上 print_signature
:
這樣執行時期就可輸出 compliance test 的結果。
model_test.h
中定義了對應的 symbol,即 begin_signature 與 end_signature。所以要輸出其實很簡單,就是在執行之後找出對應的 symbol 位置,並把這些中間的資料從模擬的記憶體中讀出來即可。我整合 compliance 的方法是將 compliance 當成一個 git submodule 加進來。這麼做是因為怕 compliance 頻繁改版,以 submodule 的方式整合就可以控制使用的版本。
我也在 Makefile 中加入 compliance target,來方便重跑所有 test case。
Makefile 新增:
因為 compliance 可以支援透過設定 TARGETDIR 來更改搜尋 target 的位置,所以我是將我的 target 設定資料夾放在專案根目錄中的 test-rv32emu
資料夾中。
目前 rv32emu 在 I、M 和 Zifencei 都可以全數通過。但 privilege 中有許多無法通過:
以上正在嘗試解決當中。不過根據最新的 commit message 來看,似乎有些 test case 目前是沒有正確答案的。