最初,我几年前以ARM的执行核心验证工程师的身份编写了此文档。 当然,我的观点受到与不同处理器的执行核心的深入合作的影响。 因此,请打折以免打折:也许我太过分了。
但是,我仍然相信RISC-V的创建者可以做得更好。 另一方面,如果我今天设计了32位或64位处理器,那么我可能会实施这种架构以利用现有工具。
本文最初描述了RISC-V 2.0指令集。 对于2.2版,它进行了一些更新。
原始前言:一些个人意见
RISC-V指令集已减少到绝对最小值。 人们对减少指令数量,规范化编码等给予了极大关注。这种对极简主义的渴望导致了错误的正交性(例如,将相同的指令重新用于转换,调用和返回)和强制性的冗长性,从而夸大了大小和数量。说明。
例如,这是C代码:
int readidx(int *p, size_t idx) { return p[idx]; }
这是索引数组的简单情况,这是非常常见的操作。 这是x86_64的编译:
mov eax, [rdi+rsi*4] ret
或ARM:
ldr r0, [r0, r1, lsl #2] bx lr // return
但是,对于RISC-V,需要以下代码:
slli a1, a1, 2 add a0, a1, a1 lw a0, a0, 0 jalr r0, r1, 0 // return
简化RISC-V通过执行更多指令来简化解码器(即CPU前端)。 但是缩放流水线的宽度是一个困难的问题,同时很好地实现了对轻微(或强烈)不规则指令的解码(主要困难出现在难以确定指令长度的情况下:这在带有众多前缀的x86指令集中尤为明显)。
不应简化指令集。 寄存器存储的移位来进行寄存器和寄存器的加法运算是程序中的一条简单且非常常见的指令,处理器很容易有效地实现它。 如果处理器不能直接执行该指令,那么将其分解成各个组件可能相对容易; 与合并简单操作序列相比,这是一个简单得多的问题。
我们必须将CISC处理器的“复杂”特定指令(复杂,很少使用和效率低下的指令)与CISC和RISC处理器常见的“功能”指令区分开来,后者结合了少量的操作序列。 后者经常使用且性能高。
平庸的实施
- 几乎无限的可扩展性。 尽管这是RISC-V的目标,但它创建了一个分散的,不兼容的生态系统,必须格外谨慎地进行管理。
- 相同的指令(
JALR
)用于调用,返回,寄存器间接分支,其中分支预测需要附加解码
- 通话:
Rd
= R1
- 返回值:
Rd
= R0
, Rs
= R1
- 间接过渡:
Rd
= R0
, Rs
≠ R1
- (奇怪的过渡:
Rd
≠ R0
, Rd
≠ R1
)
- 记录字段的可变长度编码不会自同步(这很常见-例如x86和Thumb-2的类似问题-但这会导致实现和安全性方面的各种问题,例如反向编程,即ROP攻击)
- RV64I需要所有32位值的字符扩展。 这导致以下事实:64位寄存器的上半部分无法用于存储中间结果,从而导致对寄存器的上半部分进行不必要的特殊放置。 最好使用带有零的扩展名(因为它减少了切换次数,并且通常可以在已知上半部为零时通过跟踪“零”位来进行优化)
- 乘法是可选的。 尽管快速乘法块可以在微小的晶体上占据相当大的面积,但是您始终可以使用速度稍慢的电路,这些电路将现有的ALU有效地用于多个乘法周期。
LR
/ SC
对部分应用程序SC
严格的升级要求。 尽管此限制非常严格,但是对于小型实现(尤其是没有缓存),它可能会带来一些问题。
- 存储器粘性位FP和舍入模式位于同一寄存器中。 如果执行RMW操作以更改舍入模式,则这需要对FP通道进行序列化。
FP
指令的编码精度为32位,64位和128位,但不是16位(在硬件中比128位更常见)
- 这很容易解决:尺寸为
0b10
代码0b10
免费的。
- 更新: 小数位占位符出现在2.2版中,但是没有半精度占位符。 头脑是不可理解的。
- FP值在FP寄存器文件中的表示方式未定义,但可以观察(通过加载/存储)
- 仿真器作者会恨你
- 迁移虚拟机可能变得不可能
- 更新: 2.2版要求更宽的NaN装箱值
不好
- 没有条件代码,而是使用compare-and-branch语句。 这本身不是问题,但后果令人不快:
- 由于需要对一个或两个寄存器说明符进行编码,因此减少了条件分支中的编码空间
- 没有条件选择(对于非常不可预测的过渡很有用)
- 没有结转或借入的结转/减法
- (请注意,这仍然比将标志写入通用寄存器然后切换到接收到的标志的命令集更好)
- 似乎在无特权的ISA 中需要高精度计数器(硬件周期)。 实际上,为他们提供应用程序是第三方渠道攻击的极好载体
- 乘法和除法是同一扩展的一部分,似乎如果实现了一个,则另一个也应该实现。 乘法比除法简单得多,并且在大多数处理器上很常见,但除法却不是。
- 基本指令集体系结构中没有原子指令。 多核微控制器变得越来越普遍,因此原子指令(例如LL / SC)变得便宜(对于单个[多核]处理器中的最少实现,只需要一个处理器状态位)
LR
/ SC
与更复杂的原子指令具有相同的扩展名,这限制了小型实现的灵活性
- 一般原子指令(不是
LR
/ SC
)不包括CAS
原语
CmpHi:CmpLo
要避免需要读取五个寄存器( Addr
, CmpHi:CmpLo
, SwapHi:SwapLo
)的SwapHi:SwapLo
,但这将比保证的前向LR
/ SC
带来更少的实现开销。替代品
- 提供的原子指令适用于32位和64位值,但不适用于8位或16位值
- 对于RV32I,无法通过整数在FP寄存器文件之间传输DP FP值,除非通过内存,也就是说,从32位整数寄存器不可能生成64位双精度浮点数,必须首先将中间值写入内存并加载他从那里进入寄存器文件
- 例如,RV32I中的32位
ADD
指令和RVI64中的64位ADD
具有相同的编码,并且在RVI64中ADD.W
另一个ADD.W
编码。 对于同时执行这两个指令的处理器来说,这是不必要的复杂操作-最好添加新的64位编码。
- 没有
MOV
指令。 MV
命令的助记码由汇编器翻译为指令MV rD, rS
> ADDI rD, rS, 0
。 高性能处理器通常MOV
都会优化MOV
指令,同时对指令进行大量重新排序。 在RISC-V中,选择具有直接12位操作数的指令作为MV
指令的规范形式。
- 在没有
MOV
的情况下MOV
ADD rD, rS, r0
指令ADD rD, rS, r0
实际上比规范的MOV
更可取,因为它更易于解码,并且通常对CPU中零寄存器(r0)的操作进行了优化。
糟透了
JAL
在通信寄存器的编码上花费了5位,该位始终等于R1
(或用于过渡的R0
)
- 这意味着RV32I使用21位分支位移。 对于大型应用程序(例如Web浏览器),如果不使用多个命令序列和/或“分支岛”,这是不够的
- 与命令体系结构的1.0版相比,这是一个恶化!
- 尽管付出了巨大的努力来统一编码,但是加载/存储指令的编码方式却有所不同(大小写和立即数字段会发生变化)
- 显然,输出寄存器的编码正交性优于两个紧密相关的指令的编码正交性。 考虑到地址生成对时间的要求更高,这种选择似乎有些奇怪。
- 没有带有寄存器偏移量(
Rbase
+ Roffset
)或索引( Rbase
+ Rindex
<< Scale
)的内存加载指令。
FENCE.I
隐含了指令缓存与所有先前存储库的完整同步,有无隔离。 实现需要清除栅栏上的所有I $或寻找D $和存储缓冲区
- 在RV32I中,读取64位计数器需要读取两次上半部分,如果在读取操作期间在下半部分和上半部分之间进行传输,则进行比较和分支
- 通常,32位ISA包含读取特殊对寄存器指令,以避免出现此问题。
- 没有在体系结构上定义的用于提示编码的空间,因此该空间中的指令不会在较旧的处理器(作为
NOP
处理)上引起错误,但会在最现代的CPU上执行某些操作
- 纯NOP提示的典型示例是自旋锁产量
- 较新的处理器还具有更复杂的提示(较新的处理器具有明显的副作用;例如,在提示空间中编码了x86边界检查指令,以便二进制文件保持向后兼容)