ARM 指令的设计研究
Table of Contents
在复习嵌入式系统设计时,对 ARM 指令集的设计有一些自己的思考。
初接触 ARM 指令时,发现一些指令有着特别的设计。
感觉违背了我对 RISC 精简的设计认知。
但再深入思考一些就发现,这些设计都有其用意。
一:步长为 2 —— 立即数编码
在 32 位的定长指令中,留给数据的空间只有12 位。那如何可以最大化地利用空间来表示数字呢?
ARM 给出的答案是循环右移:通过把 8 位数据(0-255)循环右移偶数位(0, 2, 4, …, 30),实现 32 位数的全覆盖。
但难免让人产生疑问:为什么只右移偶数位?
为什么是 2?
这 12 位空间被 ARM 拆解为:
- 前 8 位:作为基础数值(0-255)。
- 后 4 位:作为移位控制(
rotate_imm)。
4 位二进制最多只能表示 16 种状态(0-15)。而我们的目标是让这 8 位数据能在 32 位的寄存器中遍历(覆盖 0-31 位)。
这是一道简单的数学题:
- 如果步长是 1:只能覆盖一半的寄存器空间。
- 如果步长是 2:完美覆盖所有位域。
为什么不分给 rotate 5 位?
那新的疑问随之而来:为什么不直接用 5 位来表示 rotate 呢?这样就能真正地直接覆盖 0-31 位,而不是像现在这样只能覆盖 0-31 中的偶数位域。
这就是一个设计上的取舍了,如果移位用 5 位,那么剩下的数据位就只剩 7 位:
- 7 位只能存储 0 ~ 127。
- 而 8 位存储 0 ~ 255,刚好是一个字节。
即 ARM 工程师认为,“保证 8 位数据完整性” 比 “能移动到奇数位置” 更重要,这是一种规范性层面的设计哲学。
二:RSB —— 反向减法指令
RSB 指令的基本语法如下:
RSB{S}{cond} Rd, Rn, Operand2
Rd:目标寄存器,存储结果。Rn:第一个操作数寄存器。Operand2:第二个操作数,可以是立即数或寄存器。
公式实际上等同于 Rd = Operand2 - Rn。那不免让我疑问:提出反向减法的意义何在,在正向减法中调换操作数指针不可以吗?
解决减法局限性(核心)
这是设计师提出反向减法的初衷。
在 ARM 指令集中,规定了立即数只能作为第二个操作数使用,无法直接作为第一个操作数参与减法运算。这样做虽然确保了运转的稳定,但也带来了局限:如果偶尔需要常数减变量呢?
场景: 计算 100 - R0。
-
尝试用 SUB:
SUB R1, #100, R0Error!,因为#100是立即数,不能放在第一个位置。
-
尝试用 SUB(换位置):
SUB R1, R0, #100- 算出来是
R0 - 100,不是我们要的结果。
- 算出来是
改用 RSB: 允许我们把寄存器放在第一个位置(Rn),把立即数放在第二个位置(Op2),但是执行反向减法。
- 代码:
RSB R1, R0, #100 - 含义:
R1 = #100 - R0 - 结果: 解决了常数减变量的问题。
快速求负数(取反)
除了解决刚需问题,RSB 的提出也实现了一些操作的简化:
如果想对 R0 取反(比如把 5 变成 -5)。
- 如果是 SUB:
- 先找个寄存器存 0。
SUB R1, R2(存了0), R0。- 要么总是保有一个零寄存器;要么总是需要临时做一个零寄存器,这是一个非常笨重的思路。
- 用 RSB 可以一行代码搞定:
RSB R0, R0, #0- 含义:
R0 = 0 - R0 - 标准的数学取负操作。
- 含义:
乘法优化(进阶)
RSB 配合移位指令后还可以实现一些乘法指令的优化。
场景:计算 R0 = R1 * 7。
- 常规思路是使用
MUL指令,但乘法指令只是写起来简单,在片上的实际实现是非常繁琐的。 - 用 RSB 优化:
RSB R0, R1, R1, LSL #3 - 解析:
R1, LSL #3相当于R1 * 8RSB实际上计算R1 * 8 - R1,等同于R1 * 7
这条指令实现了一个周期计算 R1 * 7,比传统的乘法指令更高效。
三:BIC —— 位清除指令
BIC 在硬件底层做了两步操作:
-
取反:先把 Op2(掩码)里的每一位都黑白颠倒。
-
相与:再拿着这个颠倒后的数,和 Rn 进行 AND 运算。
理解 BIC
假设 R0 是 8 个灯的状态,Op2 是一个灭灯清单。
-
R0 (当前状态):
1 1 1 1 0 0 1 1(灯亮为 1,灭为 0) -
Op2 (灭灯清单):
0 0 0 0 1 1 1 1(希望把最后 4 位灭掉)
BIC 执行过程
-
取反 Op2:
1 1 1 1 0 0 0 0 -
AND 运算:
1111 0011 (R0 原来的值) & 1111 0000 (取反后的 Op2) ----------- 1111 0000 (结果)
为什么需要 BIC?
理解了 BIC 的实际运用后反而更疑惑了:为什么要多余用这个指令来完成,直接用 AND R0, R0, #0xF0 (1111 0000) 不就好了?
经过查询,我选择了两个比较让我信服的解释:
-
符合编码直觉:
- 使用 AND 清零: 需要在掩码里把想保留的写成 1,把想清除的写成 0。
- 使用 BIC 清零: 直接把想清除的写成 1,想保留的写成 0。更符合“清除”的直觉。
-
合法立即数的瓶颈: 现在我需要把 R0 的 第 0 位清零(其他位保持不变)。
- 使用 AND:掩码需要是
1111 1110,但这个数无法通过 ARM 的立即数编码规则表示出来。 - 使用 BIC:掩码是
0000 0001,合法的立即数。
- 使用 AND:掩码需要是
当然,这两个理由其实都是充分不必要的,我认为最根本的原因有且只有一点:BIC 并没有浪费硬件资源:
-
ARM 的 ALU(算术逻辑单元)前面本来就挂着一个桶形移位器,这个移位器不仅能做移位,其内部还很容易实现“取反”功能。
AND令走的是:A & BBIC令走的是:A & ~B
在电路设计上,这只是在 B 的输入端加了一排反相器。
换而言之,设计师对“为什么需要 BIC”的两个原因的解决几乎是零成本的。