基於五階段流水線的RISC-V CPU模擬器實現

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

1、開發環境

1.1 RISC-V環境的安裝與配置

首先,必須搭建RISC-V相關的編譯、運行和測試環境。簡便起見,本次實驗所有基於RISC-V 64I指令集,參考的指令集標準是RISC-V Specification 2.2。爲了配置環境,執行了以下步驟。算法

  1. 從GitHub上下載了riscv-tools,從中針對Linux平臺配置,編譯和安裝了riscv-gnu-toolchain
  2. 爲了使用官方模擬器做爲參照,從GitHub上下載、編譯和安裝了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

1.2 使用的測試程序和測試方法

對一個體繫結構模擬器進行測試有必定難度,主要是因爲指令數衆多、代碼龐大、從而對模擬器代碼進行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/文件夾中。數據結構

2、設計概述

2.1 開發環境

我測試的模擬器運行環境爲Mac OS X,使用的編程語言爲C++ 11,構建環境爲CMake,編譯器爲Apple Clang 10.0.0,編譯使用的Flag爲-O2 -Wall。開發使用的工具爲VS Code。不過,模擬器代碼儘可能避免了使用標準庫之外的平臺相關功能,因此應該也能在其餘平臺和編譯器上編譯運行。架構

2.2 設計考量

首先,模擬器的運行必須是健壯的。具體地說,必須可以處理各類非法輸入,包括不正常的訪存,不正常的ELF文件,非法指令,非法的訪存地址等等。編寫細緻全面的錯誤處理不只有助於鍛鍊系統編程能力,也有助於在早期發現細微的程序錯誤。app

其次,模擬器的實現必須簡單、易於理解和易於調試。此模擬器是一個課程項目級別的模擬器,容許的實現時間有限,所以代碼實現必須簡單,調試系統必須完備,從而儘量地減小編寫程序和調試程序所須要的時間。

此外,模擬器實現的主要目的是可以被用於簡單性能評測,所以必須可以儘量貼近流水線硬件,並能夠擴展出分支預測和緩存模擬等各類功能,便於在真正的程序上實驗和評測流水線的性能,以及各類分支預測和緩存模擬策略。

本次模擬器的實現並非要作一個成熟可用的工業級體系結構模擬器,也就是說,本次模擬器的實現並不注重性能和功能的全面性。在性能上,對於極端複雜和龐大的程序,模擬器的程序會執行緩慢,也有可能會消耗過多內存,對於模擬器自己的性能優化不在本實驗的範圍內。在功能上,爲了實現簡單,本模擬器使用自定義的系統調用,而不是兼容Linux的系統調用,所以,此模擬器只能運行專門爲此編譯的RISC-V程序(程序源碼參見test/文件夾)。

2.3 編譯與運行

編譯方法與一個典型的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。

2.4 代碼架構

模擬器代碼架構的概覽圖見上。模擬器的入口是Main.cpp,其中包含了解析參數、加載ELF文件、初始化模擬器的模塊,並在最後調用模擬器的simulate()函數進入模擬器的執行。除非模擬器執行出錯,否者simulate()函數理論上不會返回。

模擬器自己被設計成一個巨大的類,也就是代碼中的class Simulator(參見Simulator.hSimulator.cpp)。Simulator類中的數據包含了PC、通用寄存器、流水線寄存器、執行歷史記錄器、內存模塊和分支預測模塊,其中,因爲內存模塊和分支預測模塊相對比較獨立,所以實現爲獨立的兩個類MemoryManagerBranchPredictor

模擬器中最核心的函數是simulate()函數,這個函數對模擬器進行週期級模擬,每次模擬中,會執行fetch()decode()execute()accessMemory()writeBack()五個函數,每一個函數會以上一個週期的流水線寄存器做爲輸入,並輸出到下一個週期的流水線寄存器。在週期結束時,新的寄存器的內容會被拷貝到做爲輸入的寄存器中。在執行過程當中,每一個函數都會處理有關數據、控制和內存訪問冒險的內容,而且在適當的地方記錄歷史信息。因爲之間的交互關係比較複雜,所以在上圖中並無畫出。因爲相關函數代碼過長,不便於在此貼出,所以關於實現的更多細節請參見src/Simulator.cpp

3、具體設計和實現

3.1 內存管理模塊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

3.2 可執行文件的裝載、初始化

本模擬器的可執行文件加載部分採用了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();

3.3 指令語義的解析和控制信號的處理

本小節中涉及代碼因爲廣泛過長,且存在很是強的相互依賴,單獨貼出可能難以理解,所以不會在此直接貼出代碼,具體內容請參見src/Simulator.cpp

指令的取值過程參見Simulator::fetch()函數,因爲RV64I指令集都是4字節定長,因此實現起來很是簡單。

指令的解碼過程參見Simulator::decode()函數,其中絕大多數內容都是對RISC-V Specification 2.2中規定的指令編碼的直接翻譯。在解碼過程當中,爲了便於調試,decode()函數會按照RISC-V彙編格式翻譯出指令字符串。此外,decode函數會模仿硬件實如今指令中抽象出op1op2dest等幾個共有的域。分支預測模塊會在解碼階段作出預測判斷。

指令的執行過程參見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,才能確保取指階段取出的是跳轉後的指令。這不會增長分支預測錯誤的開銷,可是會使得預測正確的開銷多了一個週期。若是要改進這個設計的話,必須將分支預測模塊轉移到取指階段實現。

3.4 系統調用和庫函數接口的處理

本模擬器使用自定義的系統調用接口。系統調用的ecall指令會使用a0a7寄存器,其中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

3.5 性能計數相關模塊

在當前模擬器架構下,對於模擬器進行性能統計只需在代碼裏適當的地方加入統計代碼便可。數據統計模塊的定義以下

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的執行歷史,便於在調試的時候使用。爲了防止模擬器佔用過多內存,instRecordregRecord當內容多於100000條時會被清空,memoryDump只會在要求生成內存快照時被使用。

3.6 調試接口

因爲對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) 
-----------------------------------

3.7 實現中遇到的坑

在整個實現中,我在第一階段的單週期指令級模擬的實現並無遇到什麼問題,可是流水線相關的模擬中,遇到了幾個至關微妙的錯誤。

  1. 一個根本的困難在於咱們對流水線的模擬程序本質上仍是線性執行的,並不能像硬件那樣多階段並行執行。所以,必須很是當心地設計五個階段的代碼的執行流和對數據結構的訪問,才能模擬出硬件的效果。
  2. 當多個階段發現數據冒險並向前轉發數據時,必須優先傳送更新的數據。在模擬器中,因爲相關階段的執行順序是執行->訪存->寫回,所以會存在前面的階段向前轉發的數據被後面的階段的舊數據覆蓋的可能。對於這種狀況,模擬器中必須加以特別的斷定。
  3. 分支預測模塊應當在解碼階段根據預測結果修改PC的值,可是,若是這個跳轉指令是被錯誤取進來,而且應該在以後被Bubble的話怎麼辦?必須想辦法恢復被修改的PC值,或者延遲寫入預測的PC值。
  4. 也是因爲代碼是順序執行的,所以當執行階段發現訪存指令,而解碼階段的指令依賴訪存數據並致使內存冒險時,必須很是當心地設計整個執行過程和數據訪問流程,才能模擬出正確的結果。
  5. 用於系統調用的ecall指令也會致使數據冒險!而且產生數據冒險的條目,取決於這個系統調用的參數數量和其對應的寄存器!當前的系統調用會依賴的寄存器有a0a7兩個,所以恰好能做爲op1op2塞入流水線,可是若是系統調用須要的參數更多,實現將會變得更爲複雜。
  6. zero寄存器是一個至關獨特的存在,理論上他任什麼時候候值應該都是0,因此進行數據轉發的時候必須到處特判零寄存器,若是向零寄存器裏的值進行數據轉發就會致使很是難以發現的錯誤。

4、功能測試與性能評測

4.1 模擬器的功能正確性測試

我本身編寫的測試程序見下表,注意全部的程序都須要和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
-----------------------------------

4.2 運行給定的5個測試程序

4.2.1 原始的執行結果

給定的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
-----------------------------------

從這些原始數據中能夠分析獲得要求的結果,下面會對這些結果進行總結。

4.2.2 動態執行的指令數

程序名 執行的指令數
add.riscv 876
mul-div.riscv 901
n!.riscv 1112
qsort.riscv 19427
simple-function.riscv 886

4.2.3 執行週期數和平均CPI

程序名 執行週期數 平均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倍的指令吞吐量。

4.2.4 不一樣類型的冒險統計

程序名 數據冒險 控制冒險 內存訪問冒險
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

5、其它內容

5.1 分支預測模塊

分支預測模塊是一個相對比較獨立的模塊,所以單獨實現爲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;
}

這樣便可完成分支預測模塊的實現,而且很容易可以擴展出新的分支預測策略。

5.2 分支預測模塊的性能評測

有趣的是,有了這個分支預測模塊以後,咱們能夠對不一樣分支預測策略的性能進行評測。下面的表格是一個對分支預測準確率的簡單統計。

評測程序 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%左右徘徊。

5.3 意見和建議

  1. 編寫RISC-V CPU模擬器極大地鍛鍊了個人系統編程能力。雖然在編寫的過程當中遇到了一些難以解決的Bug,但在解決它們的過程當中,使我收穫了不少Debug經驗,而且更加深入地認識到了編寫健壯和包含完備錯誤處理程序的重要性。
  2. 在配置RISC-V環境的過程當中,我發現RISC-V工具鏈存在一些文檔缺失的問題,有時會遇到默認配置比較奇怪或者一些參數過期的問題,爲安裝相關工具形成了一些困難。我但願要是能在每次Lab發佈前,能給出配置環境的一些有關教程就更好了。
  3. 計算機體系結構課教的體系結構是MIPS,不知道爲何Lab卻要作RISC-V,有一點增長了學習成本和完成Lab的時間。
相關文章
相關標籤/搜索