A Comparison between WebAssembly and RISC-V
WebAssembly and RISC-V are both new Instruction Set Architectures (ISAs) that evolved in the recent 10 years. Let’s do a comparison.

WebAssembly 与 RISC-V 都是近十年内新出现的指令集架构。摘录一段 WebAssembly 官方网站的介绍:

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications.
WebAssembly (简写为 Wasm)是一种栈虚拟机的二进制指令格式。Wasm 被设计为 C/C++/Rust 等高级语言的可移植编译目标,从而允许客户端和服务器应用程序部署在 Web 上。

RISC-V 的官方介绍

RISC-V is a free and open ISA enabling a new era of processor innovation through open standard collaboration. Born in academia and research, RISC-V ISA delivers a new level of free, extensible software and hardware freedom on architecture, paving the way for the next 50 years of computing design and innovation.
RISC-V 是一种自由、开放的指令集架构,通过开放标准协作使新的处理器创新成为可能。RISC-V 来自学术研究,提供了更加自由、可扩展的软硬件架构,为未来 50 年的计算技术设计与创新铺平道路。

这两种指令集架构看起来很不一样。WebAssembly 的目标平台主要是 JIT 编译器(以及浏览器);而 RISC-V 则专为硬件设计,其目标之一即是在 FPGA 和电路上的高效实现。WebAssembly 与 RISC-V 的关系似乎类似于早期的 Java 与 x86:一种字节码格式,和一个硬件指令集。它们有什么关系呢?

其实字节码格式与硬件指令集的分界线并不是非常清晰。例如在 x86 指令集的早期实现中,指令经过一层解码直接被分发到执行电路去执行。但近年的 Intel/AMD CPU 采用了不同的执行策略:每条 x86 指令首先被翻译为某种内部 RISC 指令,执行组件收到的指令其实已经是翻译后的 RISC 编码格式。这实际上相当于从 x86 到内部 RISC 指令集的某种“硬件实时编译”过程。同时,JavaWebAssembly 也有实验性的硬件实现,虽然它们还不能赶上 JIT 编译器在现代 CPU 上所能达到的效率。

我们已经可以看到 RISC-V 指令集被用于区块链的智能合约,而 WebAssembly 此前几乎是原生语言智能合约的唯一选择。

在这里我们对 WebAssembly 与 RISC-V 做一个比较。

WebAssembly RISC-V
开源
内存架构 Load/Store Load/Store
浮点支持 是 (定义于扩展)
SIMD 是 (定义于扩展)
代码/数据分离
指针宽度 32 32/64
最大数据宽度 64 32/64
静态类型
控制流约束
机器模型 栈机 寄存器机
内存布局 线性 分页
内存保护 RWX
多线程同步 CAS LL/SC
指令编码 变长 定长 (2 或 4 字节)
与环境交互 导入函数 系统调用
可执行映像格式 WebAssembly ELF

1. 代码/数据分离

包括 RISC-V 在内的大多数现代架构对代码和数据使用同一个地址空间,但是 WebAssembly 没有这样做。实际上,运行中的代码甚至没有办法读/写它自己。这个设计的原因可能是:

  • 简化 JIT 编译器的实现。如果代码可以自我修改,那么 JIT 编译器则需要有检测修改和重新生成目标代码的能力,而这需要相当复杂的实现机制。
  • WebAssembly 假设了一个功能完善的运行环境。运行环境会处理链接、重定位等准备工作,程序不需要关心怎样把它自己运行起来。
  • 安全。能够动态生成和修改的代码是个很危险的攻击点。

2. 静态类型与控制流约束

WebAssembly 具有很强的“结构性”。其标准要求所有函数调用、循环、跳转和值类型遵循特定的结构约束,例如向一个接受三个参数的函数传入两个参数、跳转到其他函数里的某个位置、在两个整数上执行浮点加操作等都会导致编译/验证失败。RISC-V 则不具有此类约束,指令的有效与否只取决于其自身的编码是否正确。

3. 机器模型

WebAssembly 是一种栈机指令集,而 RISC-V 是一种寄存器机指令集。

在 WebAssembly 中,每条指令语义上会从值栈上弹出它的操作数,然后将结果推入值栈。然而与 Java 等其他基于栈机的字节码格式不同的是,程序中任意指令处值栈的结构都可以被静态确定。这一设计有利于更好的编译优化。

在 RISC-V 中,每条指令的编码包含 0 - 3 个寄存器编号 rd, rs1, rs2 。其中 rs1rs2 是输入寄存器, rd 是输出寄存器。除内存访问、特权指令等特殊类型指令外,每条指令只从输入寄存器中读取数据,在输出寄存器中存放结果。

4. 内存管理

虽然 WebAssembly 与 RISC-V 都定义了一个无类型的、字节可寻址的内存,它们之间还是有一些细节上的区别。WebAssembly 的内存等同于一个大数组:有效地址从 0 开始,连续扩展到某个由程序定义初值、可增长的上限。而 RISC-V 则使用了虚拟内存,用页表将地址映射到物理内存。

内存布局

WebAssembly 的内存设计虽然简洁且易于实现,但存在一些问题:

  • 地址 0 是有效的,这会导致一些程序在解引用空指针时的行为与预期不同。
  • 无法建立不映射到任何物理地址的“无效”地址区间,因此无法实现多线程环境下的栈保护页 (guard page) 。

5. 同步机制

图灵完备的计算机器需要至少一条条件分支指令。同样,支持多线程同步的指令集架构需要至少一条“原子条件分支”指令。这类指令在 WebAssembly 下是 i{32,64}.atomic.rmw.cmpxchg,在 RISC-V 下是 LR/SC,分别对应 CAS 模型和 LL/SC 模型。

LL/SC 的语义比 CAS 更强。CAS 存在难以解决的 ABA 问题,但 LL/SC 不受影响。这也意味着在 CAS 架构上模拟 LL/SC 比反过来要困难很多。


Note: 我写过一个 RISC-V 解释器 FlatRv ,且正在参与一个 WebAssembly JIT 运行环境 Wasmer 的开发。如果你感兴趣可以点个 Star :)