CKB/CKB-VM 宏指令融合 MOP
原理介绍
宏指令融合(Macro-Operation Fusion, MOP Fusion)是现代微处理器体系结构中的一种性能优化技术: 在指令解码阶段将若干相邻的宏操作序列合并为单条内部微操作, 从而降低指令分派开销并减少执行周期. 该技术已在 x86, ARM 等主流架构的高性能微架构中得到广泛应用, 也可在虚拟机层面通过解码器实现.
CKB-VM 在 2021 Edition 硬分叉中引入了宏指令融合机制, 并在 2023 Edition 中进一步扩充了融合规则集. 本文介绍 CKB-VM 当前支持的全部融合模式.
以有符号除法为例: 计算 100 / 42 并分别获取商与余数, 对应的 RISC-V 汇编如下:
addi t0, t0, 100
addi t1, t1, 42
div a0, t0, t1
rem a1, t0, t1
在非融合模式下, CKB-VM 会依次执行 div 和 rem 两条独立指令. 然而, CKB-VM 的 ASM 解释后端运行于 x86-64 宿主机之上, x86-64 的 IDIV 指令在执行有符号除法时会同时产生商与余数(参考手册). 因此, 当 div 与 rem 相邻出现且两个操作数完全相同时, 可以将这两条 RISC-V 指令融合为单条内部伪指令, 实际执行时仅执行一条 IDIV, 从而大幅度降低执行周期.
CKB-VM 在解码阶段检测相邻指令序列是否满足融合条件, 若满足则将其合并为一条内部伪指令; 这些伪指令在 x86-64 后端通常仅对应一条原生指令, 而非融合前的多条指令.
支持的宏融合指令
CKB-VM 支持的宏融合规则随版本迭代逐步扩充, 目前共有 15 条融合规则. 融合后指令的执行周期数等于原始序列中最高单条指令周期数, 其余指令周期归零. 下面分别介绍两次硬分叉引入的融合规则.
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
SBB: 带借位减法(Subtract with Borrow), 5 条指令, 周期 1.
用于 128 位整数减法的低位字计算: 将两个 64 位值相减并传播借位.
sub r0, r1, r0
sltu r2, r1, r4
sub r1, r0, r3
sltu r3, r0, r1
or r0, r3, r2
激活条件:
r0 != x0,r1 != x0,r2 != x0,r3 != x0- 第二条
sltu的 rs2(r4)不受融合规则约束
WIDE_MUL: 有符号宽乘法, 2 条指令, 周期 5.
同时计算两个有符号 64 位整数之积的高 64 位与低 64 位, 得到完整的 128 位结果.
mulh r0, r1, r2
mul r3, r1, r2
激活条件:
r0 != r1,r0 != r2,r0 != r3
WIDE_MULU: 无符号宽乘法, 2 条指令, 周期 5.
mulhu r0, r1, r2
mul r3, r1, r2
激活条件:
r0 != r1,r0 != r2,r0 != r3
WIDE_MULSU: 有符号与无符号宽乘法, 2 条指令, 周期 5.
mulhsu r0, r1, r2
mul r3, r1, r2
激活条件:
r0 != r1,r0 != 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_DIVU: 无符号宽除法, 2 条指令, 周期 32.
divu r0, r1, r2
remu r3, r1, r2
激活条件:
r0 != r1,r0 != r2,r0 != r3
FAR_JUMP_REL: PC 相对远跳转, 2 条指令, 周期 3.
用于调用偏移量超出 jal 可达范围(±1 MiB)的函数.
auipc ra, hi
jalr ra, ra, lo
激活条件:
jalr的rd和rs1均为ra
FAR_JUMP_ABS: 绝对地址远跳转, 2 条指令, 周期 3.
lui ra, hi
jalr ra, ra, lo
激活条件:
jalr的rd和rs1均为ra
LD_SIGN_EXTENDED_32_CONSTANT: 加载 32 位符号扩展立即数, 2 条指令, 周期 1.
将一个 32 位符号扩展立即数装入寄存器.
lui r0, hi
addiw r0, r0, lo
激活条件:
lui与addiw的目标寄存器相同
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
SBBS: 带借位输出的减法(Borrowing Subtraction), 2 条指令, 周期 1.
SBB 的简化形式, 仅执行一次减法并同时输出借位标志.
sub r0, r1, r2
sltu r3, r1, r2
激活条件:
r0 != r1,r0 != 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
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
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
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
激活条件:
auipc与addi的目标寄存器相同
性能分析
以下基准测试对 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 | 9,261,983 | −2.6% |
| k256_ecdsa | 37,225,290 | 41,084,856 | +10.4% |
| k256_schnorr | 17,603,180 | 18,830,487 | +7.0% |
| p256 | 28,359,207 | 24,815,077 | −12.5% |
| rsa | 27,871,846 | 27,321,671 | −2.0% |
| secp256k1_ecdsa | 10,776,242 | 10,732,561 | −0.4% |
| secp256k1_schnorr | 10,465,140 | 10,622,287 | +1.5% |
| sphincsplus_ref | 279,791,883 | 232,608,091 | −16.9% |
分析
显著受益: sphincsplus_ref (−16.9%) 和 p256 (−12.5%) 是 MOP 融合的最大受益者. SPHINCS+ 以哈希函数调用为主, 含有大量 256 位整数加减链, ADD3A/B/C 和 ADC/ADCS 系列可在关键循环中频繁触发. P-256 的域乘法同样有较多宽乘法序列, WIDE_MUL 融合在此发挥效果.
收益有限: secp256k1 和 RSA 的提升不足 2%. secp256k1 在 CKB 生态中经过高度手工优化, 编译器生成的指令序列未必与 MOP 融合窗口对齐; RSA 的主要开销集中在模幂的乘法累加, 部分路径可被 WIDE_MUL 覆盖但绝对量较小.
出现回退: k256 系列 (k256_ecdsa +10.4%, k256_schnorr +7.0%) 和 secp256k1_schnorr (+1.5%) 在开启 MOP 后实际变慢. k256 回退尤为明显, 且其置信区间也明显比 asm 组宽 (asm/k256_ecdsa 区间约 150K cycles, mop/k256_ecdsa 约 2000K cycles), 说明 MOP 解码在该代码路径上引入了额外的分支预测压力.
值得注意的是, 以上测试均使用了 Rust 和 C 语言编写的算法实现, 其编译器优化策略和生成的指令序列可能不完全适配 MOP 融合规则, 因此实际性能表现可能受限于特定实现细节. 实际上, 针对热点代码路径进行手工汇编优化可以更好地利用 MOP 融合, 实际性能提升可能远超上述基准测试结果.
您可以在 此仓库 中找到完整的基准测试代码和数据.