CKB/CKB-VM 宏指令融合 MOP

原理介绍

宏指令融合(Macro-Operation Fusion, MOP Fusion)是现代微处理器体系结构中的一种性能优化技术: 在指令解码阶段将若干相邻的宏操作序列合并为单条内部微操作, 从而降低指令分派开销并减少执行周期. 该技术已在 x86, ARM 等主流架构的高性能微架构中得到广泛应用, 也可在虚拟机层面通过解码器实现. 我们通过一个实际的例子来说明 MOP 融合的原理与优势.

以有符号除法为例, 假设我们要计算 100 / 42 并分别获取商与余数, 对应的 RISC-V 汇编如下:

addi t0, t0, 100
addi t1, t1, 42

div a0, t0, t1
rem a1, t0, t1

在正常执行模式下, CKB-VM 会依次执行 divrem 两条独立指令. 然而我们发现有优化空间, CKB-VM 的 ASM 解释后端运行于 x86-64 宿主机之上, x86-64 的 IDIV 指令在执行有符号除法时会同时产生商与余数(参考手册). 因此, 当 divrem 相邻出现且两个操作数完全相同时, 我们其实可以将这两条 RISC-V 指令融合为单条内部伪指令, 该内部指令只对应一条 x86-64 的 IDIV 指令而不是两条, 从而大幅度降低执行周期.

CKB-VM 在 2021 Edition 硬分叉中引入了宏指令融合机制, 并在 2023 Edition 中进一步扩充了融合规则集. CKB-VM 在解码阶段检测相邻指令序列是否满足融合条件, 若满足则将其合并为一条内部指令. 这些内部指令在 x86-64 后端通常仅对应一条原生指令, 而非融合前的多条指令.

CKB-VM 支持的宏融合规则随版本迭代逐步扩充, 目前共有 16 条融合规则. 融合后指令的执行周期数等于原始序列中最高单条指令周期数, 其余指令周期归零. 下面分别介绍两次硬分叉引入的融合规则.

CKB 2021 Edition

ADC: 带进位加法(Add with Carry), 5 条指令, 周期 1. 用于 128 位整数加法的低位字计算: 将两个 64 位值相加并传播进位.

激活条件:

add  r0, r0, r1
sltu r1, r0, r1
add  r0, r0, r2
sltu r2, r0, r2
or   r1, r1, r2
  • r0 != x0, r1 != x0, r2 != x0

融合后指令:

adc r0, r1, r2
  • 语义视角: r0 = (r0 + r1 + r2) mod 2^64; r1 = carry1 | carry2; r2 = carry2.

SBB: 带借位减法(Subtract with Borrow), 5 条指令, 周期 1. 用于 128 位整数减法的低位字计算: 将两个 64 位值相减并传播借位.

激活条件:

sub  r0, r1, r0
sltu r2, r1, r0
sub  r1, r0, r3
sltu r3, r0, r1
or   r0, r3, r2
  • r0 != x0, r1 != x0, r2 != x0, r3 != x0

融合后指令:

sbb r1, r0, r3, r2
  • 语义视角: r1 = (r1 - r0) - r3; r0 = borrow1 | borrow2; r2 = borrow1; r3 = borrow2.

WIDE_MUL: 有符号宽乘法, 2 条指令, 周期 5. 同时计算两个有符号 64 位整数之积的高 64 位与低 64 位, 得到完整的 128 位结果.

激活条件:

mulh r0, r1, r2
mul  r3, r1, r2
  • r0 != r1, r0 != r2, r0 != r3

融合后指令:

wide_mul r0, r3, r1, r2
  • 语义视角: r1 * r2, 高位写入 r0, 低位写入 r3

WIDE_MULU: 无符号宽乘法, 2 条指令, 周期 5.

激活条件:

mulhu r0, r1, r2
mul   r3, r1, r2
  • r0 != r1, r0 != r2, r0 != r3

融合后指令:

wide_mulu r0, r3, r1, r2
  • 语义视角: 无符号 r1 * r2, 高位写入 r0, 低位写入 r3

WIDE_MULSU: 有符号与无符号宽乘法, 2 条指令, 周期 5.

激活条件:

mulhsu r0, r1, r2
mul    r3, r1, r2
  • r0 != r1, r0 != r2, r0 != r3

融合后指令:

wide_mulsu r0, r3, r1, r2
  • 语义视角: 有符号/无符号 r1 * r2, 高位写入 r0, 低位写入 r3

WIDE_DIV: 有符号宽除法, 2 条指令, 周期 32. 同时计算有符号除法的商与余数, 对应 x86-64 的单条 IDIV 指令.

激活条件:

div r0, r1, r2
rem r3, r1, r2
  • r0 != r1, r0 != r2, r0 != r3

融合后指令:

wide_div r0, r3, r1, r2
  • 语义视角: 有符号 r1 / r2, 商写入 r0, 余数写入 r3

WIDE_DIVU: 无符号宽除法, 2 条指令, 周期 32.

激活条件:

divu r0, r1, r2
remu r3, r1, r2
  • r0 != r1, r0 != r2, r0 != r3

融合后指令:

wide_divu r0, r3, r1, r2
  • 语义视角: 无符号 r1 / r2, 商写入 r0, 余数写入 r3

FAR_JUMP_REL: PC 相对远跳转, 2 条指令, 周期 3. 用于调用偏移量超出 jal 可达范围(±1 MiB)的函数.

激活条件:

auipc ra, hi
jalr  ra, ra, lo
  • jalrrdrs1 均为 ra

融合后指令:

far_jump_rel ra, imm
  • 语义视角: ra 写入返回地址, 下一条 PC 为 (pc + imm) & ~1

FAR_JUMP_ABS: 绝对地址远跳转, 2 条指令, 周期 3.

激活条件:

lui  ra, hi
jalr ra, ra, lo
  • jalrrdrs1 均为 ra

融合后指令:

far_jump_abs ra, imm
  • 语义视角: ra 写入返回地址, 下一条 PC 为绝对地址 imm (最低位清零)

LD_SIGN_EXTENDED_32_CONSTANT: 加载 32 位符号扩展立即数, 2 条指令, 周期 1. 将一个 32 位符号扩展立即数装入寄存器.

激活条件:

lui   r0, hi
addiw r0, r0, lo
  • luiaddiw 的目标寄存器相同

融合后指令:

custom_load_imm r0, imm

语义视角: r0 = (imm << 12) + lo, 其中 loimm 的低 12 位, 并进行符号扩展至 64 位.

CKB 2023 Edition

ADCS: 带进位输出的加法(Overflowing Addition), 2 条指令, 周期 1. 它是 ADC 的简化形式, 仅执行一次加法并同时输出进位标志, 也是 ADD3A/ADD3B/ADD3C 的基本组成单元.

激活条件:

add  r0, r1, r2
sltu r3, r0, r1
// 或
add  r0, r2, r1
sltu r3, r0, r1
  • r0 != r1, r0 != x0

融合后指令:

adcs r0, r1, r2, r3
  • 语义视角: sum = r1 + r2; r0 = sum; r3 = (sum < r1).

SBBS: 带借位输出的减法(Borrowing Subtraction), 2 条指令, 周期 1. 它是 SBB 的简化形式, 仅执行一次减法并同时输出借位标志.

激活条件:

sub  r0, r1, r2
sltu r3, r1, r2
  • r0 != r1, r0 != r2

融合后指令:

sbbs r0, r1, r2, r3
  • 语义视角: dif = r1 - r2; r0 = dif; r3 = (r1 < r2).

ADD3A: 进位传播加法变体 A, 3 条指令, 周期 1. 加法结果写回 rs2 所在寄存器, 进位标志存入独立寄存器, 再加上额外的进位输入.

激活条件:

add  r0, r1, r0
sltu r2, r0, r1
add  r3, r2, r4
  • r0 != r1, r0 != r4, r2 != r4, r0 != x0, r2 != x0

融合后指令:

add3a r0, r1, r2, r3, r4
  • 语义视角: r0 = r0 + r1; r2 = carry(r0 + r1); r3 = r2 + r4.

ADD3B: 进位传播加法变体 B, 3 条指令, 周期 1. 进位标志回写至 rs1 所在寄存器, 再与第三个操作数相加.

激活条件:

add  r0, r1, r2
sltu r1, r0, r1
add  r3, r1, r4
  • r0 != r1, r0 != r4, r1 != r4, r0 != x0, r1 != x0

融合后指令:

add3b r0, r1, r2, r3, r4
  • 语义视角: r0 = r1 + r2; r1 = carry(r1 + r2); r3 = r1 + r4.

ADD3C: 进位传播加法变体 C, 3 条指令, 周期 1. 进位标志存入独立寄存器后原地累加第三个操作数.

激活条件:

add  r0, r1, r2
sltu r3, r0, r1
add  r3, r3, r4
  • r0 != r1, r0 != r4, r3 != r4, r0 != x0, r3 != x0

融合后指令:

add3c r0, r1, r2, r3, r4
  • 语义视角: r0 = r1 + r2; r3 = carry(r1 + r2) + r4.

LD_SIGN_EXTENDED_32_CONSTANT (auipc 变体): PC 相对地址物化, 2 条指令, 周期 1. CKB 2023 Edition 将 auipc + addi 纳入与 lui + addiw 相同的融合操作码(LD_SIGN_EXTENDED_32_CONSTANT), 用于将 PC 相对偏移地址在解码阶段直接物化为立即数常量.

激活条件:

auipc r0, hi
addi  r0, r0, lo
  • auipcaddi 的目标寄存器相同

融合后指令:

custom_load_imm r0, imm

语义视角: r0 = sext_i32(hi20 + lo12 + pc), 最终以 i32 解释并符号扩展到 64 位(PC 相对地址物化).

性能分析

以下基准测试对 CKB-VM ASM 后端在启用/关闭 MOP 融合时的表现进行比较:

  • asm: ASM 后端, 不启用 MOP 融合
  • mop: ASM 后端, 已启用 MOP 融合

测试对象均为 CKB 链上常见的密码学算法, 使用 Criterion 测量 100 次采样的中位周期数. 下表中的 cycles 指的是 host 端 x86-64 CPU 上执行 CKB-VM 时的周期数, 而不是 RISC-V 指令周期数, 这点值得注意. 对比数据如下:

算法 asm (cycles) mop (cycles) 变化幅度
ed25519 9,508,012 8,804,419 −7.4%
k256_ecdsa 37,225,290 36,353,859 -2.3%
k256_schnorr 17,603,180 16,870,957 -4.2%
p256 28,359,207 24,815,077 −12.5%
rsa 27,871,846 24,778,071 −11.0%
secp256k1_ecdsa 10,776,242 9,732,561 −9.7%
secp256k1_schnorr 10,465,140 9,308,162 -11.0%
sphincsplus_ref 279,791,883 327,076,711 +16.9%

几乎所有算法在启用 MOP 后都获得了性能提升, 测试中只有一例算法在启用 MOP 后出现了性能回退. 为了分析这些性能变化, 我们统计了每个算法在启用 MOP 后触发的融合操作码类型以及 MOP 指令占总执行指令的比例, 结果如下:

算法 mops mop 指令百分比(%)
ed25519 WIDE_MULU(15876), ADCS(52946), ADD3B(1518) 7.1635
k256_ecdsa WIDE_MULU(63072), ADCS(56685), SBBS(488), ADD3B(1827), ADD3C(12152) 3.2395
k256_schnorr WIDE_MULU(31389), ADCS(22439), SBBS(4), ADD3B(766), ADD3C(5490) 3.0162
p256 WIDE_MULU(87082), ADCS(414371), SBBS(4846), ADD3B(15903), ADD3C(262) 12.8793
rsa WIDE_MULU(191533), ADCS(198216) 10.5770
secp256k1_ecdsa WIDE_MUL(146), WIDE_MULU(11877), ADCS(53290), ADD3B(1146), ADD3C(613) 13.7066
secp256k1_schnorr WIDE_MUL(64), WIDE_MULU(11672), ADCS(51557), ADD3B(1127), ADD3C(589) 13.6859
sphincsplus_ref / 0

这里有一个很有意思的细节: 在收益最明显的几组数据里, ADCSWIDE_MULU 的出现频率普遍较高, 这与大整数运算中 carry-chain 和宽乘法的热点特征高度一致. 也就是说, MOP 的优化方向和密码学代码的热点路径是对齐的.

我们可以从上表中看出, 各算法在启用 MOP 后触发的融合操作码类型和次数差异较大, 这也解释了不同算法在性能提升上的差异. 对于 sphincsplus_ref 来说, 由于其从未触发过任何融合操作码, 在启用 MOP 后反而出现了性能回退: 这是因为 MOP 解码器在解码阶段引入了额外的计算开销, 但该算法的指令序列中没有任何融合机会, 因此无法抵消解码器开销, 导致整体性能下降.

值得注意的是, 以上测试均使用了 Rust 和 C 语言编写的算法实现, 其编译器优化策略和生成的指令序列可能不完全适配 MOP 融合规则, 因此实际性能表现可能受限于特定实现细节. 实际上, 针对热点代码路径进行手工汇编优化可以更好地利用 MOP 融合, 实际性能提升可能远超上述基准测试结果. 进一步说, MOP 在工程上更像"乘数因子": 它放大的是你原本就接近目标模式的代码质量. 若编译产物天然容易形成融合模板, 收益会比较自然地出现; 若指令形态和规则长期错位, 那么即使开启 MOP 也不会自动变快.

您可以在 此仓库 中找到完整的基准测试代码和数据.

总结

CKB-VM 的 MOP 融合本质上是在解码阶段用少量模式匹配开销, 换取执行阶段显著的指令级收益: 当指令序列满足融合条件时, 多条 RISC-V 指令可被压缩为单条内部操作, 并映射到宿主机更高效的原生指令路径. 从 2021 Edition 到 2023 Edition, 融合规则从 10 条扩展到 16 条, 覆盖了大整数加减乘除, 远跳转与常量/地址物化等常见热点模式, 这也是链上密码学算法获得整体加速的核心原因.

基准结果也说明了 MOP 的收益边界: 它不是无条件加速, 而是针对特定模式的优化器. 能频繁触发融合操作码的算法, 往往可以获得可观性能提升; 若几乎无法命中融合规则, 解码器额外开销就可能导致轻微回退. 因此, 在实际工程中应将 MOP 与代码生成策略协同考虑, 通过编译器选项调优或热点手工汇编, 主动构造更易融合的指令序列, 才能稳定释放 CKB-VM ASM 后端的性能潜力.