3. 寄存器体系对比:ARM vs RISC-V

好,咱们今天聊聊寄存器体系。说实话,寄存器是处理器的“命根子”。你想想看,CPU 所有计算都得靠寄存器来暂存数据。ARM 和 RISC-V 在这块儿的设计思路,差别还挺大的。

我个人习惯,看一个架构先看它的寄存器。因为寄存器数量、用途、调用约定,直接决定了编译器怎么写、操作系统怎么做上下文切换。我在项目中遇到过好几次,因为寄存器不够用导致性能瓶颈,最后不得不改代码。嗯,这事儿后面细说。

3.1 ARM 寄存器组

ARM 的寄存器,分通用寄存器和状态寄存器两大类。咱们先看通用寄存器。

3.1.1 通用寄存器(R0-R15)

ARM 有 16 个通用寄存器,编号 R0 到 R15。但注意,它们不是完全“通用”的。有些有特殊用途:

  • R0-R3:参数传递和返回值。函数调用时,前 4 个参数放这里。
  • R4-R11:普通通用寄存器,函数内随便用。但调用者要自己保存。
  • R12:也叫 IP(Intra-Procedure-call Scratch Register),临时用一下。
  • R13:栈指针 SP。这个不能乱动,否则程序就崩了。
  • R14:链接寄存器 LR。存返回地址。
  • R15:程序计数器 PC。指向当前指令地址。

这里有个坑。R13 和 R15 虽然名义上是“通用寄存器”,但你真拿它们当普通寄存器用,编译器会骂人的。我曾经在移植代码时,不小心把 SP 当临时变量用了,结果栈指针飞了,调试了一整天。所以,记住:通用寄存器 ≠ 随便用

3.1.2 状态寄存器(CPSR/SPSR)

ARM 还有一个特殊的寄存器——当前程序状态寄存器(CPSR)。它存的是啥?标志位:

  • N(Negative):结果为负
  • Z(Zero):结果为零
  • C(Carry):进位/借位
  • V(Overflow):溢出

还有模式位、中断使能位等。异常发生时,CPSR 会复制到 SPSR(Saved Program Status Register),等异常返回时再恢复。

说实话,ARM 的状态寄存器设计挺巧妙的。一条指令就能同时改标志位和通用寄存器。但这也带来一个问题:状态寄存器是隐式修改的。你写一条加法指令,它自动改 N、Z、C、V。有时候你根本不想改,但它还是改了。这会导致流水线中的依赖问题。

关键点:ARM 的状态寄存器是“隐式副作用”的典型代表。编译器优化时得小心处理。

3.2 RISC-V 寄存器组

RISC-V 就清爽多了。它把寄存器分成了 32 个通用寄存器,编号 x0 到 x31。注意,x0 是硬编码的 0,你写啥它都是 0。

3.2.1 x0 的特殊性

x0 是 RISC-V 的一个“神来之笔”。它永远返回 0。你往 x0 写数据,数据就丢了。这有啥用?

  • 实现 NOP 指令:addi x0, x0, 0 就是啥也不干。
  • 清零操作:addi x1, x0, 0 把 x1 清零。
  • 比较操作:beq x1, x0, label 判断 x1 是否为 0。

我在项目中用 x0 实现过“条件移动”操作,省了一条分支指令。嗯,这个技巧挺实用的。

3.2.2 通用寄存器(x1-x31)

RISC-V 的寄存器命名遵循 ABI 约定。虽然硬件上都是通用的,但软件上分了角色:

寄存器 ABI 名称 用途
x0 zero 硬编码 0
x1 ra 返回地址
x2 sp 栈指针
x3 gp 全局指针
x4 tp 线程指针
x5-x7 t0-t2 临时寄存器
x8 s0/fp 保存寄存器/帧指针
x9 s1 保存寄存器
x10-x17 a0-a7 参数/返回值
x18-x27 s2-s11 保存寄存器
x28-x31 t3-t6 临时寄存器

你看,RISC-V 没有状态寄存器。标志位?没有。条件码?也没有。那它怎么做条件判断?靠比较指令和分支指令。比如 beq 直接比较两个寄存器,然后跳转。没有隐式的标志位修改。

我的建议:RISC-V 这种设计,让流水线实现更简单。没有状态寄存器的依赖,乱序执行也更容易。但代价是,某些操作需要多条指令才能完成。比如 ARM 一条 ADDS 就能同时做加法和改标志,RISC-V 得分开做。

3.3 调用约定差异

调用约定,说白了就是函数之间怎么传参数、怎么保存寄存器。ARM 和 RISC-V 的约定差别很大。

3.3.1 ARM 调用约定(AAPCS)

ARM 的调用约定叫 AAPCS(ARM Architecture Procedure Call Standard)。核心规则:

  • 参数传递:R0-R3 传前 4 个参数。多余的参数压栈。
  • 返回值:R0 存返回值。64 位返回值用 R0 和 R1。
  • 寄存器保存:R4-R11 是“被调用者保存”。函数里用了这些寄存器,得先保存到栈上,返回前恢复。
  • 栈对齐:栈必须 8 字节对齐。

这里有个细节。ARM 的 LR(R14)在函数调用时会被覆盖。所以嵌套调用时,你得把 LR 压栈。我曾经在写递归函数时忘了保存 LR,结果返回地址丢了,程序直接跑飞。嗯,血的教训。

3.3.2 RISC-V 调用约定

RISC-V 的调用约定更简洁:

  • 参数传递:a0-a7(x10-x17)传前 8 个参数。比 ARM 多一倍。
  • 返回值:a0 存返回值。64 位返回值用 a0 和 a1。
  • 寄存器保存:s0-s11(x8-x9, x18-x27)是“被调用者保存”。t0-t6(x5-x7, x28-x31)是“调用者保存”。
  • 栈对齐:栈必须 16 字节对齐。

你看,RISC-V 有 8 个参数寄存器,ARM 只有 4 个。这意味着,对于参数多的函数,RISC-V 可以减少栈操作。我在做嵌入式系统时,经常遇到函数有 5-6 个参数。ARM 得压栈 1-2 个,RISC-V 直接放寄存器里。性能差距就出来了。

注意:RISC-V 的 ra(x1)在函数调用时也会被覆盖。和 ARM 的 LR 一样,嵌套调用时要保存。但 RISC-V 没有专门的“压栈指令”,你得用 sw ra, 0(sp) 手动存。别搞忘了。

3.4 我的实战体会

说了这么多,我分享一个实际案例。去年我在做一个 RISC-V 的 RTOS 移植。任务切换时需要保存和恢复所有寄存器。

ARM 这边,我得保存 R0-R12、LR、SP、CPSR。一共 15 个寄存器。RISC-V 呢?x1-x31,一共 31 个寄存器。但注意,x0 不用保存,因为它永远是 0。

你想想看,31 个寄存器 vs 15 个寄存器。上下文切换的开销,RISC-V 明显更大。但反过来,RISC-V 的寄存器多,函数调用时参数传递更高效。这是个 trade-off。

我个人习惯,在做性能敏感的系统时,会尽量让函数参数不超过 4 个。这样 ARM 和 RISC-V 都能用寄存器传参。如果参数多,我会考虑用结构体指针。嗯,这也是个优化技巧。

3.5 总结

ARM 和 RISC-V 的寄存器体系,本质上是两种哲学:

  • ARM:寄存器少,但有状态寄存器。指令可以隐式改标志。适合代码密度要求高的场景。
  • RISC-V:寄存器多,没有状态寄存器。指令更简单,适合高性能实现。

调用约定上,RISC-V 的参数寄存器更多,但上下文切换开销更大。ARM 的参数寄存器少,但上下文切换快。没有绝对的好坏,看你的应用场景。

最后,记住一点:寄存器是处理器的核心资源。写代码时,多想想寄存器怎么分配。编译器虽然能帮你优化,但你自己心里得有数。我曾经见过一个同事,函数里用了 20 个局部变量,结果编译器全压栈了,性能惨不忍睹。嗯,这就是不懂寄存器的代价。