Why RISC-V ?
WHY RISC-V ?
读 Design of the RISC-V ISA 论文后的总结与思考
Introduction
软硬件接口,作为指令集架构 (ISA) 的重要组成部分,一直以来都是计算机系统中最重要的接口之一。然而相对于其他计算机系统中的接口,所有流行的 ISAs 都是商业且私有的 (Spring 2016)。在这篇论文中,作者 Andrew Shell Waterman 等人详细介绍了 RISC-V 体系结构。RISC-V 是一个免费且开放的 ISA。设计人员回顾三十几年来体系结构的发展,充分吸取经验教训,在原始的精简指令集计算机(RISC)体系结构上进行了构建和改进,从而构造为具有各种可选扩展的小型基础 ISA,并将其统一称之为 RISC-V。RISC-V 基本的 ISA 非常简单,因此很适合于研究和教育,但又足够完整,足以成为便宜的低功耗嵌入式设备的 ISA,而可选扩展对其的补充,使得 RISC-V 可用于通用和高性能计算。
在 RISC-V 还未问世之前,所有流行的 ISAs 都是专有标准。当然,这些国际标准化组织用知识产权来保护自己是无可厚非的,但保持标准的封闭会阻碍创新,并人为地抬高微处理器的成本。因此在 2010 年,Yunsup Lee 和 David Patterson 等人设计了一种完全自由开放的、基于 RISC 体系结构的指令集体系结构 RISC-V 。起初,RISC-V 是用于教学与研究,且在设计中主要参考了 SPARC 和 MIPS 这两个 ISA 。
为何当初 David Patterson 等人不怕麻烦,要动手设计一个全新的 ISA 呢?RISC-V 与其他的 ISA 相比,其优点到底在哪里呢?
Why Develop a New Instruction Set ?
这个问题是设计人员在设计 RISC-V 时被经常问到的。为什么要设计一个新的 ISA 呢?毕竟很多的商业 ISAs 非常受欢迎,充分利用好其中的一个都会减少很多“不必要”的劳动。对于设计人员来说,他们有两个主要的考量:
- 商业专利问题。所有流行的商业 ISA 都是专营的。其供应商销售 ISA 的实现——比如以 IP 核的形式等的利润非常可观。虽然他们并没有禁止使用 ISA 来进行学术活动,但的确,这行为阻止了科研人员对 ISA 的完全 RTL 实现与共享,也为成功的研究想法商业化设置了障碍。
- 过于复杂的 ISA 。经过若干年的发展,流行的商业 ISA 都变得非常复杂,难以全部在硬件上实现。当然,我们可以实现其子集,但是,由于没有完全对整个 ISA 进行实现,未修改的软件就不能在上面运行,也破坏了 ISA 的完整性。
即使如此,作者他们仍然仔细考虑了可能的 ISA 选项,但最终全部否决了它们 ┑( ̄Д  ̄)┍。鉴于篇幅原因和本人水平有限(仅熟悉 RISC-V 、MIPS 和 80x86 )因此先翻译整理一下这两者的劣势。
MIPS
MIPS 指令集架构是一个典型的RISC ISA 。MIPS 的设计思想受到了 IBM 801 微机的深刻影响。MIPS 采用了通用寄存器下的 load-store 架构,且内存只允许被从寄存器中读出/写入的指令访问,算术运算只允许在寄存器上完成。这些设计有效地减少了硬件、流水线等设计的复杂度。
在原先的设计中,MIPS 的用户级指令集只有 58 条,可以非常简单地设计出一个单发射、顺序的流水线。然而经过 30 年的发展,它已经进化成了一个非常庞大的 ISA ,有约 400 条指令。虽然 MIPS-I 的简单微架构可以被学术架构者轻松掌握,ISA 有几个技术缺陷,使其对高性能实现不太有吸引力:
- 对五阶段流水、单发射、顺序流水线的过度优化。例如,Branch 和 jump 指令被延时了一周期,导致了超标量和超流水线设计复杂化。delayed branch 指令增加了代码数量,当空出的延时槽无法适当填充时,还会浪费了指令发射带宽。况且,即使对于经典的五阶段流水线,删除延迟槽并添加一个小的branch target buffer (BTB) 通常会带来更好的绝对性能和单位面积性能。但考虑到保持向后兼容性,分支延迟槽已经不太可能被删除了。
- 对 PIC (position-independent code) 支持不足,因此动态链接也很不行。MIPS 的直接跳转指令是伪指令,且跳转偏移量不是相对于 PC 的,而是绝对值,因此在 PIC 中没有任何用处。在 PIC 中,MIPS 只能使用间接跳转指令,这有很大的开销——不论是代码数量还是性能损失 (The 2014 revision of MIPS has improved PC-relative addressing, but PC-relative function calls still generally take more than one instruction) 。
- 16-bit wide 的立即数消耗大量的编码地址空间,只留下一小部分操作码空间用于ISA扩展。当 MIPS 架构师试图通过压缩指令编码来减少代码数量时,他们别无选择,只能创建第二种指令编码,并启用模式开关,因为他们无法将新指令放到原始编码空间中。
- 乘法和除法运算使用了特殊的寄存器 lo 和 hi,这增加了上下文大小,代码数量和微体系结构的复杂度。
- **MIPS 预先假设浮点处理单元是一个与主处理器分离的协处理器。**在整型寄存器和浮点寄存器文件之间移动时,会有一个 software-exposed 的延迟槽,这会影响性能。
- 在标准 ABI 层,2 个整型寄存器被保留给内核程序 ($k0 和 $k1),减少了用户程序可使用的寄存器数量。
- 需要处理不对齐的 load/store 指令,消耗大量的操作码空间并且使实现复杂化。
- MIPS 架构缺失了整数的 compare&branch 指令。受限于时代发展,设计师对时钟速率和 CPI 进行折中处理,在分支预测的出现和向静态CMOS逻辑迁移的今天看来,就不是很合适。
抛开技术问题不提,MIPS 不适合用于许多场合,因为它是一个专有指令集。历史上,MIPS 的技术专利对非对齐的 load-store 指令已经阻止其他人完全实现 ISA 。虽然该专利已经过期,但没有他们的许可,对 MIPS 的兼容性可不能随便声称😓。
SPARC
Alpha
ARMv7
ARMv8
OpenRISC
80x86
Intel 的 8086 体系架构已经有四十多年的历史了,它是笔记本、台式机、服务器等领域最受欢迎的体系架构了。在嵌入式领域之外,几乎所有流行的软件都已经移植到 x86 ,或干脆为 x86 开发。相信这也是大家接触最多、学得最多的 ISA 了。80x86 的成功原因非常复杂:
- 在 IBM PC 诞生之初,这种架构的偶然发现的可用性
- Intel 执着地专注于二进制兼容性
- Intel 大胆的微架构实现
- Intel 的尖端制造技术
然而,ISA 的设计质量可不在其中之一🙂。在 1994 年,AMD 80x86 架构师 Mike Johnson 说过一句著名的话,“ x86 其实并没有那么复杂,它只是在很多地方不太讲道理”。现在看来,这句话看起来有点黑色幽默—— x86 已经变得既复杂又不讲道理了。x86 现有 (2015) 1300 条指令,无数的寻址模式,几十个专用寄存器,和多个地址转换方案。所以,不应该感到奇怪的是,AMD 的 K5 微体系结构中所有的 Intel 乱序执行引擎已经动态地将 x86 指令转换为一种更类似于 RISC 风格的内部格式。简单地来说,在 80x86 下又套了一层 RISC 指令。
再来看看它多么不讲道理:
-
ISA 不是典型的可虚拟化的。因为一些特权指令会在用户模式下 sliently fail ,而不是被 trapped 。VMware 的工程师们用复杂的动态二进制翻译软件解决了这个缺陷,这可是出了名的。
ISA 的指令长度是可变的,最长的指令有 15 个字节,然而数量较少的短操作码(可以明显降低代码规模的)已经被随意地使用了。例如,Intel 的 IA-32 作为 80x86 的 32 位化身,256 个 8 位操作码中有 6 个加速了二进制编码十进制数的操作——这些操作非常深奥,以至于 GNU 编译器甚至不发出这些指令😓。好在 x86-64 放弃了这个特别糟糕的东西,但对 8 位操作码空间的大量浪费仍然存在,包括检查已弃用的 x87 浮点单元中挂起的浮点异常的指令。
-
ISA 的寄存器过少。32-bit 架构的 IA-32 只有 8 个 整数寄存器 (eax ebx ecx edx esi edi ebp esp) 。这导致堆栈溢出非常常见。为了减少流水线占用和数据缓存流量,最近 Intel 微体系结构使用了一个特殊功能单元来管理栈指针的值,并缓存堆栈的前几个字。
在认识到这一缺陷后,AMD 的 64-bit x86-64 将整数寄存器的数量增加了一倍,达到 16 个。即便如此,许多程序——尤其是那些从循环展开和软件流水等编译器优化中受益的程序,仍然面临着寄存器数量不足的压力。
-
大多数寄存器在 ISA 中有特殊功能,这让寄存器数量不足的问题雪上加霜。例如,整数除法的两个源操作寄存器必须是 DX 和 AX 寄存器;移位运算中的移位量必须来自 CX 寄存器,当然 CX 寄存器还要用作字符串操作的循环变量;ESI 用作 load 寻址时的增量偏移,EDI 用作 store 寻址时的增量偏移。博主对这点深有体会,在本科学习微机原理写 x86 汇编语言时真是令人吐血。
-
更加糟糕的是,x86 的大多数指令是破坏性的——得出的结果往往会覆盖一个源寄存器。通常,为保存这一运算结果或者源操作数,往往需要加一个额外的移动指令。
-
有些 ISA 的特性使得设计复杂,并且,这些复杂的设计并不能带来多少性能的提高,因为它们的考虑不周,导致编译器不敢做 aggressive 的优化。例如,x86 提供了一个有条件的 load 指令,但如果无条件的 load 会出现异常,却由实现来决定是否有条件版本也会出现异常。
认识到条件操作的低效率后,Intel 最近在一定程度上将比较指令和分支指令融合到内部 compare & branch 操作中。
这些 ISA 设计对静态代码的大小有很大的影响,这使得原本非常密集的指令编码完全消失:IA-32 只比固定宽度的 32-bit ARMv7 编码稍微密集一些,而 x86-64 则比 ARMv8 少一些。
撇开这些缺陷不谈,x86 编码的程序通常比 RISC 架构使用更少的动态指令(指程序实际运行时执行的指令数),因为 x86 可以将多个原始操作编码在一起。例如,C 语言中的表达式 x[2] += 13
在 MIPS 中需要:
1 |
|
然而在 IA-32 中只需要 addl 13, 8(eax)
。这样的动态指令密度有其优点:减少了取指能量消耗。但增加了实现的复杂性。在本例中,常规流水线会出现两种结构冒险 (structural hazards),因为指令会执行两次内存访问和两次加法。
最后,80x86 是一种专用指令集,敢于尝试实现 x86 微处理器与 Intel 竞争的架构师们可能会面临法律障碍:Intel 一直以来都是好打官司的,即使他们自己也面临着反垄断危险。
总结
下图总结了之前提到的 ISA 中支持的特性。这些特性都是我们认为现代 ISA 应该重点关注的。所有 ISA 都至少缺少两个重要的技术特征。表现最好的 ARMv8 是一个私有标准😓。开放的 ISA —— SPARC 和 OpenRISC 缺少了太多重要的架构特性。除了有争议的 DEC Alpha 之外,所有 ISAs 都有可能极大增加实现复杂性,特别是对于高性能实现的复杂性的属性。