RISC-V处理器的指令集如何巧妙设计,既简单又高性能。
自从1990年代后期爆发RISC和CISC战争以来,人们就宣称RISC和CISC不再重要。许多人会声称指令集是无关紧要的。
但是指令集很重要。他们限制了可以轻松添加到微处理器的优化类型。
我最近一直在学习有关RISC-V指令集体系结构(ISA)的更多信息,以下是使RISC-V ISA印象深刻的一些方面:
这是一个RISC指令集,它很小且易于学习(基础为47个)。对于任何对学习微处理器感兴趣的人都非常有利。 RISC-V备忘单。
它经过精心设计,可让CPU制造商使用RISC-V ISA创建高性能微处理器。
无需执照费,并且被设计为允许简单的硬件实现,那么专业的业余爱好者就可以在原则上在合理的时间内进行自己的RISC-V CPU设计。
易于修改和使用的开源设计:Berkely乱序(BOOM)RISC-V处理器。
当我开始更好地理解RISC-V时,我意识到RISC-V是从根本上转变为许多人认为过去的计算时代。在设计方面,RISC-V几乎就像将时光机带回80年代和90年代初期的经典精简指令集计算机(RISC)。
在随后的几年中,许多人指出RISC和CISC的区别不再重要,因为像ARM这样的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对保持唯一指令数量低的痴迷得到了回报。压缩的指令只剩下更多空间。
后一部分需要一些阐述。在RISC架构上,指令通常为32位宽。这些位需要用于编码不同的信息。例如。说您有一条这样的指令(哈希标记注释):
这将寄存器x4和x8的内容相加并将结果存储在x1中。我们需要对此进行编码的位数取决于我们拥有的寄存器数量。 RISC-V和ARM64具有32个寄存器。数字32可以用5位表示:
由于必须指定3个不同的寄存器,因此总共需要15位(3×5)来编码操作数(用于加法运算的输入)。
因此,我们希望在指令集中支持的内容越多,可用32位中消耗的位就越多。当然,我们可以使用64位指令,但这将消耗过多的内存,从而降低性能。
通过大幅度降低指令数量,RISC-V留出了更多空间来添加表示我们正在使用压缩指令的位。如果CPU看到指令中的某些位被设置,则知道应该将其解释为压缩指令。
这意味着,我们可以将两条16位宽的指令放入32位字中,而不必在32位字中插入一条指令。自然,并非所有的RISC-V指令都可以16位格式表示。因此,根据32位指令的效用和使用频率来选择它们的子集。未压缩的指令可以使用3个操作数(输入),而压缩的指令只能使用2个操作数。因此,一条压缩的ADD指令如下所示:
RISC-V汇编使用C.前缀表示汇编器应将一条指令转换为压缩指令。但是实际上您不需要编写此代码。如果适用,RISC-V汇编程序将能够选择未压缩指令而不是未压缩指令。
基本上压缩的指令减少了操作数的数量。三个寄存器操作数将消耗15位,而只剩下1位来指定操作!因此,通过使用两个操作数,我们剩下了6位来指定操作码(执行操作)。
实际上,这与x86汇编的工作方式非常接近,在x86汇编中,保留的位数不足以拥有3个寄存器操作数。取而代之的是x86会花费一些比特来允许例如ADD指令从存储器和寄存器中读取输入。
但是,当我们将指令压缩与宏操作融合相结合时,我们才能看到真正的收获。您会看到,如果CPU获得的32位字包含两个压缩的16位指令,它可以将它们融合为一条复杂的指令。
听起来像胡说八道,难道我们不是刚开始吗?我们不是要避免使用CISC样式的CPU吗?
不,因为我们避免使用很多复杂的指令,x86和ARM策略来填充ISA规范。相反,我们基本上是通过简单指令的各种组合间接地表达大量复杂指令。
在正常情况下,宏融合存在问题:虽然可以用一条指令替换两条指令,但是它们仍然消耗两倍的内存空间。但是通过指令压缩,我们不再消耗更多空间。我们两全其美。
让我们看一下Erin Shepherd的例子之一。在对RISC-V ISA的批评中,她展示了一个简单的C函数。为了清楚起见,我有一些自由可以重写:
当您以编程语言调用函数时,通常会根据已建立的约定将参数传递给寄存器中的函数,这取决于您使用的指令集。在x86上,第一个参数放在寄存器rdi中,第二个参数放在rsi中。按照惯例,返回值必须放在寄存器eax中。
第一条指令将rsi的内容乘以4。它包含我们的i变量。为什么要相乘?由于数组由整数元素组成,因此它们之间的间隔为4个字节。因此,数组中的第三个元素实际上处于字节偏移量3×4 = 12。
然后,我们将其添加到包含数组基地址的rdi中。这给了我们数组ith元素的最终地址。我们读取该地址处存储单元的内容,并将其存储在eax中:完成任务。
在这里,我们不是与4相乘,而是向左移寄存器r1 2位,这等效于与4相乘。这可能也是x86代码中发生情况的更真实的表示。我怀疑您是否可以乘以2的倍数以外的任何值,因为乘法是一个相当复杂的操作。换班既便宜又简单。
无论如何,您几乎可以从我的x86描述中猜测其余的内容。 现在让我们进入RISC-V,真正的乐趣开始了! (哈希开始评论) SLLI a1,a1,2#a1←a1<< 2添加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忽略结果 JALR将跳转到引用返回地址的ra中的地址。 ra是x1的别名。 无论如何,这看起来简直太可怕了吧? 这样简单而通用的操作的指令是在表中进行基于索引的查找并返回结果的两倍。 确实确实看起来很糟。 这就是为什么艾琳·谢泼德(Erin Shepherd)高度批评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(装入字)指令就是这种情况。因此,CPU将这些指令转换为一条指令。
如果SLLI也是如此,我们可以将所有三个指令融合为一个。因此,CPU会看到类似于更复杂的ARM指令的内容:
因为我们的ISA不包含对它的支持!请记住,可用位数有限。为什么不延长说明时间呢?不,那会消耗太多内存,并更快地填充宝贵的CPU缓存。
但是,如果相反,我们在CPU内部制造这些长的半复杂指令,则无需担心。在任何时候,CPU永远不会漂浮数百条指令。因此,在每个指令上浪费128位并不重要。每个人都有很多硅。
因此,当解码器获得正常指令时,通常会将其转换为一个或多个微操作。这些微操作是CPU实际处理的指令。这些可能真的很广泛,并且包含许多额外的有用信息。考虑到它们很宽,将它们称为“微型”可能看起来具有讽刺意味。但是,“微型”是指它们执行的任务数量有限。
宏操作融合使解码器的工作变得微不足道:我们没有将一条指令变成多个微操作,而是采取了多种操作并将它们变成一个微操作。
相反,其他指令最终可能会分成多个微操作,而不是被融合。为什么有些人会融合而另一些人会分裂呢?疯狂有制度吗?
关键是最终要进行适当程度的复杂性的微操作:
不太复杂,因为否则它无法在为每个指令分配的固定数量的时钟周期内完成。
不太简单,因为那样我们就在浪费CPU资源。执行两次微操作所需的时间是执行一次微操作所需时间的两倍。
这一切都始于CISC处理器。英特尔开始将其复杂的CISC指令拆分为微操作,因此它们可以像RISC指令那样更轻松地适应其流水线。但是,在后来的设计中,他们意识到许多CISC指令是如此简单,以至于它们很容易与一种中等复杂的指令融合在一起。如果执行的指令较少,则可以更快地完成。
好的,这有很多细节,也许很难弄清重点是什么。为什么要进行所有这些压缩和融合?这听起来像很多额外的工作。
首先,指令压缩与zip压缩完全不同。 “压缩”一词有点用词不当,因为立即解压缩已压缩的指令非常简单。这样做不会浪费时间。记住,对于RISC-V来说很简单。仅使用400个逻辑门,即可执行解压缩。
宏操作融合也是如此。尽管这看起来很复杂,但是这些方法已经在现代微处理器中使用。因此,已经支付了这种复杂性的税收或成本。
但是,与ARM,MIPS和x86设计人员不同,RISC-V设计人员在开始设计ISA时就知道指令压缩和宏操作融合。通过使用第一个最小指令集的各种测试,他们发现了两个重要发现:
RISC-V程序通常会比其他任何CPU体系结构占用或减少内存空间。包括x86,考虑到它是CISC ISA,它本来可以节省空间。
基本上,通过设计具有融合功能的基本指令集,他们能够融合足够多的指令,从而使任何给定程序的CPU执行的微操作都比竞争对手少。
这使得RISC-V团队将宏操作融合作为RISC-V的核心策略加倍。您可以在RISC-V手册中看到很多有关可以融合哪些操作的注释。您会看到已对指令进行了修订,以便更轻松地融合以常见模式显示的指令。
将ISA保持较小意味着学生更容易学习。这意味着对于学习CPU架构的学生来说,实际上更容易构建运行RISC-V指令的CPU。
RISC-V具有每个人都必须实现的小型核心指令集。但是,所有其他指令都作为扩展的一部分存在。压缩指令只是一个可选扩展。因此,对于简单设计,可以省略。
宏操作融合只是一种优化。它不会改变整体行为,因此不需要您在特定的RISC-V处理器中实现它。
相反,对于ARM和x86,很多复杂性不是可选的。即使您尝试创建最小的简单CPU内核,也必须实现整个指令集和所有复杂的指令。
RISC-V充分利用了我们对现代CPU的了解,从而使他们可以选择设计和ISA。例如,我们知道:
今天,CPU内核具有先进的分支预测器。他们的预测可以在90%的时间内纠正。
这意味着不再需要诸如ARM支持的条件执行之类的东西。在ARM上支持以指令格式占用位。 RISC-V可以保存这些位。
有条件执行的最初目的是避免分支,因为分支对管道不利。 为了使CPU快速运行,通常会预取下一条指令,以便在上一条指令完成其第一阶段后立即选择下一条指令。 但是通过条件分支,您不知道开始填充管道时下一条指令将在哪里。 但是,超标量CPU可以简单地并行执行两个分支。 这也是RISV-C没有状态寄存器的原因。 这在指令之间创建了依赖关系。 每条指令越独立,与另一条指令并行运行就越容易。 基本上,RISC-V策略是如何使ISA尽可能简单,以及使RISC-V CPU的最小实现尽可能简单,而又不做出无法制造高性能CPU的设计决策。