发布时间:2020-12-28 15:58:10来源:电子产品世界
前言
本文最初完成于几年之前,彼时作者正在ARM公司担任执行核心验证工程师职位。作者当时的工作深入或围绕多种处理器核心,而文中提到的观点深受这些经验的影响,换句话说,这些观点存在不同程度的偏见。
作者依旧坚持认为RISC-V的设计并不完美,但同时也承认,如果现在需要搭建一个32或64位的CPU,他在实现构建时也会从现有工具中受益。
本文主要基于RISC-VISA规范v2.0,部分已更新至v2.2。
一些观点
RISC-VISA对极简主义的追求钻了牛角尖,它极力强调减少指令数量,规范编码等等。而这种追求则导致了错误的正交性(分支、调用、返回时重复使用同一指令),以及对赘余指令的需求,这些在程序大小和指令数量上都会影响到代码密度。
以下面的C代码为例:
intreadidx(int*p,size_tidx){returnp[idx];}
简单的数组索引,非常常见的操作。将其在x86_64中编译:
moveax,[rdi+rsi*4]ret
或者是ARM中:
ldrr0,[r0,r1,lsl#2]bxlr//return
但是在RISC-V中需要的代码则是:
#很抱歉如果有任何语法错误,risc-v并没有在线编译器sllia1,a1,2adda0,a1,a1lwa0,a0,0jalrr0,r1,0//return
RISC-V的极简主义让解码器(CPU前端)变得更简单,代价则是需要执行更多的指令。然而,相对于拓宽流水线这个难题而言,解码不规则指令的问题很好解决,主要难点在于确定指令的长度是否一致。x86的众多前缀就是个极佳的反面教材。对指令集的简化不应追求极限。寄存器+移位寄存器的内存操作指令是程序中非常常见且简单的操作,对于CPU而言也很容易实现。即使无法直接执行,CPU也可以相对轻松地将其分步执行,其操作复杂程度远逊色于融合简单操作的序列。
CISCCPU中的“复合”指令,繁复、少有使用且普遍性能低下,而CISC和RISCCPU通用的“功能”指令则意指结合了少量操作序列并且使用率高、性能高的指令。这二者应当有所区分。
还不错的部分
几乎不受任何限制的可扩展性。虽说这是RISC-V的卖点,但它同时也是碎片化、不兼容生态系统的罪魁祸首,在管理时还需加倍小心。
调用、返回和寄存器间接分支使用同一指令(JALR)。分支预测需要额外解码。
调用:Rd=R1
返回:Rd=R0,Rs=R1
间接分支:Rd=R0,Rs≠R1
(奇怪分支:Rd≠R0,Rd≠R1)
可变长度编码无法自我同步。x86和Thumb-2中都存在的常见问题,会导致实现和安全性方面的各种漏洞,例如面向返回的编程攻击。
RV64I规定所有32位值的符号扩展。这一点会导致不必要的上半切换,或者需要对寄存器的上半部分进行特殊调整。建议采用零扩展,在减少切换的同时,通常还可以在已知上半部分为零的情况下,通过追踪”为零“位来进行优化。
乘法是可选项。考虑到高速乘法器在微型实现中占用的面积不容忽视,创建占用更小,还可以将现有ALU广泛用于多循环乘法的小型乘法器不失为良策。
LR/SC指令对有限使用子集有严格的最终转发要求。尽管这项限制颇为严苛,但对于没有缓存的小型实现而言有可能会带来一些问题。
这一点似乎是CAS指令的替代品,具体请参照有关该指令的注释。
FP粘性位和舍入模式处于同一寄存器中。如果想通过执行RMW操作改变舍入模式,则需要对FP管道进行序列化。
FP指令支持的编码精度有32位、64位和128位,唯独没有硬件中更为常见的16位。
这点很容易修正:我们有免费的字组编码2’b10。
更新:v2.2中添加了十进制FP扩展占位符,但仍然没有半精度占位符。迷惑行为。
FP寄存器文件中的FP值未指定,但可以通过加载/存储观察到。
仿真器作者要恨死你了。
VM迁移会将变为不可能。
更新:v2.2需要NaN装箱更宽的值。
糟糕的部分
没有条件代码,只有比较和分支指令。这一点自身没什么问题,但它意味着:需要编码一到二个寄存器说明符,导致条件分支中的编码空间减少。
没有条件选择,这一点在高度不可预测的分支中很有用。
加法/减法没有加进位或借位。(即使这样,这也比ISA将flag写入通用寄存器GPR,然后在结果flag上分支要好。)
用户级ISA需要高精度计数器。在实践中,将这些计数器暴露给应用程序意味着侧通道攻击的好机会。
乘法和除法同属于一个扩展,无法单独实现其中之一。相比除法,乘法要简单许多,而且在大多的CPU上很常见。
基础ISA中没有原子指令。多核微型处理器越来越普遍的今天,LL/SC类型原子指令也越来越廉价:只需要1位CPU状态即可完成最小CPU实现。
LR/SC和更复杂的原子指令同属于一个扩展。直接限制了小型实现的灵活性。
▶非LR/SC的一般原子指令不包含CAS原语
CAS的设计是为了避免需要一条指令读取5个寄存器的情况,例如:加法器、Cmp:CmpLo,SwapHi:SwapLo。但LR/SC用于取代CAS的保底进度很可能只会在实现上带来更高的开销。
原子指令仅支持32位或64位操作,不支持8位或16位。
对RV32I而言,想在整数和浮点寄存器文件之间转换DP和FP,只能通过内存解决。
举例来说:RV32I的32位ADD和RV64I的64位ADD共用同一套编码,RV64I又多加了一套ADD.W编码。如此一来,CPU实现这两种指令时麻烦了许多,不如直接新增一套64位编码。
没有MOV指令。汇编器对于MV的等效指令是:MVrD,rS->ADDrD,rS,0。MOV优化通常由高端处理器,尤其是失序处理器完成。识别RISC-V规范的MV需要一个12位的立即数。
在没有MOV指令的情况下,ADDrD,rS,r0是对MOV不错的替代。它更易被解码,而CPU通常也会有特殊情况下的逻辑来识别零寄存器。
尤为糟糕的部分
JAL在本该只是R1(分支时是R0)的链接寄存器编码上浪费了5比特。
这意味着RV32I有21位的分支位移(对于诸如浏览器等大型应用时,不使用多指令序列或者分支island时会不够用)。
▶其实是1.0版本ISA的历史遗留问题
尽管RISC-V在统一编码上花了大功夫,但加载/存储指令的编码仍然是不同的(寄存器vs立即字段互换)。
似乎寄存器编码的最终正交性要比两种高度相关指令的正交性更受欢迎。考虑到地址生成是对时序更为敏感的操作,这种选择有点奇怪。
寄存器偏移量(Rbase+Roffset)或索引(Rbase+Rindex<
FENCE.I意味着指令缓存和前面的存储区必须完全同步,无论是否有fence。实现时需要在fence上刷新I,或者通过snoop的方式监视D和存储缓存区。
RV32I中,读取64位计数器需读取上半部分两次,并进行比较和分支,以防在读取操作时下半部分和上半部分发生借位。
通常32位ISA包含了一个“读取一对特殊寄存器”的指令来避免这个问题。
架构上没有定义“提示”编码空间。提示编码是指在当前处理器上作为NOP执行,但在之后的变量上有操作的编码。
“NOP提示”的常见例子是自旋锁yield。
更复杂的提示也有实现。即那些对新处理器有明显副作用的提示,例如x86的边界检查指令被编码在提示空间,以便二进制文件保持向后兼容。
原文地址
https://gist.github.com/erincandescent/8a10eeeea1918ee4f9d9982f7618ef68
本文转自InfoQ中文站,版权归原作者所有,如有侵权请联系删除
首发地址:
https://www.infoq.cn/article/qp5c2tUjk88zE2EipZuE
扫码入群
扫码添加管理员微信
加入“电子产品世界”粉丝交流群
↓↓↓↓点击阅读原文,查看更多新闻