主页
FPGA培训
关于平台
企业孵化
峰会大赛
展厅预约
FPGA知识库
联系我们
登录 注册
首页 > 技术文章 > RISC-V架构,精简孕育无限可能
RISC-V架构,精简孕育无限可能
来源: 2021-09-28 10:51

原作者:Erik-Engheim

编译:半导体行业观察(ID:icbank)

校对:芯来科技



自上世纪九十年代后期RISC(精简指令集)和CISC(复杂指令集)大战爆发以来,人们就开始搅浑水,宣称到底用RISC还是CISC都不那么重要。还有人说指令集其实无关紧要。

事实上指令集很重要,它限制了优化微处理器的操作。



我最近一直在学习有关RISC-V ISA(指令集架构)的知识,以下几点是它让我印象深刻的方面:


  • 该指令集很小且易于学习(只有47条基本指令),对于想学习微处理器的人非常友好;

  • 是大学中用于数字设计教学的主流架构;

  • 设计精妙,CPU制造商可用其开发高性能微处理器。

  • 无需架构授权费,硬件实现简单便捷,原则上讲一位专业的CPU爱好者可以在合理的时间内就能完成自己的设计。

  • 开放的指令集,易于修改和使用。



RISC-V王者归来


随着对RISC-V了解的加深,我认识到RISC-V其实是一个彻底的转变,它让我们回到了那个好多人认为已经过去了的计算时代。就设计风格而言,RISC-V确实是回到了上世纪八九十年代的经典RISC时代。

在随后的几年中,许多人指出RISC和CISC的区别已经不再明显。因为像ARM这样的RISC CPU添加了很多复杂指令,以至于现在看来它比纯RISC CPU更像是一种混合指令CPU。人们对于其他RISC CPU(例如PowerPC)也有类似的看法。

211.png

相比之下,RISC-V则是RISC CPU中的最典型的代表。如果您阅读过网上有关RISC-V的讨论就一定听到过这样的论调:RISC-V是由一些拒绝与时俱进的老派RISC激进分子设计的。

前ARM工程师Erin Shepherd几年前对RISC-V发表了一条有趣的评论:RISC-V指令集架构错误的过于追求极简主义。它过分强调了最小化指令数量,规范化编码等。这种极简主义导致错误的正交性(例如将相同的指令重复用于分支、调用和返回),并且需要过多条的指令,这不论是在指令大小还是指令数量上来说都会影响代码密度。

简而言之,虽然保持较小的代码对性能有利(这样可以更轻松地将正在运行的代码保持在高速CPU缓存中),但这导致了如果想要完成一个现实的项目将需要更多条指令,而这样会占用更多的内存空间。根据以往经验来看,RISC处理器应添加一些复杂指令来向CISC方向发展。也就是通过增加专用指令来代替多个通用指令。

这里的批评是针对RISC-V设计者过于关注使用极小的精简的指令集。这毕竟是最初设计RISC-V的目标之一。


CISC和RISC的历史包袱



众所周知,CPU设计中的复杂指令让性能背上了沉重的历史包袱。而后诞生的两项设计创新也使得使用复杂指令的变得多余,这两项创新就是:


指令压缩:指令在内存中进行压缩,并在CPU的第一阶段进行解压缩。

宏融合:将CPU读取的两个或更多的简单指令融合为一个复杂指令。

RISC-V设计中融合了这两个设计理念,但它们并非RISC-V的新招,实际上ARM也采用了这两条策略,而x86 CPU则是采用了后者。

但关键是RISC-V从这些策略中获得了更大的优势,其原因有二:

  • RISC-V从设计之初就应用了指令压缩。ARM上使用的Thumb 2压缩指令是通过将其添加为单独的指令集来实现的,这需要一个内部模式开关和单独的解码器来处理。但RISC-V的指令压缩可以为CPU添加最少400个额外逻辑门(AND、OR、NOR、NAND门);

  • RISC-V在竭力控制专有指令数量上得到了回报:为指令压缩赢得了更多的空间。

后面这部分需要一些阐述:RISC架构的指令通常有32-bits位宽,用于编码不同的信息。假设有一条这样的指令(哈西标记注释):

212.png

它将在x4和x8的寄存器上增加内容,并将结果存储到x1。我们编译这条指令需要多少位宽取决于我们有多少数量的寄存器。RISC-V和ARM64有32个寄存器。32可以用5-bits表示:

213.png

由于必须指定3个不同的寄存器,因此总共需要15比特(3×5)来编码操作数(输入加法运算)。

因此如果我们希望指令集能支持更多的操作,那么我们将消耗更多的位宽。当然,我们可以使用64位指令,但这将消耗过多的内存,从而降低性能。
通过积极降低指令数量,RISC-V留出了更多空间以增加位宽,这就表明了正在使用指令压缩。当CPU看到指令中的某些位被设置,就知道应该将其理解为指令压缩。

压缩指令:合二为一

指令压缩指的是一个32位字节可以同时容纳两条16比特位宽的指令。并非所有的RISC-V指令都是16比特位宽。因此,根据使用效用和频率选出了一个32位指令的子集。未压缩的指令可以使用3个操作数(输入),而压缩的指令只能使用2个操作数。因此,一个压缩的ADD指令将如下所示:

214.png

RISC-V汇编使用C.作为前缀来指示汇编器应将指令转换为压缩指令,但是实际上并不需要编写这行代码。当情况适用时,RISC-V汇编程序将自动选择压缩指令而不是未压缩指令。

压缩的指令减少了操作数的数量:三个寄存器操作数将消耗15个位宽,而只剩下1个位宽来指定操作!通过使用两个操作数,我们省下了6个位宽来指定操作码(执行操作)。实际上,在x86汇编中,如果没有为3个寄存器操作数保留足够的位宽时,x86汇编也会这样操作。而不同的是,x86会消耗位宽来允许指令(比如,ADD)从存储器和寄存器中读取输入。

宏融合:拆一为二

宏融合指的是,如果CPU得到一条包含两个压缩的16位指令的32位字节,它能将其融合成一条复杂指令。

这听起来很荒谬,我们这不是又回到了原点么?回到了我们不想使用的CISC CPU了吗?

非也非也,因为我们并没采用x86和ARM的策略,在指令集中增加专用的复杂指令。相反,我们正是通过组合使用简单指令来代替使用大量的复杂指令。
在正常情况下,宏融合存在这样一个问题:尽管两条指令可以被一条指令代替,但它们仍然消耗两倍的内存空间。但是通过压缩指令,我们没有消耗更多空间。这不就两全其美了么?

Erin Shepherd在发表她对RISC-V ISA的批判时,举了一个简单的C函数的例子。为了清楚起见,我重写了一下:

215.png

在x86上,它被编译为:

  216.png

用编程语言调用函数时,通常会根据所使用的指令集将参数传递给寄存器中的函数。在x86上,第一个参数放置在rdi寄存器,第二个参数放置在中rsi寄存器中。按照惯例,返回值必须放在eax寄存器中。

第一条指令将rsi中的内容乘以4,它包含了i变量。为什么要相乘?因为array都是由整数元素组成,它们之间的间隔为4个字节。因此,array中的的第三个元素实际上处于字节偏移量3×4 = 12。

之后,我们将其添加到rdi寄存器中,因其包含了array的基本地址,它也为我们提供了array中i元素的最终地址。我们在这个地址中读存储单元的内容,并将其存储在eax。如此这般,任务就完成了。

在ARM上,操作也非常相似:

  217.png

在这我们不是乘以4,而是将r1寄存器向左移动2位,这等同于与乘以4。这可能也是x86代码中的更典型的表达方式:在x86上,只能乘以2、4或8,所有这些都可以通过左移1、2或3位来实行。
这样一来,根据我对x86描述,你几乎可以猜中后面。

现在让我们使用RISC-V,有意思的来了!(哈希起始注释)

218.png

在RISC-V寄存器上,a0,a1仅是x10和x11的别名,用以放置函数中的第一个和第二个参数。RET是伪指令(简写):

219.png

JALR跳转到ra(返回地址),ra是x1的别名。

无论如何,这都看起来很糟糕吧?基于表中查找来进行编译并返回结果,这比简单通用的操作指令难上两倍。这也就是Erin Shepherd狠狠批评RISC-V设计团队的原因。她写道:“RISC-V的简化以执行更多指令为代价而使得解码器(即CPU前端)变得简单。但是,当轻微(或高度)不规则指令的解码已有了较好接受度的时候(比如x86的指令前缀过多,导致代码长度很长),缩放流水线的宽度就成了一个难题。”

然而有了指令压缩和宏融合,我们可以解决这个问题。

230.png

这样它就与例子中的ARM使用的内存空间同样大了。
接下来让我们做一些宏融合!
RISC-V规则之一是只要目标寄存器是相同的,就允许进行融合操作。ADD和LW(加载字)指令就符合这种情况。因此,CPU将这些指令转换为一条指令。如果SLLI目标寄存器也相同,我们也可以将全部三个指令融合为一个。因此,CPU会看到类似于更复杂的ARM指令的内容:

2231.png

为什么我们直接在代码中编写这种复杂的宏操作?
因为我们的ISA不支持啊!切记可用位宽是有限的。为什么不延长指令呢?因为那会消耗太多内存,并更快地占用宝贵的CPU缓存。
但是,我们在CPU内部制造一些长的半复杂指令,就没关系了。因为在任何时候,CPU在任何事都都不会浮动超过几百条指令。因此,在每个指令上浪费128位并不可惜。
当解码器获得一条正常指令时,通常会将其转换为一个或多个微操作。这些微操作是CPU实际在处理的指令,他们很宽且包含许多额外的有用信息。也正因为它们很宽,所以将它们称为“微型”就很讽了。其实“微型”是指它们执行的任务数量有限。


哥尔迪定理锁定指令的复杂性


宏融合使解码器的工作变得微不足道:我们并没有将一条指令变成多个微操作,而是采取各种方法将多个操作变成一个微操作。

由此看来,现代CPU中发生的事情相当奇怪:

1.首先,它通过压缩将两条指令组合为一条。

2.然后通过解压将其分拆。

3.再通过宏融合将它们组合回一个操作中。

然而其他的指令反而不会被融合,而是最终被分成多个微操作。为什么有些指令会被融合而另一些会被分拆呢?系统不会因此崩溃么?

关键在于最终要进行复杂程度适宜的微操作:

  • 不能太复杂,否则它无法为每条指令分配固定数量的时钟周期。

  • 也不能不太简单,不然就会浪费CPU资源。执行两次微操作所需的时间是执行一次微操作所需时间的两倍。

这一切始于CISC处理器:英特尔为让其指令像RISC一样轻松地适应其流水,便开始将其复杂的CISC指令拆分为微操作。但在后来的设计中,他们意识到许多CISC指令是如此简单,以至于它们能很容易地融合为一条中等复杂的指令。指令越少,完成地越快。

为什么要进行这些压缩和融合?这听起来增加了很多额外的工作。

首先,指令压缩与zip压缩完全不同。“压缩”一词有点用词不当,因为立即解压缩一条压缩指令并不会消耗时间。对于RISC-V来说,拥有近400个逻辑门,解压缩实在是小菜一碟。宏操作融合也是如此。

尽管这看起来很复杂,但是这些方法已经用于现代微处理器中。而我们已经为此付出了一些代价。


RISC-V的设计之光


RISC-V 指令集在设计之初就考虑到了压缩指令和宏操作融合;而ARM,MIPS和x86在设计原始处理器架构时对此一无所知。当x86和ARM在设计64位版本时,他们意识到了这一点,却并没采用。就公司来讲,他们在设计新的架构时,不会过于偏离之前的版本。他们通常会删掉过去明显错误,而不是彻底改变架构。

RISC-V设计人员在对第一个最小指令集进行各种测试后,获得了两个重大发现:

  • 就内存空间而言,RISC-V比其他任何CPU结构占用的都少,包括x86架构(考虑到它是CISC架构,本身就可以节省空间);

  • 与其他架构相比,RISC-V需要执行的微操作更少。

总体来讲,RISC-V通过设计具有融合功能的基本指令集,从而使CPU在执行任何给定程序时进行的微操作都比其他架构要少。这坚定了RISC-V设计团队将宏操作融合作为其核心策略的决心。在RISC-V手册中就可以看到很多有关进行融合操作的注释。对融合指令进行修订,成为了一中常见模式。

对于CPU体系结构的学生来讲,一个小的指令集更易于学习,用RISC-V指令集构建一个CPU将更容易。

RISC-V指令集中必须要执行的核心指令是最少的,其他指令都可作为扩展指令,压缩指令也只是一个扩展选项。如果只设计一个简单的CPU,拓展指令都可以不用。

宏融合也只是一种优化,它不会改变整体架构。在某些特定的RISC-V处理器中可能都用不到它。

而对于ARM和x86来说,很多复杂指令并不是可选项:即使想创建最小最简单CPU内核,也必须用上整个指令集中所有复杂的指令。

RISC-V吸收了我们对现代CPU的了解,并在其设计中体现出来。例如:

  • CPU内核具有先进的分支预测器,预测正确率超过90%;

  • CPU内核是超标量的,可并行执行多条指令;

  • 用乱序执行来实现超标量;

  • 流水线作业。

这意味着一些条件执行(诸如ARM支持的)就可有可无了,RISC-V可以省下为支持ARM指令格式而消耗的占位。条件执行的最初目的是避免分支,因为分支对流水不利。为了使CPU快速运行,通常会预取下一条指令,以便在上一条指令完成其第一阶段后立即选择下一条指令。
但是对于条件分支,当开始运行流水时,我们并不知道下一条指令在哪,但是超标量CPU可以轻松的地并行执行两个分支。这也是RISV-C没有状态寄存器的原因,它在指令之间创建了依赖关系。每条指令越独立,与另一条指令进行并行运行就越容易。

总的来说,RISC-V的设计理念是:运用最精简的指令集创建一个最简单的CPU,而又不丧失应用其做出高性能CPU的可能性。



声明:文章来源于硅农亚历山大,本文内容及配图的版权归版权所有人所有,内容仅代表作者个人观点,不代表本网站观点或证实其内容的真实性。对于本网刊载的各类评论非本网评论员评论,仅代表评论者个人观点,并不代表本网证实或赞成其描述。如其他媒体、网站或个人转载使用,需保留本网注明的“稿件来源”,并自负法律责任。本文转载仅为更好的传播行业信息,若有内容图片侵权或者其他问题,请及时通过邮件联系我们,以便做侵删处理。