把同一个函数分别编译到 x86-64 和 RISC-V,对比着看,ISA 的所有核心分歧一次全露馅。函数朴素到不能再朴素:
int add3(int a, int b, int c) { return a + b + c; }
x86-64 的输出(gcc -O2,反汇编标注了每条指令的字节):
8d 04 37 lea eax,[rdi+rsi] ; 3 字节
01 d0 add eax,edx ; 2 字节
c3 ret ; 1 字节
RISC-V 的输出:
00b50533 add a0,a0,a1 ; 4 字节
00c50533 add a0,a0,a2 ; 4 字节
00008067 ret ; 4 字节
三件事直接可读:x86 指令长短不一(1 到 15 字节都可能),RISC-V 条条 4 字节;x86 用了两条完全不同的指令(一条带内存计算,一条纯加法),RISC-V 两条长得一模一样;x86 代码 6 字节,RISC-V 12 字节,前者省一半空间。变长还是定长、指令强还是弱,这就是 ISA 合同的两大条款。第 1 章把 ISA 叫「软硬件的分界线」,本章展开它的具体条文。
每条指令至少包含两块信息:干什么(操作码),对谁干(操作数在哪)。按操作数个数分类其实是段历史:
add a, b, c,含义 a = b + c):最直观,RISC-V 的 add 就是。add a, b,含义 a = a + b):结果覆盖一个源,x86 的主流写法,所以上面 x86 代码里 eax 反复出现。指令长度是两种哲学的直接体现。定长(32 位)解码器闭着眼睛都知道下一条指令在哪,第 6 章流水线取指会感谢这个决定;变长省内存,但解码器得逐字节判断「这条指令到哪结束」,x86 的解码器是业界公认的复杂度黑洞,Intel 干脆在芯片里把变长指令预先切成定长的微操作再执行,等于在里面藏了个 RISC 内核。
同样一条「加」,操作数可以来自很多地方。以「把某个数加进 r1」为例:
| 寻址方式 | 写法示例 | 操作数实际在哪 | 对应的 C |
|---|---|---|---|
| 立即 | addi r1, r1, 5 | 就在指令里 | x + 5 |
| 寄存器 | add r1, r1, r2 | 寄存器 | 最普通的变量运算 |
| 寄存器间接 | ld r1, 0(r2) | r2 指向的内存 | *p |
| 变址 | ld r1, 8(r2) | r2 + 8 指向的内存 | p->field、a[i] |
| 基址变址 | ld r1, (r2,r3) | r2 + r3 指向的内存 | 二维数组元素 |
| PC 相对 | beq r1, r0, +16 | 相对当前指令偏移 | if/循环跳转 |
这张表值得盯一分钟:C 的每个语法构造几乎一比一对应一种寻址。指针是寄存器间接,结构体字段是「基址加固定偏移」的变址,数组 a[i] 是「基址加变量偏移」。你以后看任何汇编,先认寻址方式,基本就知道这行汇编对应源码里的哪种东西。
不同 ISA 的寻址能力差别很大:x86 有十几种(还包括「基址 + 变址 × 比例」这种一步算出 a[i*8] 的豪华写法),RISC-V 砍到只剩四五种。RISC 的态度是:寻址方式越多,单条指令越强,但解码越复杂、流水线越难做;少而规整,编译器多干点活就是了。
RISC-V 有 32 个通用寄存器,x0 到 x31,其中 x0 永远是 0:写它等于扔进黑洞,读它恒得 0。第一眼觉得浪费,一个寄存器的位置就为了存个 0?实际上这个设计相当值钱:
add x5, x0, x0 就把 x5 清零(不过真要清零,编译器更常用 mv x5, x0,同一件事的别名)。beq x1, x0 就是「x1 等于 0 就跳」,不用专门的「与零比较」指令。ld r1, 0(r2),把 r2 换成 x0 就变成直接寻址 ld r1, 100(x0):一种寻址机制,两种用法。ARM 的 xzr 是同一个思路。这类小设计透露 RISC 的整体口味:不堆指令,用少量机制组合出大量效果。
教科书喜欢把这两派写成正面对决,列一张长长的对比表。我更愿意这样总结:分歧的本质是「复杂度放硬件还是放编译器」,而这场战争在现代 CPU 里已经以「内部合流」的方式结束了。
RISC 阵营(ARM、RISC-V):指令少而规整、定长、只有 load/store 能碰内存(所以叫 load-store 架构),复杂操作交给编译器拼装。换来的是解码简单、流水线好做、功耗低,移动端和嵌入式全面获胜。
CISC 阵营(x86):一条指令能干一串事(上面 lea eax,[rdi+rsi] 一条指令做了取寄存器、相加、送回三件事),省指令数省内存。代价是解码复杂。
而现实是:现代 x86 处理器在前端把每条复杂指令翻译成类 RISC 的微操作(µop),后端执行的其实是 RISC;现代 ARM/RISC-V 为了省内存带宽,也搞出了压缩指令扩展(RISC-V 的 C 扩展把常用指令压到 16 位)。两边在架构层面互相抄作业,真正的性能分野发生在微架构(第 5、6 章的流水线、分支预测、Cache 设计)。「CISC 还是 RISC」作为选型问题基本失去了意义,Apple 用 ARM(精简指令)做 M 系列打桌面,Intel 用 x86 守服务器,赢的都不是指令集本身。
| 维度 | x86-64 | ARM (AArch64) | RISC-V |
|---|---|---|---|
| 指令长度 | 1~15 字节变长 | 定长 32 位(另有 16 位压缩) | 定长 32 位(可扩展 16 位) |
| 通用寄存器 | 16 | 31 + 零寄存器 | 32 + 零寄存器 |
| 访存 | 大量指令可直接访存 | 仅 load/store | 仅 load/store |
| 授权模式 | 封闭 | 授权收费 | 开源免费 |
| 主战场 | 桌面、服务器 | 手机、(如今)笔记本 | 嵌入式、新兴市场 |
一个值得记住的观察:RISC-V 基础整数指令集只有四十来条指令,一张 A4 纸印得下。对照 x86 上千条指令、两千多页手册,你就明白为什么学界和新兴公司都爱 RISC-V:教得完,学得完,造得完。
lea eax,[rdi+rsi] 一条指令干了三件事。把它拆成三条最朴素的 RISC 风格指令(加载、加、存),再想想为什么编译器宁可用 lea。gcc -O2 -S 生成 x86 汇编和 riscv64-linux-gnu-gcc -O2 -S(没有交叉编译器就找在线 Compiler Explorer,编译完对比指令条数和字节数。