[RISC-V指令集架构的设计] 为什么要发明一个新的指令集?

最近看了篇UC Berkeley的RISC-V指令集设计的技术报告,主要是设计者们阐述设计思路,说明为何要如此设计,相当于RISC-V标准的补充文档了。里面第二章名为“为什么要发明一个新的指令集”,实则为“对所有其他指令集的吐槽”,跟说相声似的乐的不行。特此把此章节选翻译了一下。翻译水平不高,因为很多词汇对应的中文拿捏不准,见谅 。

英文文献地址

本文允许转载,但必须附上原址 https://www.jianshu.com/p/a543b092f312

本章中,作者解释了为何RISC-V项目要另起炉灶,而不是基于已有指令集进行修改。作者对已有的几套指令集进行了分析,解释了其为何不适合作为RISC-V项目的基础。节选翻译片段如下:

  1. MIPS
  2. SPARC
  3. Alpha
  4. ARMv7
  5. ARMv8
  6. OpenRISC
  7. 80x86

每段开头会有一段对指令集历史的介绍,和维基差不多,翻译中省去。想看黑成碳的直接跳到x86和ARM段即可

MIPS

…尽管学术用途上简单的MIPS-I微架构很好设计,但是这套指令集有一些技术缺陷,导致在高性能实现上差强人意:

  • MIPS设计之初特别针对了一种微架构进行了过度优化:五级,单发射,顺序流水线。为此,分支指令和跳转指令故意设计了一个延迟,使得超标量和更长流水线的处理器设计更加复杂。在没有合适的指令用来填充延迟槽的情况下,延迟分支指令浪费了编码密度和指令发射带宽。而即使对于他针对的五级流水线微架构,抛弃延迟槽而改用小的分支目标缓存也能带来更好的总性能与单位面积性能。
    其他的流水线hazard,包括load,乘法,除法带来的data hazard,在MIPS-I中本来是暴露给上层的,但是之后的更新中他们被移除了,因为事实上隐藏这些hazard对软件实现难度和性能都有好处。但是分支指令的延迟槽,因为向后兼容性的原因,就没法移除了。
  • MIPS对位置无关代码(PIC)和动态链接支持很差。直接跳转指令是基于伪绝对地址的,所以位置无关代码中无法使用直接跳转指令,而只能使用间接跳转,导致了严重的代码体积和性能上的问题。(2014版MIPS改善了基于PC的间接寻址,但是基于PC的函数调用仍然需要超过一条指令)
  • 16位立即数消耗了太多的编码空间,只有很小的余地用来扩展指令集——在2014版中只有1/64的空余编码空间。当MIPS试图加入压缩指令集来减小代码体积时,他们只能创造一套新的指令集,让处理器在两套指令集间切换,因为原有的指令集剩余编码空间不足以添加压缩指令集了。
  • 乘法除法用了特殊的寄存器,导致上下文空间,指令数量,代码体积和实现难度增大。
  • MIPS设计之初假定浮点运算应当放在一块独立的协处理器上,所以对于集成在一块芯片上的设计欠佳。比如说,浮点转整形需要把结果写进浮点的register file里,所以还需要一条额外的指令取用结果。更加尴尬的是,在浮点和整形的register file之间move还有一个暴露给上层的延迟槽。
  • 在ABI中,两个整数寄存器被预留给内核态软件使用,用户软件便少了两个寄存器。但是,真正的内核不怎么使用这两个寄存器,因为没有机制保护这两个寄存器不被用户态代码修改。
  • 用特殊的指令处理非对齐的load和store,浪费了编码空间,加大了主流微架构的复杂度,仅仅为了简化极简处理器的设计。
  • 设计者没有加入比较后分支 (compare-and-branch) 指令,是由于对指令延迟方面的考虑。然而时至今日,硬件实现的分支预测如此普及,缺少这条指令已经不合时宜了。

除了技术因素以外,MIPS是私有指令集,在很多用途上并不适合。MIPS对非对齐load和store指令申请了专利,导致其他人无法完整实现此指令集。曾有一个案例,一个公司已经在其实现中移除了非对齐load和store,但仍遭到起诉,因为MIPS声称在内核中模拟这些指令仍然属于侵权。尽管现在这些专利已经过期,但MIPS商标仍属于Imagination;MIPS兼容的处理器必须由他们认证。

SPARC

…尽管如此,一些指令集设计上的决定导致其相比MIPS-I缺乏吸引力。

  • 为了加快函数调用,SPARC使用了一个大型的,滑窗式的register file。在过程调用处,滑窗滑动,对被调用方展示出一组全新的寄存器。这个设计使得被调用方无需保存寄存器和恢复现场,减小了代码体积,改善了性能。如果过程调用栈使用的寄存器超出了滑窗的最大范围,性能就会大幅受损:操作系统不得不经常被唤醒以处理滑窗正溢出和负溢出。这大幅增加了架构状态数,加大了运行时上下文切换开销。由于清理滑窗需要调用操作系统,使得纯粹的用户态线程难以实现。
    寄存器滑窗带来了显著的面积和功耗开销。对超标量处理器来说,降低寄存器滑窗的开销尤其困难。为了避免给所有寄存器都加入大量的连线,UltraSPARC-III维护了一份当前寄存器滑窗的浅拷贝。每次滑窗滑动,浅拷贝就必须被更新一次,导致大部分函数调用和返回都会导致流水线中断。Fujitsu的乱序执行处理器也使用了可以与之媲美的、“史诗级”难度的设计,把寄存器滑窗逻辑全部塞进了寄存器重命名单元里。
  • 分支指令使用条件码,增加了架构状态数,同时引入指令间关联导致设计更复杂。带有寄存器重命名的乱序执行的微架构需要对条件码单独进行重命名,否则会遇到频繁的瓶颈。SPARC也缺乏比较后分支 (compare-and-branch) 指令,导致常见代码的指令数膨胀。
  • 对于简单的实现来说,同时load和store两个相邻寄存器的指令很有吸引力,因为这些指令几乎没有什么硬件复杂度,白白提升了处理器的吞吐。但是对于复杂的带有寄存器重命名的处理器,这极大的增加了复杂度,因为重命名过后相邻的寄存器早就不相邻了。
  • 在浮点和整形的register file之间move需要经过内存,限制了混合浮点整形的代码的性能。
  • (这条涉及浮点的不精确异常,编者并不熟悉,略过)
  • 原子指令只有一个:fetch后store。这并不足以实现很多无等待的数据结构。

SPARC和其他1980年代的RISC指令集一样,都有很多短视的设计。其目标是实现一个单发射、顺序、五级流水线的处理器,所以指令集的设计包含了这种假定。SPARC加入了分支指令的延迟槽,以及各种短视的、暴露给上层的hazard,对软件编译和高性能实现都没有好处。另外,它也缺乏位置无关寻址能力。最后,SPARC无法添加压缩指令集,因为其没有预留足够的空闲编码空间。

和其他商业RISC不同,SPARC V8是一个开放标准,这一点归功于Sun。SPARC International对V8和V9延续了开放的许可政策,只需$99管理费。开放的指令集下涌现出了很多免费的设计,其中两个是Sun自己的Niagara微架构的衍生。尽管如此,SPARC的后续——Oracle SPARC Architecture——是私有的,而且高性能软件大概率会追随Oracle的脚步,逐渐抛弃老旧的开放SPARC。

Alpha

Digital Equipment Corporation (DEC) 的架构师们在1990年代设计Alpha时有很多前人经验可供学习。他们去除了很多早期RISC指令集中最不受欢迎的特性,包括分支指令延迟槽、条件码、寄存器滑窗,最终设计出了一个干净、简单、支持高性能的64位指令集。除此以外,Alpha的架构师们小心的将特权指令和硬件平台的细节隔离在了一个抽象接口之下——Privileiged Architecture Library (PALcode)

尽管如此,DEC仍然针对顺序执行微架构做了过度优化,加入了一些对现代处理器并不友好的特性:

  • 为了追求高频,Alpha最初版本刻意回避了8bit和16bit的load和store,事实上构建了一个基于32位字的内存系统。为了弥补一些特定程序因此损失的性能,他们加入了特殊的非对齐load和store指令,还有一些整数指令来加速重对齐过程。最终,架构师们认识到了他们的错误之处——应用性能仍然受损,而且有些设备的驱动无法编写了——加入了小于WORD长度的load和store指令。但是他们已经背上了对齐指令的包袱,这些指令现在现在毫无作用了。
  • 为了充分利用浮点指令的乱序完成特性,Alpha的浮点采用了不精确捕获模型。单独来看,这个设计也许可以接受,但是Alpha同时规定了如果需要的话,软件必须提供异常标志和默认值。这个组合对IEEE-754标准下的程序是灾难性的:大部分浮点运算指令前后都必须插入捕获屏障指令(或者如果遵循一长串晦涩的编译限制的话,可以减少成每个代码块插入一次)
  • Alpha没有整数除法指令,反而推荐软件层使用牛顿迭代法求解。这个决定只节省下了些许的芯片面积,但极度增大了部分程序的指令数。这产生了令人震惊的结果——在Alpha处理器上,浮点除法远远快于整数除法。
  • 和其他RISC一样,Alpha没有考虑过添加压缩指令集,所以也没有预留足够的编码空间。
  • Alpha包含了条件move指令,增加了包含寄存器重命名的微架构的复杂度:如果move条件为假,那么这条指令仍然需要把老值拷贝到新的物理寄存器上。这导致条件move成了唯一一条需要三个源寄存器的指令。
    事实上,DEC第一版乱序执行实现中使用了一些技巧以避免这条指令带来的额外的datapath。Alpha 21264把条件move拆分为两条微指令,第一条计算条件,第二条进行move。这个方案需要让物理register file增宽一个比特来保存中间结果。

Alpha的例子也突出了使用商业ISA的风险:他们可能会死亡。90年代末,Compaq收购了衰颓的DEC公司,不久之后,他们就决定暂停Alpha的开发,换用Intel的安腾架构。Compaq将Alpha的产权卖给了Intel。最后一款Alpha处理器在2004年由惠普(在收购Compaq之后)发布。

ARMv7

…精简ARMv7或者扩展它都是被明令禁止的;即使在微架构上进行创新也需要购买ARM所谓的架构许可。

即使知识产权上不存在障碍,ARMv7也存在一些技术缺陷,严重阻碍了我们使用其设计新指令集:

  • 在设计的时候,没有加入64位的支持,另外硬件上还缺少对IEEE754-2008标准的支持(ARMv8修正了这些问题,下一节将进行讨论)
  • 特权指令的细节渗透进了用户级指令中。这不仅仅是一个美学问题。ARMv7不是“可经典虚拟化”的,有很多原因,其中之一就是异常后返回指令RFE,其在用户模式中不会触发捕获。ARM在最近的版本中加入了hypervisor特权模式,但截至本文写成之时,ARMv7仍不可能实现经典虚拟化,除非使用动态二进制翻译。
  • ARMv7包含一个定长16bit的压缩指令集,名为Thumb。Thumb代码尺寸非常出色,但性能很低,尤其是对浮点密集型代码。随后ARM添加了一个变长指令集,Thumb-2,性能大大提升。不幸的是,由于Thumb-2是在ARMv7设计完成后才被考虑的,32bit的Thumb-2和32bit的ARMv7编码并不相同(同样的,16bit的Thumb-2也和16bit的Thumb编码不同)。事实上,指令译码器需要同时理解三组指令集,增大能耗、延迟和设计上的开销。
  • ARMv7包含很多增加设计复杂度的特性。它不是一个真正的通用寄存器架构:PC是通用寄存器之一,导致几乎每一条指令都有可能打乱控制流。更糟糕的是,PC的最低位代表着当前运行的指令集(ARM或Thumb)——一条简单的ADD指令就有可能切换了处理器指令模式!对分支指令使用条件码、引入predication,导致高性能实现更加复杂。

ARMv7既庞大又复杂。ARM和Thumb单单整数指令就超过600条。其整数SIMD和浮点扩展指令,NEON,又增加了数百条。即使法律上允许我们实现ARMv7,技术上我们也会有重大挑战。

ARMv8

…新的架构移除了ARMv7中一些增加复杂度的特性:比如,PC现在不再是一个通用寄存器;predication被移除;LDM和SDM被移除;指令编码更加规律。但是很多累赘仍然留了下来,包括条件码,以及一些不怎么那么通用的寄存器(链接寄存器是隐式的,x31在不同的上下文中要么是栈指针要么是0)。另外,更多的瑕疵被引入了,包括一个巨大的、事实上强制性的、subword长度的SIMD架构。总体上,这个指令集既复杂又笨重:一共有1070条指令,53种编码格式,8种寻址模式,需要5778页文档描述。既然如此,一堆重要特性的缺席未免让人感到吃惊:比如,仍然缺少比较后分支指令。

像大多数架构一样,ARMv8继续混杂用户指令和特权指令,时不时暴露底层实现。有一个让人无法理喻的例子——一个混杂了复杂语义、未定义行为、虚伪通用寄存器的特殊性质的例子——load-pair指令可能会给用户空间返回一个非精确异常。

如果指令编码标记了前变址寻址或者后变址寻址,并且(t==n || t2==n) && n != 31,那么以下三种行为之一可能会出现

  • 此指令未定义
  • 此指令被视为NOP
  • 此指令依照标注的寻址模式进行load,基址寄存器被设为UNKNOWN。另外,如果此指令过程中出现异常,基址寄存器数据可能会被损坏,此指令无法再被重复。

另外,随着ARMv8的推出,ARM停止了压缩指令编码的支持。Thumb指令集并未被带入64位中。从定长指令集的标准来看,ARMv8确实很紧凑,但是我们将会在第五章中提到,其无法与变长指令集比拼代码尺寸。不出所料,ARM第一款64位处理器的指令缓存相较于32位处理器增大了50%。

最后,ARMv8仍是一个封闭标准。其无法被精简,导致其实现嵌入式处理器和专用加速器时太笨重。事实上,使用这个指令集的情况下,根本无法设计一个强耦合的协处理器,因为除了ARM以外任何人都不能扩展它。即使架构师想在微架构上进行创新也需要一个昂贵的许可证,限制了可以设计ARMv8的人数。

OpenRISC

…就像DLX那样,其有一些技术缺陷限制了其应用前景:

  • OpenRISC项目主要是为了设计一个开源处理器,而不是一个开放指令集。指令集和实现耦合得十分紧密。
  • 定长32位编码,加上16位立即数,断绝了压缩指令集的可能性。
  • 没有对于2008版的IEEE-754标准的硬件支持
  • 条件码,在分支指令和条件move指令中出现,提升了高性能实现的复杂性
  • 指令集对于位置无关寻址支持较差
  • OpenRISC不是“可经典虚拟化的”,因为异常后返回指令RFE在用户模式中会正常工作,而不是被捕获。

我们2010年调研OpenRISC时,这套指令集还有两个额外的缺陷:强制的分支指令延迟槽,无64位支持。但必须称赞他们的是,这两个问题都被纠正了。延迟槽变为了可选,64位版本发布了(尽管据我们所知,还没有实现过)。最终,我们决定从头开始设计指令集,而不是基于OpenRISC修改。

x86

(编者按:重头戏来了)

…其如此流行的原因有很多:IBM PC中x86的无处不在;Intel对于二进制兼容的精益求精;他们微架构设计的激进前卫;以及他们生产工艺的遥遥领先。

但是指令集设计的品质可能不在其中。

1994年,AMD的80x86架构师,Mike Johnson,说出了那句名言:“x86真的没有那么的复杂——它只是毫无道理而已。”在那个时候,这句玩笑对x86的历史包袱轻描淡写。但是在过去二十年内,这句话显得如此的不靠谱:2015年的x86极度的复杂。现在有1300条指令,繁杂的寻址模式,大量的专用寄存器,众多的译址方式。毫不意外地,在AMD K5微架构率先试验成功后,所有的Intel乱序处理器都跟着采用了将x86动态翻译成类RISC指令的方案。

如果x86最终实现了更加高效的处理器的话,那么它的复杂性其实上可以接受。但是时至今日Johnson名言的下半句还是对的。是个人都会怀疑这个设计当初进行了多少考量

  • 这个指令集不是可经典虚拟化的,因为一些特权指令在用户模式里只会静静的失败,而不是被捕获。VMware的工程师用复杂的动态二进制翻译解决了这个问题,一举成名。
  • 这个指令集允许任意整数字节长度的指令,最长15字节,但是稀缺的短指令码却被随意挥霍。比如,在IA-32,Intel的32位版本80x86中,一共256个8bit指令码,有6个被用来进行十进制数的操作——这些操作如此冷门,以至于GNU编译器根本就不输出这些指令。(尽管这个暴行在x86-64中被移除了,但是其他各种浪费8bit指令码的操作仍然存在,比如一个检查可能的(如今已不使用的)x87浮点单元浮点异常的指令。)
  • 这个指令集的寄存器数量非常精简。32位的IA-32,只有8个寄存器.寄存器溢出如此常见,以至于为了避免流水线中断和数据拥堵,Intel微架构里特意缓存了栈顶几个字的值。
    针对这个问题,AMD的64位x86-64,将整数寄存器数量翻倍到16。即便这样,很多程序——尤其是那些运用了编译器优化,如循环展开,软件流水——仍然面临寄存器压力。
  • 雪上加霜的是,大部分整数寄存器在指令集里都有特殊用途。比如,整数除法数隐式的来自于DX和AX。算数位移的位数来自于CX,但同时又是字符串操作时使用的寄存器。……总体来说,这种设计模式会导致非常低效的寄存器与栈的数据交换。
  • 更糟的是,大部分x86指令只有覆盖原操作数的破坏性形式。这经常导致很多额外的用来保存数据的move。
  • 很多指令集特性,包括隐式的条件码和predicated move,在激进的架构中实现起来非常的繁琐。可是,这些额外的复杂度通常不带来更高的性能,因为这些指令的语义并没有被正确实现。比如,x86提供了条件load指令,但是如果此时无条件load会触发一个异常的话,那么条件load是否产生一个异常,则视具体实现而异。所以,编译器几乎不会使用这条指令来进行优化。
    为了解决条件代码的问题,Intel在微架构中花了大力气将比较指令和分支指令在内部合并为比较后分支指令。

这些指令集设计上的决定对于静态代码尺寸有着深远的影响。我们将在第五章看到,本来可以实现非常高密度的编码,x86却根本没做到:IA-32只比定长32bit ARMv7密了一点点,x86-64比ARMv8甚至还稀疏了不少。

尽管有这些问题,x86编码程序时一般需要更少的动态指令数量,因为x86指令能编码多个基础操作。比如,C语句x[2]+=3在MIPS中编译成三条指令,但是IA-32只需要一条。在动态指令密度上的优势有以下好处:比如,它可以减少指令fetch单元的能耗。但是它也加大了实现的难度,无论什么级别的实现。在这个例子中,一般的流水线会产生两个structural hazard,因为这条指令执行了两次访存和两次加法。

最后,80x86是一套私有指令集。勇于实现x86微处理器去和Intel竞争的架构师大概率会面临法律上的挫折:历史上,Intel非常喜欢打官司,哪怕是自己被告反垄断的官司。

你可能感兴趣的:([RISC-V指令集架构的设计] 为什么要发明一个新的指令集?)