网红RISC-V有何特别之处?
扫描二维码
随时随地手机看文章
20世纪90年代末,RISC和CISC爆发了一场大战,自那以后,大家都说RISC和CISC的区别没那么重要了。许多人表示,指令集也就那么回事,对CPU没什么太大的影响。但其实不然,指令集决定了我们可以轻松为微处理器做哪些优化。
本文将介绍RISC-V处理器是如何设计指令集的,这样的设计具有什么好处。
我最近一直在了解有关RISC-V指令集架构 (ISA) 的更多信息,说起来,关于RISC-V ISA有几件事给我留下了非常深刻的印象: ◆ 它是一种RISC指令集,体积小,易于学 习 。 不管是任何人,只要有兴趣学习微处理器,选它准没错。 ◆ 它在大学数字化设计教学中占据重要地位:为什么大学要学RISC-V。 ◆ 经过巧妙地设计,它允许CPU设计生产者使用RISC-V ISA打造高性能微处理器。 ◆ 无需授权费,且被设计成允许简单的硬件实现,按道理,专业爱好者可以在合理的时间内完成他自己的RISC-V CPU设计。 ◆ 易于修改和使用的开源设计:The Berkely Out-of-Order(BOOM)RISC-V处理器。
01
RISC的复仇
当我开始更深入地理解RISC-V时,意识到RISC-V是一种根本性的转变,它回到了许多人认为已经过时的计算时代。在设计方面,RISC-V就像乘着时光机穿越回了上世纪八九十年代的经典RISC时代。近年来,许多人指出RISC和CISC的区别不再重要,因为像ARM这样的 RISC CPU已经添加了太多的指令,很多指令相当复杂,以至于它现在更像是一个混合的RISC CPU,而不是纯粹的RISC CPU。而那些RISC CPU也是如此,比如PowerPC。 相比之下,RISC-V是RISC CPU真正的硬核。事实上,如果你在网上看过大家就RISC-V的讨论,会发现有人声称RISC-V出自一些老学究式的RISC激进分子之手,他们拒绝与时俱进。 前ARM工程师Erin Shepherd几年前就RISC-V写过一篇评论,内容很有意思: RISC-V ISA过分追求极简主义。它非常强调最小化指令数、规范化编码这些事。这种对极简主义的追求导致了错误的正交性(例如针对分支、调用和返回重用同一条指令)和对冗余指令的需求,这在指令大小和数量这两个方面影响了代码密度。 我向大家简单介绍一下背景。让代码更小对性能有好处,因为这样就可以更容易将正在运行的代码保存在高速CPU缓存中。 本文在此批评的是,RISC-V设计者太过于追求拥有小的指令集。尽管,这是RISC最初的目标之一。然而,这样做的弊端是,实际上程序在完成工作时将需要更多的指令,从而消耗更多的内存空间。多年来,人们普遍认为RISC处理器应该增加更多的指令,变得更像CISC。其理论依据是,更专门化的指令可以替代对多个通用指令的使用。
02
压缩指令和宏融合
然而,在CPU设计中有两项特别的创新,这些创新从许多方面使添加更多复杂指令的策略变得多余:◆ 压缩指令——在内存中压缩指令,并在CPU的第一阶段进行解压。 ◆ 宏融合——将两个或两个以上由CPU读取的简单指令融合成一个复杂指令。 实际上ARM已经采用了这两种策略,而x86 CPU则采用了后者,所以这不能算是RISC-V的新招术。 然而,关键是RISC-V从这些策略中获得了更大的优势,这里面有两个重要原因: ◆ 它从一开始就添加了压缩指令。 在 ARM 上使用的是 Thumb2 压缩指令格式,这就必须将其作为一个单独的ISA来添加以完成改造,这需要一个内部模 式切换和单独的解码器来进行处理。 RISC-V压缩指令可以添加到具有400个额外逻辑门 (AND、OR、NOR、NAND 门) 的CPU上。 ◆ RISC执着地控制独特指令的数量得到了回报,如此就有了更多的空间来容纳压缩指令。
03
指令编码
这一部分需要进行一些解释。指令在RISC体系结构上通常是32位(即比特)宽的。这些比特需要用来编码不同的信息。例如,下面这样一条指令: ADD x1, x4, x8 # x1 ← x4 + x8
这条指令将累加寄存器x4和x8的内容,然后将结果存储在x1中。我们需要多少比特来编码,这取决于我们有多少寄存器。RISC-V和ARM64有32个寄存器。可以用5比特表示数字32:
2⁵ = 32
因为我们需要指定3个不同的寄存器,所以我们需要总共15比特 (3×5) 来编码操作数 (累加操作的输入)。 因此,我们想要在指令集中支持的东西越多,那么就会消耗掉那32比特中更多的比特。当然,我们可以使用64位指令,但是这会消耗太多的内存,从而降低性能。 通过刻意压低指令的数量,RISC-V节省下更多的空间来添加表示我们正在使用的压缩指令的比特。如果CPU看到指令中的某些位被设置为1,它就知道这条指令应该作为一条压缩指令来进行解释。 04
压缩指令——二到一
这表示,我们可以将两条16位宽的指令装入一个32位字,而不是一个32位字只装入一条指令。当然,并不是所有的RISC-V指令都可以用16位格式表示。因此,32位指令的子集是根据它们的效用和使用频率来挑选的。未压缩指令可以接受3个操作数 (输入),而压缩指令只能接受2个操作数。因此,压缩后的ADD指令应该如下所示:(# 号后为注释)
C.ADD x4, x8 # x4 ← x4 + x8
RISC-V汇编程序使用前缀 c. 来指示一条指令应该被汇编程序转换成一个压缩指令。但实际上你不需要去写它。RISC-V汇编程序将能够在适当的时候选择是压缩指令还是非压缩指令。 基本上,压缩指令减少了操作数的数量。三个寄存器操作数将消耗15比特,而留给我们指定操作的就只剩下1比特了!因此,将操作数减少到两个,我们就能剩下6比特来指定操作码 (要执行的操作)。 这实际上接近于x86汇编的工作方式,在x86汇编中没有足够的比特来保留3个寄存器操作数。取而代之的是,x86使用一些比特来允许像ADD这样的指令从内存和寄存器中读取输入。 05
宏融合——一到二
但是,当我们将指令压缩与宏融合结合起来看时,才能发现真正的收益。你看,如果CPU得到一个包含有两个16比特的压缩指令的32比特的字,它可以把这些合并成一条单一的复杂指令。这听起来很荒谬,我们不是又回到原点了吗?我们是不是又回到CISC风格的CPU,这不正是我们试图要避免的吗? 不是的,因为我们避免用大量复杂的指令、x86和ARM策略填充ISA规范。相反,我们基本上是通过各种简单指令的组合来间接地表达一整套复杂指令。 在正常情况下,宏融合存在一个问题:虽然两条指令可以被一条指令替换,但它们仍然会消耗两倍的内存空间。但是使用指令压缩,我们不会消耗更多的空间。我们做到了两全其美。 让我们来看看艾琳·谢泼德的一个例子。在她批评RISC-V ISA时,展示了一个简单的C函数。为了解释起来更清楚一些,我把它重新写了下来,内容如下:
int get_index(int *array, int i) {
return array[i];
}
在x86上编译成:
mov eax, [rdi+rsi*4]
ret
当你在编程语言中调用函数时,参数通常会根据既定的约定传递给寄存器中的函数,这将取决于你所使用的指令集。在x86上,第一个参数放在寄存器 rdi 中,第二个放在 rsi 中。按照惯例,返回值必须放在寄存器eax中。 第一条指令将 rsi 的内容乘以4。它包含了变量 i。为什么乘?因为数组是由整数元素组成的,所以它们之间的间距为4个字节。因此,数组中的第三个元素的字节偏移量实际上是3×4=12。 然后,我们把它添加到rdi,它包含数组的基址。于是,我们得到了数组第 i 个元素的最终地址。我们读取该地址的存储单元的内容,并将其存储在 eax 中:大功告成。 在ARM上与之很相似:
LDR r0, [r0, r1, lsl #2]
BX lr ; return
这里我们不是乘以4,而是将寄存器r1向左平移2位,这就相当于乘以 4。这也可能是更本真地表示了x86代码中所发生的情况。在x86上,你只能乘以2、4、8,所有这些其实都可以通过左移1、2、3位来实现。 我想,x86描述中的剩余内容你肯定都能猜得到了。现在让我们进入 RISC-V,真正有趣的内容开始喽!(# 号后为注释)
SLLI a1, a1, 2 # a1 ← a1 << 2 ADD a0, a0, a1 # a0 ← a0 + a1 LW a0, a0, 0 # a0 ← [a0 + 0] RET
RISC-V上的寄存器a0和a1只是x10和x11的别名。它们是放置函数调用的第一个和第二个参数的地方。RET是一条伪指令(简写):
JALR x0, 0(ra) # sp ← 0 + ra
# x0 ← sp + 4 ignoring result
JALR 跳转到 ra 引用返回地址的地址。ra 是 x1 的别名。 不管怎样看,这似乎都很糟糕,对吧?对于像在表中执行基于索引的查找并返回结果这样简单而常见的操作,需要两倍的指令。 看上去确实很糟糕。这就是为什么艾琳·谢泼德强烈批评了 RISC-V 的设计选择。她写道: RISC-V 的简化使解码器 (即 CPU 前端) 更简单,但代价是执行更多的指令。然而,真正棘手的问题是扩展流水线的宽度,而稍稍不规则甚至很不规则的指令其解码都不会有太大的问题,主要难点是确定指令的长度,尤其是 x86,因为它有很多前缀。 然而,多亏了有指令压缩和宏融合,我们可以扳回这一程。
C.SLLI a1, 2 # a1 ← a1 << 2 C.ADD a0, a1 # a0 ← a0 + a1 C.LW a0, a0, 0 # a0 ← [a0 + 0] C.JR ra
现在,这和 ARM 的例子中所占用的内存空间是完全相同的。 好吧,接下来让我们做一些 宏融合! 在 RISC-V 中允许将多个操作融合为一个的规则之一是,目标寄存器得是相同的。ADD 和 LW(加载字) 指令就属于这种情况。因此,中央处理器将把这些转换成一条指令。 如果 SLLI 也是这样的话,我们就可以把这三条指令融合成一条了。因此,CPU 会看到一些类似于更复杂的 ARM 指令的东西:
LDR r0, [r0, r1, lsl #2]
06
为何不能在代码中直接编写此宏操作
因为我们的 ISA 不包含对它的支持!记住,可用的比特数是有限的。为什么不把说明写长一点呢?不行,那样会消耗太多的内存,并且会更快填满宝贵的 CPU 缓存。然而,如果我们在 CPU 内部制造这些半复杂的长指令,也没有什么可担心的。CPU 在任何时候所面对的指令最多也不过几百条。所以在每条指令上浪费个 128 比特不是什么大问题。每个人都有足够的硅。 因此,当解码器得到一条正常指令时,它通常会把它转换成一个或多个“微”操作。这些“微”操作是 CPU 实际要处理的指令。它们可以非常地“宽广”,包含很多额外的有用信息。称之为“微”似乎有些讽刺,因为它们其实很“广”。然而事实上“微”指的是它们做的任务数量有限。
07
指令的复杂性
宏融合将解码器的工作做了一点改变:不再是将一条指令转换成多个微操作,而是将多个操作转换成一个微操作。因此,在现代 CPU 中发生的事情看起来相当奇怪:
-
首先,它通过压缩将两条指令合并为一条指令。
-
然后借助解压把它分成两部分。
-
通过宏融合将它们合并到一个操作中。
-
不能太复杂,否则无法在为每条指令分配的数量固定的时钟周期内完成。
-
不能太简单,因为那纯粹就是浪费CPU资源。执行两个微操作的时间是执行一个微操作的时间的两倍。
08
RISC的好处
好了,以上解释了很多细节,也许你很难一下子弄清楚重点是什么。为什么要进行压缩和融合?听起来有很多额外的工作要做。首先,指令压缩和zip压缩完全不同。“压缩”这个词其实有点用词不当,因为立即解压一条已压缩的指令非常简单。做这件事并不浪费时间。记住,对于RISC-V来说这很简单。只需400个逻辑门,就可以完成解压。 宏融合也是如此。虽然这看起来很复杂,但这些方法已经在现代微处理器中得到了应用。因此,这种复杂性的学费早就已经交过了。 然而,与ARM、MIPS和x86设计者不同的是,RISC-V设计者在开始设计ISA时就了解指令压缩和宏融合。或者更准确地说,当他们最初的ISA被设计出来的时候,那些竞争对手们并不知道这一点。当设计64位版本的 x86 和 ARM 指令集时,他们可能已经考虑到了这一点。那么,为什么他们没有这样做呢,我们只能揣测。可能是这些公司制作新的ISA时,不喜欢过多地偏离早期版本吧。通常它更着重于消除以往明显的错误,而不是颠覆之前的理论基础。 通过对第一个最小指令集展开各种测试,RISC-V的设计者有了两个重要的发现:
-
通常RISC-V程序占用的内存空间接近或少于任何其他CPU体系结构,包括x86,鉴于x86是CISC ISA,所以被公认是最节省空间的。
-
它需要执行的微操作数比其他ISA更少。
09
RISC-V设计策
RISC-V利用了我们当今对现代CPU的了解,并用这些知识指导了他们在设计时的选择。例如,我们知道:-
如今,CPU内核会提前做分支预测。它们的预测正确率超过90%。
-
CPU内核是超标量体系结构的,这意味着它们在并行执行多条指令。
-
使用无序执行做到超标量体系结构。
-
它们是流水线式的。
10
业界有什么说法?
好吧,从理论上这听起来可能很好,但在现实世界中也果真如此吗?科技公司对此有什么看法?他们是否认为RISC-V ISA比商业ISA(如ARM)提供了实实在在的好处? RISC-V甚至不在备选的采购清单上,但随着Esperanto的工程师们对它越来越多的研究,他们渐渐意识到它不仅仅是一个玩具或者是一个教学工具。“我们认为RISC-V(相对于Arm或MIPS或SPARC)可能会损失30%到40%的编译效率,因为它太简单了。”Ditzel说。“但我们的编译器专业人员对它进行了基准测试,难以置信的是只有1%左右。” Esperanto Technologies现在只是一家小公司。像英伟达这样拥有大量经验丰富的芯片设计师和资源的大公司呢?英伟达在他们的板卡上使用了一种叫做“猎鹰”的通用处理器。在评估备选方案时,RISC-V名列前茅。免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!