RISC-V是源自Berkeley的開源體系結構和指令集標準。這個模擬器實現的是RISC-V Specification 2.2中所規定RV64I指令集,基於標準的五階段流水線,而且實現了分支預測模塊和虛擬內存模擬。實現一個完整的CPU模擬器能夠很好地鍛鍊系統編程能力,而且加深對體系結構有關知識的理解。在開始實現前,應當閱讀並深刻理解Computer Systems: A Programmer's Perspective中的第四章,或者Computer Organizaton and Design: Hardware/Software Interface中的有關章節。git
本模擬器的代碼在GitHub上:https://github.com/hehao98/RISCV-Simulatorgithub
首先,必須搭建RISC-V相關的編譯、運行和測試環境。簡便起見,本次實驗所有基於RISC-V 64I指令集,參考的指令集標準是RISC-V Specification 2.2。爲了配置環境,執行了以下步驟。算法
riscv-tools,
從中針對Linux平臺配置,編譯和安裝了riscv-gnu-toolchain
。riscv-qemu
。須要特別注意的是,在編譯riscv-gnu-toolchain
時,必須指定工具鏈和C語言標準庫所使用的指令集爲RV64I,不然在編譯的時候編譯器會使用RV64C、RV64D等擴展指令集。即便設置編譯器編譯時只使用RV64I指令集,編譯器也會連接進使用擴展指令集的標準庫函數。所以,爲了得到只使用RV64I標準指令集的ELF程序,必須在riscv-gnu-toolchain
中採用以下選項從新編譯編程
mkdir build; cd build ../configure --with-arch=rv64i --prefix=/path/to/riscv64i make -j$(nproc)
在編譯時,使用-march=rv64i
讓編譯器針對RV64I標準指令集生成ELF程序。數組
riscv64-unknown-elf-gcc -march=rv64i test/arithmetic.c test/lib.c -o riscv-elf/arithmetic.riscv
對一個體繫結構模擬器進行測試有必定難度,主要是因爲指令數衆多、代碼龐大、從而對模擬器代碼進行100%覆蓋率的測試比較困難。所以,爲了便於測試,本模擬器使用了一組由簡單到複雜的測試程序,而且實現了單步調試和打印CPU狀態的接口。此外,爲了便於進行調試和性能分析,還實現了記錄執行歷史的模塊,在程序出錯時能夠得到完整的指令執行歷史和內存快照,便於對出錯進行分析。緩存
爲了對RISC-V模擬器進行測試,編寫了以下程序(見test/
文件夾)。比較複雜的是快速排序、矩陣乘法和求Ackermann函數三個。其中,快速排序和矩陣乘法涉及比較多的指令和數據,求解Ackermann函數涉及很是深的遞歸調用。性能優化
lib.c # 自定義的系統調用實現 helloworld.c # 最簡單的程序 test_arithmetic.c # 對運算指令的測試 test_branch.c # 對基本分支的測試 test_syscall.c # 對系統調用的測試 quicksort.c # 快速排序 matrixmulti.c # 矩陣乘法 ackermann.c # 求解Ackermann函數
全部程序編譯後獲得的二進制程序和反編譯獲得的彙編代碼均保存在riscv-elfs/
文件夾中。數據結構
我測試的模擬器運行環境爲Mac OS X,使用的編程語言爲C++ 11,構建環境爲CMake,編譯器爲Apple Clang 10.0.0
,編譯使用的Flag爲-O2 -Wall
。開發使用的工具爲VS Code。不過,模擬器代碼儘可能避免了使用標準庫之外的平臺相關功能,因此應該也能在其餘平臺和編譯器上編譯運行。架構
首先,模擬器的運行必須是健壯的。具體地說,必須可以處理各類非法輸入,包括不正常的訪存,不正常的ELF文件,非法指令,非法的訪存地址等等。編寫細緻全面的錯誤處理不只有助於鍛鍊系統編程能力,也有助於在早期發現細微的程序錯誤。app
其次,模擬器的實現必須簡單、易於理解和易於調試。此模擬器是一個課程項目級別的模擬器,容許的實現時間有限,所以代碼實現必須簡單,調試系統必須完備,從而儘量地減小編寫程序和調試程序所須要的時間。
此外,模擬器實現的主要目的是可以被用於簡單性能評測,所以必須可以儘量貼近流水線硬件,並能夠擴展出分支預測和緩存模擬等各類功能,便於在真正的程序上實驗和評測流水線的性能,以及各類分支預測和緩存模擬策略。
本次模擬器的實現並非要作一個成熟可用的工業級體系結構模擬器,也就是說,本次模擬器的實現並不注重性能和功能的全面性。在性能上,對於極端複雜和龐大的程序,模擬器的程序會執行緩慢,也有可能會消耗過多內存,對於模擬器自己的性能優化不在本實驗的範圍內。在功能上,爲了實現簡單,本模擬器使用自定義的系統調用,而不是兼容Linux的系統調用,所以,此模擬器只能運行專門爲此編譯的RISC-V程序(程序源碼參見test/
文件夾)。
編譯方法與一個典型的CMake項目同樣,在編譯以前必須先安裝CMake。在Linux或者Mac OS X系統上能夠採用以下命令
mkdir build cd build cmake .. make
編譯會獲得可執行程序Simulator
。該模擬器是一個命令行程序,在命令行上的執行方式是
./Simulator riscv-elf-file-name [-v] [-s] [-d] [-b param] Parameters: [-v] verbose output [-s] single step [-d] dump memory and register trace to dump.txt [-b param] branch perdiction strategy, accepted param AT, NT, BTFNT, BPB
其中riscv-elf-file-name
對應可執行的RISC-V ELF文件,好比riscv-elf/
文件夾下的全部*.riscv
文件。一個典型的運行流程和輸出以下
hehaodeMacBook-Pro:build hehao$ ./Simulator ../riscv-elf/ackermann.riscv Ackermann(0,0) = 1 Ackermann(0,1) = 2 Ackermann(0,2) = 3 Ackermann(0,3) = 4 Ackermann(0,4) = 5 Ackermann(1,0) = 2 Ackermann(1,1) = 3 Ackermann(1,2) = 4 Ackermann(1,3) = 5 Ackermann(1,4) = 6 Ackermann(2,0) = 3 Ackermann(2,1) = 5 Ackermann(2,2) = 7 Ackermann(2,3) = 9 Ackermann(2,4) = 11 Ackermann(3,0) = 5 Ackermann(3,1) = 13 Ackermann(3,2) = 29 Ackermann(3,3) = 61 Ackermann(3,4) = 125 Program exit from an exit() system call ------------ STATISTICS ----------- Number of Instructions: 430754 Number of Cycles: 574548 Avg Cycles per Instrcution: 1.3338 Branch Perdiction Accuacy: 0.5045 (Strategy: Always Not Taken) Number of Control Hazards: 48010 Number of Data Hazards: 279916 Number of Memory Hazards: 47774 -----------------------------------
在默認的設置下,一開始會首先打印執行的程序的輸出,而後會輸出一組關於CPU執行狀況的統計數據。
若是要進行單步調試的話,可使用-s
和-v
參數
./Simulator ../riscv-elf/ackermann.riscv -s -v
獲得的輸出以下
hehaodeMacBook-Pro:build hehao$ ./Simulator ../riscv-elf/ackermann.riscv -s -v ==========ELF Information========== Type: ELF64 Encoding: Little Endian ISA: RISC-V(0xf3) Number of Sections: 19 ID Name Address Size [0] 0x0 0 [1] .text 0x100b0 3668 [2] .rodata 0x10f08 29 [3] .eh_frame 0x10f28 4 [4] .init_array 0x11000 8 [5] .fini_array 0x11008 8 [6] .data 0x11010 1864 [7] .sdata 0x11758 24 [8] .sbss 0x11770 8 [9] .bss 0x11778 72 [10] .comment 0x0 26 [11] .debug_aranges 0x0 48 [12] .debug_info 0x0 46 [13] .debug_abbrev 0x0 20 [14] .debug_line 0x0 222 [15] .debug_str 0x0 267 [16] .symtab 0x0 2616 [17] .strtab 0x0 913 [18] .shstrtab 0x0 172 Number of Segments: 2 ID Flags Address FSize MSize [0] 0x5 0x10000 3884 3884 [1] 0x6 0x11000 1904 1984 =================================== Memory Pages: 0x0-0x400000: 0x10000-0x11000 0x11000-0x12000 Fetched instruction 0x00002197 at address 0x100b0 Decode: Bubble Execute: Bubble Memory Access: Bubble WriteBack: Bubble ------------ CPU STATE ------------ PC: 0x100b4 zero: 0x00000000(0) ra: 0x00000000(0) sp: 0x80000000(2147483648) gp: 0x00000000(0) tp: 0x00000000(0) t0: 0x00000000(0) t1: 0x00000000(0) t2: 0x00000000(0) s0: 0x00000000(0) s1: 0x00000000(0) a0: 0x00000000(0) a1: 0x00000000(0) a2: 0x00000000(0) a3: 0x00000000(0) a4: 0x00000000(0) a5: 0x00000000(0) a6: 0x00000000(0) a7: 0x00000000(0) s2: 0x00000000(0) s3: 0x00000000(0) s4: 0x00000000(0) s5: 0x00000000(0) s6: 0x00000000(0) s7: 0x00000000(0) s8: 0x00000000(0) s9: 0x00000000(0) s10: 0x00000000(0) s11: 0x00000000(0) t3: 0x00000000(0) t4: 0x00000000(0) t5: 0x00000000(0) t6: 0x00000000(0) ----------------------------------- Type d to dump memory in dump.txt, press ENTER to continue:
在單步調試中,能夠輸入d
來保存內存快照,使用ENTER前進到下一條指令。命令行顯示的信息包括ELF信息、流水線狀態和CPU寄存器狀態。
使用-v
參數並重定向標準輸出能夠獲得關於流水線執行狀態和寄存器狀態的完整歷史。
此外,可使用-b
參數指定不一樣的分支預測策略,例如
./Simulator ../riscv-elf/ackermann.riscv -b AT ./Simulator ../riscv-elf/ackermann.riscv -b NT ./Simulator ../riscv-elf/ackermann.riscv -b BTFNT ./Simulator ../riscv-elf/ackermann.riscv -b BPB
其中,AT表示Always Taken,NT表示Not Taken,BTFNT表示Back Taken Forward Not Taken,BPB表示Branch Prediction Buffer。
模擬器代碼架構的概覽圖見上。模擬器的入口是Main.cpp
,其中包含了解析參數、加載ELF文件、初始化模擬器的模塊,並在最後調用模擬器的simulate()
函數進入模擬器的執行。除非模擬器執行出錯,否者simulate()
函數理論上不會返回。
模擬器自己被設計成一個巨大的類,也就是代碼中的class Simulator
(參見Simulator.h
、Simulator.cpp
)。Simulator
類中的數據包含了PC、通用寄存器、流水線寄存器、執行歷史記錄器、內存模塊和分支預測模塊,其中,因爲內存模塊和分支預測模塊相對比較獨立,所以實現爲獨立的兩個類MemoryManager
和BranchPredictor
。
模擬器中最核心的函數是simulate()
函數,這個函數對模擬器進行週期級模擬,每次模擬中,會執行fetch()
、decode()
、execute()
、accessMemory()
和writeBack()
五個函數,每一個函數會以上一個週期的流水線寄存器做爲輸入,並輸出到下一個週期的流水線寄存器。在週期結束時,新的寄存器的內容會被拷貝到做爲輸入的寄存器中。在執行過程當中,每一個函數都會處理有關數據、控制和內存訪問冒險的內容,而且在適當的地方記錄歷史信息。因爲之間的交互關係比較複雜,所以在上圖中並無畫出。因爲相關函數代碼過長,不便於在此貼出,所以關於實現的更多細節請參見src/Simulator.cpp
。
MemoryManager
MemoryManager
的功能是爲模擬器提供一個簡單易使用的內存訪問接口,必須支持任意內存大小、內存地址的訪存,還要能檢測到非法內存地址訪問。事實上,這很是相似於操做系統中虛擬內存的機制。所以,MemoryManager
的內部實現採用了相似x86體系結構中使用的二級頁表的機制。具體地說,將32位內存空間在邏輯上劃分爲大小爲4KB(2^12)的頁,而且採用內存地址的前10位做爲一級頁表的索引,緊接着10位做爲二級頁表的索引,最後12位做爲一個內存頁裏的下標。
頁表結構能夠以下聲明
uint8_t **memory[1024];
其中,memory
指向一個長度爲1024的一級頁表數組,memory[i]
指向長度爲1024的二級頁表數組,memory[i][j]
指向具體的內存頁,memory[i][j][k]
能夠取出內存地址爲(i<<22)|(j<<12)|k
的一個字節。能夠在須要的時候對memory
進行動態內存分配和釋放。模擬器對memory
的一個訪存過程的示例以下
uint8_t MemoryManager::getByte(uint32_t addr) { if (!this->isAddrExist(addr)) { dbgprintf("Byte read to invalid addr 0x%x!\n", addr); return false; } uint32_t i = this->getFirstEntryId(addr); uint32_t j = this->getSecondEntryId(addr); uint32_t k = this->getPageOffset(addr); return this->memory[i][j][k]; }
關於MemoryManager
實現的更多信息,參見src/MemoryManager.cpp
。
本模擬器的可執行文件加載部分採用了GitHub上的開源庫ELFIO(https://github.com/serge1/ELFIO),因爲這個庫只有頭文件,因此導入工程至關容易,相關頭文件在include/
文件夾下。
使用這個庫進行ELF文件加載至關容易
// Read ELF file ELFIO::elfio reader; if (!reader.load(elfFile)) { fprintf(stderr, "Fail to load ELF file %s!\n", elfFile); return -1; }
加載ELF文件進內存的代碼以下,直接按照ELF文件頭的信息將每一個數據段拷貝到指定的內存位置便可,惟一須要注意的是文件內數據長度可能小於指定的內存長度,須要用0填充。值得一提的是本模擬器在設計時並未考慮支持32位以上的內存,由於內存佔用如此之大的用戶程序是比較罕見的,在咱們用的測試程序中不會出現這種狀況。
void loadElfToMemory(ELFIO::elfio *reader, MemoryManager *memory) { ELFIO::Elf_Half seg_num = reader->segments.size(); for (int i = 0; i < seg_num; ++i) { const ELFIO::segment *pseg = reader->segments[i]; uint64_t fullmemsz = pseg->get_memory_size(); uint64_t fulladdr = pseg->get_virtual_address(); // Our 32bit simulator cannot handle this if (fulladdr + fullmemsz > 0xFFFFFFFF) { dbgprintf( "ELF address space larger than 32bit! Seg %d has max addr of 0x%lx\n", i, fulladdr + fullmemsz); exit(-1); } uint32_t filesz = pseg->get_file_size(); uint32_t memsz = pseg->get_memory_size(); uint32_t addr = (uint32_t)pseg->get_virtual_address(); for (uint32_t p = addr; p < addr + memsz; ++p) { if (!memory->isPageExist(p)) { memory->addPage(p); } if (p < addr + filesz) { memory->setByte(p, pseg->get_data()[p - addr]); } else { memory->setByte(p, 0); } } } }
最後,須要在模擬器初始化時手動設置PC的值。模擬器還須要不少其餘的初始化操做,具體能夠參考src/Main.cpp
。
simulator.pc = reader.get_entry();
本小節中涉及代碼因爲廣泛過長,且存在很是強的相互依賴,單獨貼出可能難以理解,所以不會在此直接貼出代碼,具體內容請參見src/Simulator.cpp
。
指令的取值過程參見Simulator::fetch()
函數,因爲RV64I指令集都是4字節定長,因此實現起來很是簡單。
指令的解碼過程參見Simulator::decode()
函數,其中絕大多數內容都是對RISC-V Specification 2.2中規定的指令編碼的直接翻譯。在解碼過程當中,爲了便於調試,decode()
函數會按照RISC-V彙編格式翻譯出指令字符串。此外,decode
函數會模仿硬件實如今指令中抽象出op1
、op2
、dest
等幾個共有的域。分支預測模塊會在解碼階段作出預測判斷。
指令的執行過程參見Simulator::execute()
函數,這個函數簡單粗暴地根據指令類型直接執行相應的行爲。在結尾,會根據當前指令和解碼階段的狀況,檢測數據冒險、控制冒險和內存訪問冒險,並做出相應的操做。在這個階段,跳轉指令會獲得是否跳轉的結果,並在預測錯誤的狀況下在流水線寄存器中插入對應的Bubble。
指令的訪存過程參見Simulator::memoryAccess()
函數,這個函數首先執行內存讀寫操做,而且檢測數據冒險和轉發數據。在檢測數據冒險時,既須要考慮到通常的數據冒險,也必須考慮到上個週期由於內存訪問冒險而流水線Stall的狀況,此外,也必須考慮數據轉發的優先級,memoryAccess()
做爲後面的指令,數據轉發的優先級是低於execute()
的,不然可能會出現較老的數據被轉發並覆蓋新數據的狀況。
指令的寫回過程參見Simulator::writeBack()
函數,這個函數將執行結果寫回寄存器,而且相似以前的狀況處理相關的數據冒險。
流水線寄存器的控制信號設置以下,注意其中fReg表示的是下一個週期開始時,從取值階段傳輸到解碼階段的數據,以此類推。
出現的狀況 | fReg | dReg | eReg | mReg |
---|---|---|---|---|
分支預測錯誤 | Bubble | Bubble | Normal | Normal |
內存訪問冒險 | Stall | Stall | Bubble | Normal |
預測跳轉 | Bubble | Normal | Normal | Normal |
有一種狀況須要特別說明,就是分支預測器的狀況。在當前的模擬器設計中,因爲到了解碼階段結束才得知跳轉指令的存在,所以若是預測跳轉的話必須向流水線中插入一個Bubble,才能確保取指階段取出的是跳轉後的指令。這不會增長分支預測錯誤的開銷,可是會使得預測正確的開銷多了一個週期。若是要改進這個設計的話,必須將分支預測模塊轉移到取指階段實現。
本模擬器使用自定義的系統調用接口。系統調用的ecall
指令會使用a0
和a7
寄存器,其中a7
寄存器保存的是系統調用號,a0
寄存器保存的是系統調用參數,返回值會保存在a0
寄存器中。爲了能讓系統調用指令能被集成進當前的流水線,ecall
指令只支持一個返回值和一個參數。全部系統調用的語義見下表。
系統調用名稱 | 系統調用號 | 參數 | 返回值 |
---|---|---|---|
輸出字符串 | 0 | 字符串起始地址 | 無 |
輸出字符 | 1 | 字符的值 | 無 |
輸出數字 | 2 | 數字的值 | 無 |
退出程序 | 3 | 無 | 無 |
讀入字符 | 4 | 無 | 讀入的字符 |
讀入數字 | 5 | 無 | 讀入的數字 |
對應的系統調用接口以下
void print_d(int num); void print_s(const char *str); void print_c(char ch); void exit_proc(); char read_char(); long long read_num();
具體的實現須要使用內聯彙編,請參考test/lib.c
。
在當前模擬器架構下,對於模擬器進行性能統計只需在代碼裏適當的地方加入統計代碼便可。數據統計模塊的定義以下
struct History { uint32_t instCount; uint32_t cycleCount; uint32_t predictedBranch; // Number of branch that is predicted successfully uint32_t unpredictedBranch; // Number of branch that is not predicted successfully uint32_t dataHazardCount; uint32_t controlHazardCount; uint32_t memoryHazardCount; std::vector<std::string> instRecord; std::vector<std::string> regRecord; std::string memoryDump; } history;
其中,最後三個數據項用於記載CPU的執行歷史,便於在調試的時候使用。爲了防止模擬器佔用過多內存,instRecord
和regRecord
當內容多於100000條時會被清空,memoryDump
只會在要求生成內存快照時被使用。
因爲對CPU模擬器的調試相對比較困難,CPU模擬器的調試接口和錯誤執行接口必須被很是當心地設計,以便於儘量早地發現程序中的Bug。在當前模擬器的代碼中,存在大量對模擬器狀態和輸入值合法性的檢查,以便儘量早地發現錯誤。Simulator
類中存在專門的錯誤處理函數panic()
。
void Simulator::panic(const char *format, ...) { char buf[BUFSIZ]; va_list args; va_start(args, format); vsprintf(buf, format, args); fprintf(stderr, "%s", buf); va_end(args); this->dumpHistory(); fprintf(stderr, "Execution history and memory dump in dump.txt\n"); exit(-1); }
此外,模擬器還支持單步調試和verbose
輸出的功能,使用-s
和-v
參數便可開啓單步調試模式。使用-v
參數並重定向標準輸出能夠獲得寄存器狀態和流水線狀態的完整執行歷史並在過後進行分析。一條典型的CPU執行狀態記錄以下
Fetched instruction 0x00000593 at address 0x100c4 Decoded instruction 0x40a60633 as sub a2,a2,a0 Execute: addi Forward Data a2 to Decode op1 Memory Access: addi Forward Data a0 to Decode op2 WriteBack: addi ------------ CPU STATE ------------ PC: 0x100c8 zero: 0x00000000(0) ra: 0x00000000(0) sp: 0x80000000(2147483648) gp: 0x00011f58(73560) tp: 0x00000000(0) t0: 0x00000000(0) t1: 0x00000000(0) t2: 0x00000000(0) s0: 0x00000000(0) s1: 0x00000000(0) a0: 0x00000000(0) a1: 0x00000000(0) a2: 0x00000000(0) a3: 0x00000000(0) a4: 0x00000000(0) a5: 0x00000000(0) a6: 0x00000000(0) a7: 0x00000000(0) s2: 0x00000000(0) s3: 0x00000000(0) s4: 0x00000000(0) s5: 0x00000000(0) s6: 0x00000000(0) s7: 0x00000000(0) s8: 0x00000000(0) s9: 0x00000000(0) s10: 0x00000000(0) s11: 0x00000000(0) t3: 0x00000000(0) t4: 0x00000000(0) t5: 0x00000000(0) t6: 0x00000000(0) -----------------------------------
在整個實現中,我在第一階段的單週期指令級模擬的實現並無遇到什麼問題,可是流水線相關的模擬中,遇到了幾個至關微妙的錯誤。
ecall
指令也會致使數據冒險!而且產生數據冒險的條目,取決於這個系統調用的參數數量和其對應的寄存器!當前的系統調用會依賴的寄存器有a0
和a7
兩個,所以恰好能做爲op1
和op2
塞入流水線,可是若是系統調用須要的參數更多,實現將會變得更爲複雜。zero
寄存器是一個至關獨特的存在,理論上他任什麼時候候值應該都是0,因此進行數據轉發的時候必須到處特判零寄存器,若是向零寄存器裏的值進行數據轉發就會致使很是難以發現的錯誤。我本身編寫的測試程序見下表,注意全部的程序都須要和test/lib.c
一塊兒編譯。
代碼文件 | 對應的ELF文件 |
---|---|
test/helloworld.c |
riscv-elf/helloworld.riscv |
test/test_arithmetic.c |
riscv-elf/test_arithmetic.riscv |
test/test_syscall.c |
riscv-elf/test_syscall.riscv |
test/test_branch.c |
riscv-elf/test_branch.riscv |
test/quicksort.c |
riscv-elf/quicksort.riscv |
test/matrixmulti.c |
riscv-elf/matrixmulti.riscv |
test/ackermann.c |
riscv-elf/ackermann.riscv |
每一個代碼文件的功能描述以下
代碼文件 | 功能描述 |
---|---|
test/helloworld.c |
最簡單的Hello, World |
test/test_arithmetic.c |
測試一組算術運算 |
test/test_syscall.c |
測試所有的系統調用 |
test/test_branch.c |
測試條件和循環語句 |
test/quicksort.c |
分別對10和100個元素進行快速排序 |
test/matrixmulti.c |
10*10矩陣乘法 |
test/ackermann.c |
求解一組Ackermann函數的值 |
若是模擬器程序Simulator
在項目中的build/
目錄下,能夠運行以下命令,獲得運行結果,來驗證模擬器的正確性。注意test_syscall.riscv
程序中存在用戶輸入的部分。
./Simulator ../riscv-elf/helloworld.riscv ./Simulator ../riscv-elf/test_arithmetic.riscv ./Simulator ../riscv-elf/test_syscall.riscv ./Simulator ../riscv-elf/test_branch.riscv ./Simulator ../riscv-elf/quicksort.riscv ./Simulator ../riscv-elf/matrixmulti.riscv ./Simulator ../riscv-elf/ackermann.riscv
獲得的執行結果以下
hehaodeMacBook-Pro:build hehao$ ./Simulator ../riscv-elf/helloworld.riscv Hello, World! Program exit from an exit() system call ------------ STATISTICS ----------- Number of Instructions: 141 Number of Cycles: 188 Avg Cycles per Instrcution: 1.3333 Branch Perdiction Accuacy: 0.5833 (Strategy: Always Not Taken) Number of Control Hazards: 23 Number of Data Hazards: 73 Number of Memory Hazards: 1 ----------------------------------- hehaodeMacBook-Pro:build hehao$ ./Simulator ../riscv-elf/test_arithmetic.riscv 30 -10 370350 411 49380 771 Program exit from an exit() system call ------------ STATISTICS ----------- Number of Instructions: 508 Number of Cycles: 703 Avg Cycles per Instrcution: 1.3839 Branch Perdiction Accuacy: 0.4268 (Strategy: Always Not Taken) Number of Control Hazards: 91 Number of Data Hazards: 224 Number of Memory Hazards: 13 ----------------------------------- hehaodeMacBook-Pro:build hehao$ ./Simulator ../riscv-elf/test_syscall.riscv This is string from print_s() 123456abc Enter a number: 123456 The number is: 123456 Enter a character: g The character is: g Program exit from an exit() system call ------------ STATISTICS ----------- Number of Instructions: 350 Number of Cycles: 461 Avg Cycles per Instrcution: 1.3171 Branch Perdiction Accuacy: 0.5833 (Strategy: Always Not Taken) Number of Control Hazards: 53 Number of Data Hazards: 178 Number of Memory Hazards: 5 ----------------------------------- hehaodeMacBook-Pro:build hehao$ ./Simulator ../riscv-elf/quicksort.riscv Prev A: 5 3 5 6 7 1 3 5 6 1 Sorted A: 1 1 3 3 5 5 5 6 6 7 Prev B: 100 99 98 97 96 95 94 93 92 91 90 89 88 87 86 85 84 83 82 81 80 79 78 77 76 75 74 73 72 71 70 69 68 67 66 65 64 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 40 39 38 37 36 35 34 33 32 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 Sorted B: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 Program exit from an exit() system call ------------ STATISTICS ----------- Number of Instructions: 103671 Number of Cycles: 141697 Avg Cycles per Instrcution: 1.3668 Branch Perdiction Accuacy: 0.4926 (Strategy: Always Not Taken) Number of Control Hazards: 7314 Number of Data Hazards: 86448 Number of Memory Hazards: 23398 ----------------------------------- hehaodeMacBook-Pro:build hehao$ ./Simulator ../riscv-elf/matrixmulti.riscv The content of A is: 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 The content of B is: 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 The content of C=A*B is: 0 0 0 0 0 0 0 0 0 0 0 10 20 30 40 50 60 70 80 90 0 20 40 60 80 100 120 140 160 180 0 30 60 90 120 150 180 210 240 270 0 40 80 120 160 200 240 280 320 360 0 50 100 150 200 250 300 350 400 450 0 60 120 180 240 300 360 420 480 540 0 70 140 210 280 350 420 490 560 630 0 80 160 240 320 400 480 560 640 720 0 90 180 270 360 450 540 630 720 810 Program exit from an exit() system call ------------ STATISTICS ----------- Number of Instructions: 225441 Number of Cycles: 318532 Avg Cycles per Instrcution: 1.4129 Branch Perdiction Accuacy: 0.3765 (Strategy: Always Not Taken) Number of Control Hazards: 40678 Number of Data Hazards: 110957 Number of Memory Hazards: 11735 ----------------------------------- hehaodeMacBook-Pro:build hehao$ ./Simulator ../riscv-elf/ackermann.riscv Ackermann(0,0) = 1 Ackermann(0,1) = 2 Ackermann(0,2) = 3 Ackermann(0,3) = 4 Ackermann(0,4) = 5 Ackermann(1,0) = 2 Ackermann(1,1) = 3 Ackermann(1,2) = 4 Ackermann(1,3) = 5 Ackermann(1,4) = 6 Ackermann(2,0) = 3 Ackermann(2,1) = 5 Ackermann(2,2) = 7 Ackermann(2,3) = 9 Ackermann(2,4) = 11 Ackermann(3,0) = 5 Ackermann(3,1) = 13 Ackermann(3,2) = 29 Ackermann(3,3) = 61 Ackermann(3,4) = 125 Program exit from an exit() system call ------------ STATISTICS ----------- Number of Instructions: 430754 Number of Cycles: 574548 Avg Cycles per Instrcution: 1.3338 Branch Perdiction Accuacy: 0.5045 (Strategy: Always Not Taken) Number of Control Hazards: 48010 Number of Data Hazards: 279916 Number of Memory Hazards: 47774 -----------------------------------
給定的5個程序在test-inclass/
文件夾中,有以下5個
add.c mul-div.c n!.c qsort.c simple-function.c
相似以前的執行方式,獲得以下原始運行結果
hehaodeMacBook-Pro:build hehao$ ./Simulator ../test-inclass/add.riscv Program exit from an exit() system call ------------ STATISTICS ----------- Number of Instructions: 876 Number of Cycles: 1183 Avg Cycles per Instrcution: 1.3505 Branch Perdiction Accuacy: 0.4639 (Strategy: Always Not Taken) Number of Control Hazards: 124 Number of Data Hazards: 433 Number of Memory Hazards: 58 ----------------------------------- hehaodeMacBook-Pro:build hehao$ ./Simulator ../test-inclass/mul-div.riscv Program exit from an exit() system call ------------ STATISTICS ----------- Number of Instructions: 901 Number of Cycles: 1208 Avg Cycles per Instrcution: 1.3407 Branch Perdiction Accuacy: 0.4639 (Strategy: Always Not Taken) Number of Control Hazards: 124 Number of Data Hazards: 463 Number of Memory Hazards: 58 ----------------------------------- hehaodeMacBook-Pro:build hehao$ ./Simulator ../test-inclass/n\!.riscv Program exit from an exit() system call ------------ STATISTICS ----------- Number of Instructions: 1112 Number of Cycles: 1525 Avg Cycles per Instrcution: 1.3714 Branch Perdiction Accuacy: 0.4661 (Strategy: Always Not Taken) Number of Control Hazards: 189 Number of Data Hazards: 515 Number of Memory Hazards: 34 ----------------------------------- hehaodeMacBook-Pro:build hehao$ ./Simulator ../test-inclass/qsort.riscv Program exit from an exit() system call ------------ STATISTICS ----------- Number of Instructions: 19427 Number of Cycles: 25328 Avg Cycles per Instrcution: 1.3038 Branch Perdiction Accuacy: 0.4701 (Strategy: Always Not Taken) Number of Control Hazards: 1363 Number of Data Hazards: 14156 Number of Memory Hazards: 3174 ----------------------------------- hehaodeMacBook-Pro:build hehao$ ./Simulator ../test-inclass/simple-function.riscv Program exit from an exit() system call ------------ STATISTICS ----------- Number of Instructions: 886 Number of Cycles: 1197 Avg Cycles per Instrcution: 1.3510 Branch Perdiction Accuacy: 0.4639 (Strategy: Always Not Taken) Number of Control Hazards: 126 Number of Data Hazards: 438 Number of Memory Hazards: 58 -----------------------------------
從這些原始數據中能夠分析獲得要求的結果,下面會對這些結果進行總結。
程序名 | 執行的指令數 |
---|---|
add.riscv |
876 |
mul-div.riscv |
901 |
n!.riscv |
1112 |
qsort.riscv |
19427 |
simple-function.riscv |
886 |
程序名 | 執行週期數 | 平均CPI |
---|---|---|
add.riscv |
1183 | 1.3505 |
mul-div.riscv |
1208 | 1.3407 |
n!.riscv |
1525 | 1.3714 |
qsort.riscv |
25328 | 1.3038 |
simple-function.riscv |
1197 | 1.3510 |
能夠發現,對於各類類型的程序,本模擬器流水線實現的平均CPI在1.33左右,和單週期相比能實現大約3.76倍的指令吞吐量。
程序名 | 數據冒險 | 控制冒險 | 內存訪問冒險 |
---|---|---|---|
add.riscv |
433 | 124 | 58 |
mul-div.riscv |
463 | 124 | 58 |
n!.riscv |
515 | 189 | 34 |
qsort.riscv |
14156 | 1363 | 3174 |
simple-function.riscv |
438 | 126 | 58 |
分支預測模塊是一個相對比較獨立的模塊,所以單獨實現爲BranchPredictor
類。BranchPredictor
類須要指定一個分支預測有關的策略,並保存與這個策略有關的數據結構。本模擬器實現了以下幾種策略
策略名稱 | 策略說明 |
---|---|
NT | Always Not Taken |
AT | Always Taken |
BTFNT | Back Taken Forward Not Taken |
BPB | Branch Prediction Buffer |
其中,Branch Prediction Buffer採用"Computer Organization and Design: Hardware/Software Interface"中所介紹的四狀態,兩位歷史信息的方法。具體地說,使用內存後12位做爲索引維護一個長度爲4096的直接映射高速緩存,用於存儲分支指令的地址。對於一個緩存條目,其狀態爲如下四個狀態之一:Strong Taken
, Weak Taken
, Weak Not Taken
, Strong Not Taken
. 狀態轉換圖以下
具體實現以下
bool BranchPredictor::predict(uint32_t pc, uint32_t insttype, int64_t op1, int64_t op2, int64_t offset) { switch (this->strategy) { case NT: return false; case AT: return true; case BTFNT: { if (offset >= 0) { return false; } else { return true; } } break; case BPB: { PredictorState state = this->predbuf[pc % PRED_BUF_SIZE]; if (state == STRONG_TAKEN || state == WEAK_TAKEN) { return true; } else if (state == STRONG_NOT_TAKEN || state == WEAK_NOT_TAKEN) { return false; } else { dbgprintf("Strange Prediction Buffer!\n"); } } break; default: dbgprintf("Unknown Branch Perdiction Strategy!\n"); break; } return false; } void BranchPredictor::update(uint32_t pc, bool branch) { int id = pc % PRED_BUF_SIZE; PredictorState state = this->predbuf[id]; if (branch) { if (state == STRONG_NOT_TAKEN) { this->predbuf[id] = WEAK_NOT_TAKEN; } else if (state == WEAK_NOT_TAKEN) { this->predbuf[id] = WEAK_TAKEN; } else if (state == WEAK_TAKEN) { this->predbuf[id] = STRONG_TAKEN; } // do nothing if STRONG_TAKEN } else { // not taken if (state == STRONG_TAKEN) { this->predbuf[id] = WEAK_TAKEN; } else if (state == WEAK_TAKEN) { this->predbuf[id] = WEAK_NOT_TAKEN; } else if (state == WEAK_NOT_TAKEN) { this->predbuf[id] = STRONG_NOT_TAKEN; } // do noting if STRONG_NOT_TAKEN } }
而且在解碼階段和執行階段添加有關分支預測的代碼
// Sumulator::decode() bool predictedBranch = false; if (isBranch(insttype)) { predictedBranch = this->branchPredictor->predict(this->fReg.pc, insttype, op1, op2, offset); if (predictedBranch) { this->predictedPC = this->fReg.pc + offset; this->anotherPC = this->fReg.pc + 4; this->fRegNew.bubble = true; } else { this->anotherPC = this->fReg.pc + offset; } }
// Simulator::execute() if (isBranch(inst)) { ... // this->dReg.pc: fetch original inst addr, not the modified one this->branchPredictor->update(this->dReg.pc, branch); }
須要注意的是將PC修改成預測器預測的PC的時機,必需要在一個週期的結束時,也就是simulate()
函數中循環的末尾處。
// The Branch perdiction happens here to avoid strange bugs in branch prediction if (!this->dReg.bubble && !this->dReg.stall && !this->fReg.stall && this->dReg.predictedBranch) { this->pc = this->predictedPC; }
這樣便可完成分支預測模塊的實現,而且很容易可以擴展出新的分支預測策略。
有趣的是,有了這個分支預測模塊以後,咱們能夠對不一樣分支預測策略的性能進行評測。下面的表格是一個對分支預測準確率的簡單統計。
評測程序 | Always Taken | BTFNT | Prediction Buffer |
---|---|---|---|
helloworld.riscv |
0.4706 | 0.7059 | 0.4706 |
quicksort.riscv |
0.5075 | 0.9506 | 0.9587 |
matrixmult.riscv |
0.6235 | 0.6325 | 0.6275 |
ackermann.riscv |
0.4955 | 0.5053 | 0.9593 |
咱們能夠看到,對於helloworld
程序,因爲程序過於簡單,其中絕大多數指令只會被執行一次,因此基於歷史信息的Prediction Buffer方法退化到了Always Taken方法(由於默認預測是選擇跳轉),而基於程序結構的經驗性判斷方法BTFNT反而取得了最高的準確率。
對於快速排序評測程序,咱們發現Prediction Buffer和BTFNT都取得了極其高的預測準確率。這是由於排序元素較多(100個),而且絕大多數狀況下都在反覆執行不多的一段代碼。因爲這些代碼絕大多數都知足向前會跳轉的性質,因此BTFNT方法的準確率很高。因爲循環的執行長度很是長(約100次),因此基於歷史信息的Predicton Buffer可以很好地得到較高的預測準確性。
對於矩陣乘法程序,三個分支預測算法的表現很是接近。這多是因爲矩陣乘法中每次循環的執行長度都很短(10個元素),限制了BTFNT和Prediction Buffer的性能。
對於Ackermann函數求解程序,其中徹底沒有循環語句,只有函數遞歸調用和條件判斷語句,絕大多數的分支指令都在遞歸調用的函數內,所以,這時基於歷史信息的Prediction Buffer就能發揮出最大威力,得出至關高的預測準確率,而BTFNT在此則相對比較受限了,若是遞歸函數內恰好兩個if語句,一個if語句是向前跳轉,一個if語句是向後跳轉,而兩條語句在大多數狀況下都是跳轉,那麼BTFNT的準確率就會在50%左右徘徊。