RISC-V处理器是如何设计指令集的?有何特别之处
扫描二维码
随时随地手机看文章
- 它是一种 RISC 指令集,体积小,易于学习。不管是任何人,只要有兴趣学习微处理器,选它准没错。
- 它在大学数字化设计教学中占据重要地位:为什么大学要学 RISC-V
- 经过巧妙地设计,它允许 CPU 设计生产者使用 RISC-V ISA 打造高性能微处理器。
- 无需授权费,且被设计成允许简单的硬件实现,按道理,专业爱好者可以在合理的时间内完成他自己的 RISC-V CPU 设计。
- 易于修改和使用的开源设计:The Berkely Out-of-Order (BOOM) RISC-V 处理器
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。其理论依据是,更专门化的指令可以替代对多个通用指令的使用。
压缩指令和宏融合
然而,在 CPU 设计中有两项特别的创新,这些创新从许多方面使添加更多复杂指令的策略变得多余:
·压缩指令——在内存中压缩指令,并在 CPU 的第一阶段进行解压。 ·宏融合——将两个或两个以上由 CPU 读取的简单指令融合成一个复杂指令。 实际上 ARM 已经采用了这两种策略,而 x86 CPU 则采用了后者,所以这不能算是 RISC-V 的新招术。 然而,关键是 RISC-V 从这些策略中获得了更大的优势,这里面有两个重要原因:
- 它从一开始就添加了压缩指令。在 ARM 上使用的是 Thumb2 压缩指令格式,这就必须将其作为一个单独的 ISA 来添加以完成改造,这需要一个内部模式切换和单独的解码器来进行处理。RISC-V 压缩指令可以添加到具有 400 个额外逻辑门 (AND、OR、NOR、NAND 门) 的 CPU 上。
指令编码
这一部分需要进行一些解释。指令在 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,它就知道这条指令应该作为一条压缩指令来进行解释。 压缩指令——二到一
这表示,我们可以将两条 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 这样的指令从内存和寄存器中读取输入。 宏融合——一到二
但是,当我们将指令压缩与宏融合结合起来看时,才能发现真正的收益。你看,如果 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]
为什么不能在代码中直接编写此宏操作?
因为我们的 ISA 不包含对它的支持!记住,可用的比特数是有限的。为什么不把说明写长一点呢?不行,那样会消耗太多的内存,并且会更快填满宝贵的 CPU 缓存。
然而,如果我们在 CPU 内部制造这些半复杂的长指令,也没有什么可担心的。CPU 在任何时候所面对的指令最多也不过几百条。所以在每条指令上浪费个 128 比特不是什么大问题。每个人都有足够的硅。 因此,当解码器得到一条正常指令时,它通常会把它转换成一个或多个“微”操作。这些“微”操作是 CPU 实际要处理的指令。它们可以非常地“宽广”,包含很多额外的有用信息。称之为“微”似乎有些讽刺,因为它们其实很“广”。然而事实上“微”指的是它们做的任务数量有限。
指令的复杂性
宏融合将解码器的工作做了一点改变:不再是将一条指令转换成多个微操作,而是将多个操作转换成一个微操作。
因此,在现代 CPU 中发生的事情看起来相当奇怪:
- 首先,它通过压缩将两条指令合并为一条指令。
- 然后借助解压把它分成两部分。
- 通过宏融合将它们合并到一个操作中。
- 不能太复杂,否则无法在为每条指令分配的数量固定的时钟周期内完成。
- 不能太简单,因为那纯粹就是浪费 CPU 资源。执行两个微操作的时间是执行一个微操作的时间的两倍。
好 处
好了,以上解释了很多细节,也许你很难一下子弄清楚重点是什么。为什么要进行压缩和融合?听起来有很多额外的工作要做。
首先,指令压缩和 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 更少。
RISC-V 设计策略
RISC-V 利用了我们当今对现代 CPU 的了解,并用这些知识指导了他们在设计时的选择。例如,我们知道:
- 如今,CPU 内核会提前做分支预测。它们的预测正确率超过 90%。
- CPU 内核是超标量体系结构的,这意味着它们在并行执行多条指令。
- 使用无序执行做到超标量体系结构。
- 它们是流水线式的。
业界有什么说法?
好吧,从理论上这听起来可能很好,但在现实世界中也果真如此吗?科技公司对此有什么看法?他们是否认为 RISC-V ISA 比商业 ISA(如 ARM) 提供了实实在在的好处? RISC-V 甚至不在备选的采购清单上,但是随着 Esperanto 的工程师们对它越来越多的研究,他们渐渐意识到它不仅仅是一个玩具或者是一个教学工具。“我们认为 RISC-V(相对于 Arm 或 MIPS 或 SPARC) 可能会损失 30% 到 40% 的编译效率,因为它太简单了。”Ditzel 说。“但我们的编译器专业人员对它进行了基准测试,难以置信的是只有 1% 左右。” Esperanto Technologies 现在只是一家小公司。像英伟达这样拥有大量经验丰富的芯片设计师和资源的大公司呢?英伟达在他们的板卡上使用了一种叫做“猎鹰”的通用处理器。在评估备选方案时,RISC-V 名列前茅。
免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!