第2章:基础指令集精讲——数据处理、逻辑运算与移位指令

各位好,欢迎来到第二章。这一章我们直接切入ARM64最核心的指令集——数据处理、逻辑运算和移位操作。说实话,这些指令看起来简单,但我在性能调优项目中见过太多因为基础指令用得不对而导致的性能灾难。咱们今天就把它们彻底讲透。

2.1 数据处理指令:ADD、SUB、MUL、DIV

数据处理指令是CPU的“算盘”。ARM64的数据处理指令遵循一个基本原则:操作数来自寄存器,结果写回寄存器。这一点和x86那种可以操作内存的CISC风格完全不同。

2.1.1 ADD(加法)与SUB(减法)

先看最基本的加法指令:

ADD X0, X1, X2      // X0 = X1 + X2
ADD X0, X1, #42     // X0 = X1 + 42
ADD W0, W1, W2      // 32位版本:W0 = W1 + W2

减法指令格式完全对称:

SUB X0, X1, X2      // X0 = X1 - X2
SUB X0, X1, #10     // X0 = X1 - 10
个人经验:我刚开始做ARM64移植时,总习惯用ADD/SUB配合立即数。后来发现,如果立即数超过12位,编译器会拆成多条指令。这时候用MOVZ+MOVK组合加载常量,反而更快。

ARM64的立即数编码有个限制:只能编码12位立即数,并支持左移0、12、24、36、48位。举个例子:

ADD X0, X1, #0x1000     // 合法:12位立即数
ADD X0, X1, #0x1001     // 合法:12位立即数
ADD X0, X1, #0x10000    // 非法!需要拆成两条指令

为什么会这样?因为指令编码空间有限。ARM64的设计哲学是:让常用指令更紧凑,不常用的场景用多条指令组合。

2.1.2 MUL(乘法)

乘法指令比ADD复杂一些。ARM64提供了多种乘法变体:

MUL X0, X1, X2      // X0 = X1 * X2(64位乘法,取低64位)
SMULL X0, W1, W2    // 有符号32位乘法,结果扩展为64位
UMULL X0, W1, W2    // 无符号32位乘法,结果扩展为64位
注意:MUL指令只保留乘积的低64位。如果两个64位数相乘,结果可能溢出。我曾经在写加密算法时踩过这个坑——两个大数相乘,高64位被静默丢弃,调试了一整天才发现。

乘加指令(Multiply-Accumulate)在信号处理中非常有用:

MADD X0, X1, X2, X3    // X0 = X1 * X2 + X3
MSUB X0, X1, X2, X3    // X0 = X1 * X2 - X3

你看,一条MADD指令替代了MUL+ADD两条指令,既节省了代码空间,又减少了指令发射压力。

2.1.3 DIV(除法)

除法是ARM64里最慢的算术指令之一。SDIV和UDIV分别处理有符号和无符号除法:

SDIV X0, X1, X2      // 有符号除法:X0 = X1 / X2
UDIV X0, X1, X2      // 无符号除法:X0 = X1 / X2

除法指令的延迟通常在4-20个周期之间,具体取决于操作数大小和实现。相比之下,ADD只需要1个周期。

指令 延迟(周期) 吞吐量 备注
ADD/SUB 1 4/周期 最快
MUL 3-4 1-2/周期 比ADD慢,但可接受
SDIV/UDIV 4-20 1/10-20周期 最慢,尽量避免
性能建议:能用乘法替代除法时,尽量用乘法。比如除以2可以用右移1位替代,除以常数可以用乘以倒数再移位的方式优化。我在做图像处理管线时,把除法全部替换成乘法和移位,性能提升了30%以上。

2.2 逻辑运算指令:AND、ORR、EOR、BIC

逻辑运算指令在位操作、掩码生成、标志位控制中无处不在。ARM64的逻辑指令设计得非常优雅——它们都支持立即数和移位操作数。

2.2.1 AND(按位与)

AND X0, X1, X2      // X0 = X1 & X2
AND X0, X1, #0xFF   // X0 = X1 & 0xFF(取低8位)

AND最常见的用途是位掩码提取。比如从一个状态寄存器中提取第3-5位:

AND X0, X1, #0x38   // 提取位3-5(二进制:111000)

2.2.2 ORR(按位或)

ORR X0, X1, X2      // X0 = X1 | X2
ORR X0, X1, #0x80   // X0 = X1 | 0x80(设置第7位)

ORR用于设置特定位。我在做设备驱动时,经常用ORR来设置控制寄存器的某个位,而不影响其他位。

2.2.3 EOR(按位异或)

EOR X0, X1, X2      // X0 = X1 ^ X2
EOR X0, X1, #0xFF   // X0 = X1 ^ 0xFF(取反低8位)

EOR有个经典用法:寄存器清零。EOR X0, X0, X0比MOV X0, #0更高效,因为不需要加载立即数。

避坑指南:我曾经在写CRC校验算法时,用EOR做数据混淆。结果发现EOR和ADD的延迟不同,导致流水线气泡。后来重新排布了指令顺序,吞吐量才上去。

2.2.4 BIC(位清除)

BIC是ARM64的特色指令,它先对第二个操作数取反,再与第一个操作数做AND:

BIC X0, X1, X2      // X0 = X1 & ~X2
BIC X0, X1, #0xF    // X0 = X1 & ~0xF(清除低4位)

BIC比AND+MVN(取反)的组合更紧凑。你想想看,如果每次清除位都要先取反再AND,那得多浪费指令缓存。

2.3 移位指令:LSL、LSR、ASR、ROR

移位指令是ARM64的“瑞士军刀”。它们不仅能做移位,还能作为其他指令的操作数修饰符——这是ARM架构的精髓之一。

2.3.1 LSL(逻辑左移)

LSL X0, X1, #4      // X0 = X1 << 4(左移4位,低位补0)

左移1位相当于乘以2。编译器经常用LSL替代乘法指令:

// 计算 X1 * 8
LSL X0, X1, #3      // 比 MUL X0, X1, #8 更快

2.3.2 LSR(逻辑右移)

LSR X0, X1, #3      // X0 = X1 >> 3(右移3位,高位补0)

LSR用于无符号数的除以2的幂。注意:LSR总是补0,不管原数是正还是负。

2.3.3 ASR(算术右移)

ASR X0, X1, #3      // X0 = X1 >> 3(右移3位,高位补符号位)

ASR和LSR的区别在于高位填充方式。ASR保留符号位,用于有符号数的除以2的幂。举个例子:

// X1 = -16(0xFFFFFFFFFFFFFFF0)
ASR X0, X1, #2      // X0 = -4(0xFFFFFFFFFFFFFFFC)
LSR X0, X1, #2      // X0 = 0x3FFFFFFFFFFFFFFC(大正数!)
注意:ASR用于有符号除法时,对于负数会向负无穷取整,而不是向零取整。比如 -3 >> 1 的结果是 -2,而不是 -1。我在做数值计算库时,因为这个细节导致结果偏差,排查了很久。

2.3.4 ROR(循环右移)

ROR X0, X1, #4      // X0 = X1 循环右移4位

ROR把移出的低位补到高位。它在密码学、位反转、循环缓冲区中很常用。我记得在实现AES加密算法时,ROR指令让字节置换操作变得异常简洁。

2.4 指令组合的艺术:移位操作数

ARM64允许在大多数算术和逻辑指令中嵌入移位操作。这是ARM架构的杀手锏:

ADD X0, X1, X2, LSL #3    // X0 = X1 + (X2 << 3)
AND X0, X1, X2, LSR #2    // X0 = X1 & (X2 >> 2)
SUB X0, X1, X2, ASR #1    // X0 = X1 - (X2 >> 1)

这种写法不需要额外的移位指令,一条指令完成“移位+运算”两个操作。指令数减少一半,性能自然提升。

核心技巧:在循环中计算数组索引时,用LSL操作数替代乘法。比如访问 int64_t 数组的第i个元素:
LDR X0, [X1, X2, LSL #3] // X1是基址,X2是索引,LSL #3相当于乘以8

下面这张图展示了ARM64数据处理指令的完整分类和关系:

ARM64 基础指令集分类与关系 数据处理指令 逻辑运算指令 移位指令 ADD SUB MUL DIV AND ORR EOR BIC LSL LSR ASR ROR 移位操作数组合:ADD X0, X1, X2, LSL #3 延迟:ADD(1周期) < MUL(3-4) < DIV(4-20) 尽量用移位替代乘除法 BIC = AND + NOT,一条指令搞定 EOR X0, X0, X0 是最快的清零方式

这张图把本章的知识点串起来了。你看,三大类指令各有特点,但通过移位操作数这个“胶水”,它们可以灵活组合。理解了这个关系,你写出来的代码会更紧凑、更高效。

好了,这一章的内容就到这里。数据处理、逻辑运算、移位指令是ARM64的基石,掌握好它们,后续的寻址模式、内存操作、分支控制才能游刃有余。记住:指令越简单,组合越灵活,性能越容易优化。