4. 立即数操作数:编码、范围与使用

说到立即数,我脑子里第一个蹦出来的印象就是「指令里的常数」。说白了,它就是直接写在指令里的数值,不需要去寄存器或者内存里取。听起来很简单对吧?但这里面的门道,比大多数人想象的要深得多。

我记得刚做编译器后端那会儿,总以为立即数就是个数字塞进指令里完事。直到有一次,我写的代码在特定输入下死活编译不过,查了半天才发现——立即数超范围了。嗯,从那以后,我再也不敢小看这个「小常数」了。

4.1 立即数的编码方式

CPU 怎么知道指令里哪个部分是立即数?这取决于指令集的编码格式。我拿最典型的 RISC-V 和 ARM 来举例,你一看就明白了。

在 RISC-V 的 I 型指令里,立即数被拆成几段,塞进指令的不同位置。比如 addi x1, x2, 100,这个 100 就是立即数。它的编码是这样的:

imm[11:0] | rs1 | funct3 | rd | opcode
12 位      | 5 位 | 3 位   | 5 位| 7 位

你看,12 位的立即数被放在最高位。但有意思的是,不同指令集对立即数的摆放位置完全不同。ARM 的立即数编码更「艺术」——它用 8 位数值加 4 位循环移位量,能表示的范围比单纯 12 位大得多。

核心要点:立即数的编码不是简单的「数值二进制展开」。指令集设计者为了在有限的指令宽度里塞进更多信息,用了各种技巧。理解这些技巧,你才能写出更高效的代码。

4.2 立即数范围限制

这是最容易踩坑的地方。我见过太多人,包括我自己,都在这上面翻过车。

拿 RISC-V 来说,I 型指令的立即数是 12 位有符号数,范围是 [-2048, 2047]。你写 addi x1, x2, 3000,编译器直接报错。为什么?因为 3000 超过了 12 位能表示的最大值 2047。

那怎么办?编译器会帮你拆成两条指令:

# 想要实现 x1 = x2 + 3000
lui x1, 1          # 加载高 20 位,x1 = 4096
addi x1, x1, -1096 # x1 = 4096 - 1096 = 3000
add x1, x2, x1     # x1 = x2 + 3000

你看,一条指令变成了三条。这就是为什么我总说「大常数很贵」。

指令集 立即数位数 范围 典型指令
RISC-V (I型) 12 位有符号 [-2048, 2047] addi, slti
ARM (数据处理) 8 位 + 4 位循环 0~0xFFFF (部分值) MOV, ADD
x86 (立即数) 8/16/32 位 取决于操作数大小 MOV, ADD, SUB
MIPS (I型) 16 位有符号 [-32768, 32767] addi, ori

避坑指南:我曾经在优化一个视频解码器时,发现大量 addi 指令的立即数都在 2048 附近。编译器生成了大量两条指令的序列,性能直接掉了 15%。后来我把常数表重新排了一下,让所有立即数都落在 [-2048, 2047] 范围内,性能就回来了。

4.3 立即数在指令中的使用

立即数不是只能做加法。它几乎出现在所有类型的指令里:

  • 算术指令:addi、subi(有些指令集没有)、slti
  • 逻辑指令:andi、ori、xori
  • 移位指令:slli、srli(移位量也是立即数)
  • 访存指令:lw、sw 里的偏移量
  • 分支指令:beq、bne 里的跳转偏移

每种指令对立即数的解释方式都不一样。比如访存指令里的偏移量,它要和基址寄存器相加得到内存地址。而分支指令里的立即数,是相对于当前 PC 的跳转偏移。

我个人习惯在写汇编时,先把立即数按用途分类。算术用的、访存用的、分支用的,它们的范围限制各不相同。混在一起想,容易出错。

小技巧:如果你在写性能敏感的代码,尽量把立即数控制在 [-256, 255] 范围内。很多指令集对这个范围的立即数有更短的编码,或者能合并到其他指令里。我在做嵌入式优化时,光靠这个技巧就省了 5% 的代码体积。

4.4 立即数的性能影响

你可能会想:「不就是个常数吗,能有多大影响?」

影响比你想象的大。我举个例子:

# 不好的写法:大量使用大立即数
addi x1, x0, 4096
addi x2, x0, 4097
addi x3, x0, 4098

# 好的写法:用寄存器保存基址,只做小偏移
addi x10, x0, 4096
addi x1, x10, 0
addi x2, x10, 1
addi x3, x10, 2

第一种写法,每个大立即数都要拆成两条指令。第二种写法,只拆一次,后面的都是单条指令。在循环里,这个差距会放大几十倍。

为什么会这样?因为指令缓存(I-cache)是有限的。指令越多,缓存压力越大。大立即数导致指令膨胀,缓存命中率下降,性能自然就差了。

立即数编码与指令生成流程 源代码中的常数 是否在指令立即数范围内? 是 单条指令直接编码 否 拆分为多条指令 生成最终指令序列 大常数 → 指令膨胀 → 性能下降

这张图展示了我平时做代码优化时的决策流程。每次看到代码里的常数,我都会先问自己:这个数能不能变小?能不能用寄存器存起来复用?

总结一下:立即数看似简单,但它的编码方式、范围限制和使用策略,直接影响代码的体积和性能。我建议你在写性能敏感代码时,养成检查立即数的习惯。一个小常数,可能就是性能瓶颈的源头。