本文目标
本篇文章藉由阅读 rv32emu
这个开源的 risc-v emulator
原始码介绍处理器如何执行组合语言,了解其工作原理後再看看如何透过 pinpline 和 branch-prediction 提升处理器效能。
2022/02/06: rv32emu-next 已经重新命名为 rv32emu。
run()
or run_and_trace()
rv_step()
。rv_step()
拿出指令并判断其类型 (load, jump, store, branch...) 後呼叫 op()
,让其做 dispatch 和後续动作。rv_step()
达到 cycle 目标之前重复以下动作:
op()
进行处理。op()
riscv.c
的第 787 行处有预先定义好 RV32I 各类指令的 opcode (前五码)。static const opcode_t opcodes[] = {
// 000 001 010 011 100 101 110 111
op_load, op_load_fp, NULL, op_misc_mem, op_op_imm, op_auipc, NULL, NULL, // 00
op_store, op_store_fp, NULL, op_amo, op_op, op_lui, NULL, NULL, // 01
op_madd, op_msub, op_nmsub, op_nmadd, op_fp, NULL, NULL, NULL, // 10
op_branch, op_jalr, NULL, op_jal, op_system, NULL, NULL, NULL, // 11
};
只定义前 5 码是因为 RV32I opcode 的後两码都是固定的 (xxxxx11
),我们也可以在 rv_step()
中看到待执行指令 inst
的预处理:
// standard uncompressed instruction
if ((inst & 3) == 3) {
const uint32_t index = (inst & INST_6_2) >> 2;
简单来说就是判断 inst
是否属於 RV32I 指令 (末两码是否为 11
。),如果是的话我们就将末两码移除并且做 right_shift
。
补充 1 :
INST_6_2
定义在riscv_private.h
中,其值为0b00000000000000000000000001111100
。补充 2 : 除了 RVC 指令集外,其他合法 RISC-V 指令集的 OPCODE 末两码都是
11
。题外话: 在上面的原始码中就有大量的 bitwise 操作,再次凸显它的重要性。
op()
op
其实只是函式指标,透过 rv_step()
指定指令的 handler 後,再去做相关操作。
这边以 op_op_imm()
这个关於整数操作的 handler 举例:
static bool op_op_imm(struct riscv_t *rv, uint32_t inst)
{
// i-type decode
const int32_t imm = dec_itype_imm(inst);
const uint32_t rd = dec_rd(inst);
const uint32_t rs1 = dec_rs1(inst);
const uint32_t funct3 = dec_funct3(inst);
// dispatch operation type
switch (funct3) {
case 0: // ADDI
rv->X[rd] = (int32_t)(rv->X[rs1]) + imm;
break;
case 1: // SLLI
rv->X[rd] = rv->X[rs1] << (imm & 0x1f);
break;
case 2: // SLTI
rv->X[rd] = ((int32_t)(rv->X[rs1]) < imm) ? 1 : 0;
break;
case 3: // SLTIU
rv->X[rd] = (rv->X[rs1] < (uint32_t) imm) ? 1 : 0;
break;
case 4: // XORI
rv->X[rd] = rv->X[rs1] ^ imm;
break;
case 5:
if (imm & ~0x1f) {
// SRAI
rv->X[rd] = ((int32_t) rv->X[rs1]) >> (imm & 0x1f);
} else {
// SRLI
rv->X[rd] = rv->X[rs1] >> (imm & 0x1f);
}
break;
case 6: // ORI
rv->X[rd] = rv->X[rs1] | imm;
break;
case 7: // ANDI
rv->X[rd] = rv->X[rs1] & imm;
break;
default:
rv_except_illegal_inst(rv);
return false;
}
// step over instruction
rv->PC += 4;
// enforce zero register
if (rd == rv_reg_zero)
rv->X[rv_reg_zero] = 0;
return true;
}
handler 会将传入指令做解码,也就是将传入的 inst
根据上图的 I type 切割成四块:
funct3
会决定要做哪一种操作,如: ADDI, SLLI, ORI...。定义:
常数部分为 sign-extended 12-bit,会将 12-bit 做 sign-extension 成 32-bit 後,再与 rs1 暂存器做加法运算,将结果写入 rd 暂存器,addi rd, rs1, 0 可被用来当做 mov 指令。
将结果写回相关暂存器後,handler 会将 Program counter 指到下一个记忆体位置後回传结果。
上面的工作流程涵盖了处理器的部分行为:
恰好呼应到了本文开头提到的 RISC-V Arch 架构图。
就 rv32emu
的原始码来看,我们不难知道它会逐步处理 Fetch
, Decode
, Execute
, Write-back
,假设每个阶段耗费 1ms
,那一个指令需要 4ms
才能完成,若处理十个指令就需要 40ms
。由於其中一个阶段在执行时,其他的逻辑电路都是闲置的,这样未免有些浪费。
管线 (pinpline),又称为指令管线化。被设计来加速指令通过的速度。
如果以指令管线化的技术实现,第一个指令仍会耗费 4ms
,之後每 1ms
便能完成一条指令。
上图为一个 5 级管线的示意图,我们可以清楚的看到在 Clock Cycle = 2
时,指令 1 已经进入解码阶段 (ID),而读取指令的电路也没闲着,已经准备将指令 2 从记忆体读进处理器了。
使用管线可以有效加速指令的流通速。不过,在现实中还是会遇到一些问题以及缺陷:
周期同步问题
前面假设各个层级的耗费时间都相同。实际上,各层级所耗时间并不是相等的。因此,时钟周期就需要以最慢的 stage 下去做考量,若管线设计过长,反而会因为过长的等待时间造成反效果。
电路体积加大
将一块电路拆成多块电路实现管线,需要在每个阶层都放上大量的暂存器以保存前一层的输出。暂存器在逻辑电路中是用正反器去实现的,越长的管线就会有越多的正反器,这样便会造成电路体积加大以及废热的问题需要解决。
指令延迟问题
利用大量的暂存器去保存前一个阶层的结果也会间接造成指令的延迟问题。
分支预测问题
分支问题可被归类在 Control Hazards。
以 rv32emu
来看,在一般情况下,当一个指令完成後,程序计数器 (PC, Program Counter) 会读取记忆体中的下一个地址 (PC = PC + 4)。
实际情况中,还会有分支的可能需要考虑进去,像是常见的无条件转跳指令 jump
和条件转跳指令 branch
都会造成问题,像是,当指令 1 进入执行阶段时发现该指令会进行转跳,那指令 2 和指令 3 已经分别在解码和读取阶段了该怎麽办呢?这时处理器便会将前面的管线清空并将正确的指令放回来。要知道,清除管线是十分浪费效能的(做白工)。因此,随着管线加长,分支预测也是电脑科学家一直一来想去探讨的议题。
关於 Hazards,在本系列的浅谈分支预测与 Hazards 议题会有更进一步的探讨。
在本篇章中,读者快速的向各位介绍完了处理器的部分行为以及指令管线化的技术,如果对分支预测有兴趣可以参考本篇的延伸阅读。两篇都提到了分支预测的问题并且使用不同的方式解决加快了程序码的执行速度。
<<: Angular Reactive Form 表单 setValue 与 patchValue 差异
ResNet 简介 在当时的CNN中,都是较浅层的设计,较深层的训练未必会带来正面效果,容易训练不起...
这个看起来很像HTML的标签语法,但实际上他是JavaScript的语法所扩充出来的 JSX语法 c...
本文同步刊於 Medium 目录 前言 铁人赛 系列文提到的项目 比赛结束之後 区块链 近期规划 未...
新手在刚开始学习时,在工作上往往会遇到许多的困难,而在这边我有一些建议可以给新手 1. 学习怎麽Go...
在需要提示,或是小分页显示时 通常我们会选择 Dialog 弹跳视窗 这边的使用背景是在D14时,如...