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 个局部变量,结果编译器全压栈了,性能惨不忍睹。嗯,这就是不懂寄存器的代价。