iT邦幫忙

0

RISC-V on Rust 從零開始(8) - 實作instruction decoder

這次要來實作指令decoder,負責pipeline中的decode stage。計組教科書上常見的pipeline架構依序為:fetch、decode、execute、memory、write back,當然在一個instruction-accurate(IA)的模擬器中,沒有必要去切得這麼細,目前只要大致分成fetch、decode以及execute三個步驟就好,memory與write back就合併在execute stage。這三個步驟做的處理羅列如下:

  • Fetch: 從memory中讀取指令。
  • Decode: 解析指令的raw byte,並且呼叫指令相對應的函式。
  • Execute: 執行指令,依據指令的不同可能會讀寫記憶體(對應memory stage),或修改暫存器或PC的內容(對應write back stage)。

Fetch Stage

暫時先以常數取代,等memory model實作完成後再來時做此stage。

Decode Stage

稍微研究了現有的模擬器是如何實作decoder,發現主要有兩種方法:

  • 將指令的特徵放在一個array,每次decode都去查表看符合哪個entry,很明顯這個方法複雜度為O(n),n為指令列表的長度。通常這個方法會配合另一個buffer來記錄最近match到的指令,每次decode就先查buffer,沒有的話才查原本完整的表,由於locality的關係,只要這個buffer的大小合適,就能大大的降低decode所需的時間。
fn decode(inst_bytes) -> InstID {
    for entry in instruction_table_buffer {
        if (inst_bytes & entry.mask) == entry.opcode
            return entry.inst_id
            
    for entry in instruction_table {
        if (inst_bytes & entry.mask) == entry.opcode {
            instruction_table_buffer.add(entry)
            return entry.inst_id
        }
    }
}
  • 利用條件判斷的方式,判斷是哪個指令,複雜度為O(1),雖然複雜度較低,可以預見code會比較雜亂,會有大量的switch-case。
fn decode(inst_bytes) -> InstID {
    switch (inst_bytes & mask_a) {
        case 0: {
            switch (inst_bytes & mask_b) {
                case 0:
                    return InstID::SUB
                ...
            }
        }
        case 1:
            return InstID::ADD
        ...
    }
}

這邊採用第二種方案,並且保留彈性,之後也可以實作第一種方案,並且比較兩種的效能。

Execute Stage

很簡單,直接呼叫對應的指令function就好。

Summary

將以上的三個stage結合起來就可以完整的執行一條指令:

impl RVCore {
    fn step(&mut self, inst_bytes: u32) {
        // Decode
        let inst = self.id_instance.decode(inst_bytes);
        
        // Execute
        (inst.operate)(self, &inst);
        self.pc += inst.len;
    }
}

let mut core = RVCore{};
// Fetch, 假設fetch的結果為0x00002197(AUIPC)
core.step(0x00002197);

完整的程式碼可以參考此連結


尚未有邦友留言

立即登入留言