Deep Dive Into CKB-VM Macro-ops Fusion
Overview
Macro-Operation Fusion (MOP Fusion) is a performance optimization technique used in modern microprocessor architectures. During instruction decode, several adjacent macro-operations are merged into a single internal micro-operation, reducing dispatch overhead and execution cycles. This technique is widely used in high-performance microarchitectures for mainstream architectures such as x86 and ARM, and it can also be implemented at the virtual-machine layer through the decoder. The following example illustrates the principles and benefits of MOP fusion.
Take signed division as an example. Suppose we want to compute 100 / 42 and obtain both the quotient and the remainder. The corresponding RISC-V assembly looks like this:
addi t0, t0, 100 # Load 100 into register t0
addi t1, t1, 42 # Load 42 into register t1
div a0, t0, t1 # Calc 100 / 42, store the result into a0
rem a1, t0, t1 # Calc 100 % 42, store the result into a1
In normal execution mode, CKB-VM executes div and rem as two independent instructions. However, there is optimization potential here. The ASM backend of CKB-VM runs on an x86-64 host machine, and the x86-64 IDIV instruction produces both quotient and remainder when performing signed division (reference manual). Therefore, when div and rem appear adjacent to each other and both operands are exactly the same, we can fuse these two RISC-V instructions into a single internal pseudo-instruction. That internal instruction maps to one x86-64 IDIV instruction instead of two, substantially reducing execution cycles.
CKB-VM introduced macro-operation fusion in the 2021 Edition hard fork, and expanded the fusion rule set further in the 2023 Edition. During decoding, CKB-VM checks whether adjacent instruction sequences satisfy the fusion conditions, and if so, merges them into a single internal instruction. These internal instructions usually correspond to just one native instruction in the x86-64 backend, rather than the multiple instructions that existed before fusion.
The set of supported macro-fusion rules in CKB-VM has been expanded gradually across versions. There are currently 16 fusion rules in total. After fusion, the execution cycle count of the fused instruction equals the highest cycle count among the original instructions in the sequence, while the remaining instructions are reduced to zero cycles. The following sections describe the fusion rules introduced by the two hard forks.
CKB 2021 Edition
ADC: Add with Carry, 5 instructions, cycle cost 1. Used for the low-word computation in 128-bit integer addition: it adds two 64-bit values and propagates the carry.
SBB: Subtract with Borrow, 5 instructions, cycle cost 1. Used for the low-word computation in 128-bit integer subtraction: it subtracts two 64-bit values and propagates the borrow.
WIDE_MUL: Signed wide multiplication, 2 instructions, cycle cost 5. Computes both the high 64 bits and low 64 bits of the product of two signed 64-bit integers, yielding the full 128-bit result.
WIDE_MULU: Unsigned wide multiplication, 2 instructions, cycle cost 5.
WIDE_MULSU: Signed/unsigned wide multiplication, 2 instructions, cycle cost 5.
WIDE_DIV: Signed wide division, 2 instructions, cycle cost 32. Computes both the quotient and remainder of signed division, corresponding to the single x86-64 IDIV instruction.
WIDE_DIVU: Unsigned wide division, 2 instructions, cycle cost 32.
FAR_JUMP_REL: PC-relative far jump, 2 instructions, cycle cost 3. Used to call functions whose offsets exceed the range reachable by jal (+/-1 MiB).
FAR_JUMP_ABS: Absolute-address far jump, 2 instructions, cycle cost 3.
LD_SIGN_EXTENDED_32_CONSTANT: Load a 32-bit sign-extended immediate, 2 instructions, cycle cost 1. Loads a 32-bit sign-extended immediate into a register.
CKB 2023 Edition
ADCS: Add with Carry Out (Overflowing Addition), 2 instructions, cycle cost 1. This is a simplified form of ADC: it performs one addition and simultaneously outputs the carry flag. It is also the basic building block of ADD3A/ADD3B/ADD3C.
SBBS: Subtract with Borrow Out (Borrowing Subtraction), 2 instructions, cycle cost 1. This is a simplified form of SBB: it performs one subtraction and simultaneously outputs the borrow flag.
ADD3A: Carry-propagating addition variant A, 3 instructions, cycle cost 1. The addition result is written back to the register holding rs2, the carry flag is stored in an independent register, and then an extra carry-in is added.
ADD3B: Carry-propagating addition variant B, 3 instructions, cycle cost 1. The carry flag is written back to the register holding rs1, and then added to the third operand.
ADD3C: Carry-propagating addition variant C, 3 instructions, cycle cost 1. The carry flag is stored in an independent register and then the third operand is accumulated in place.
LD_SIGN_EXTENDED_32_CONSTANT (auipc variant): PC-relative address materialization, 2 instructions, cycle cost 1. CKB 2023 Edition adds auipc + addi to the same fusion opcode as lui + addiw (LD_SIGN_EXTENDED_32_CONSTANT), allowing PC-relative offsets to be materialized directly as immediate constants during decoding.
Performance Analysis
The following benchmarks compare the CKB-VM ASM backend with MOP fusion enabled and disabled:
-
asm: ASM backend, MOP fusion disabled
-
mop: ASM backend, MOP fusion enabled
All test targets are cryptographic algorithms commonly used on CKB. Criterion is used to measure the median cycle count across 100 samples. Note that the cycles in the table refer to the cycles spent executing CKB-VM on the host x86-64 CPU, not RISC-V instruction cycles. The comparison is as follows:
| Algorithm | asm (cycles) | mop (cycles) | Change |
|---|---|---|---|
| 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% |
Almost all algorithms benefit from performance improvements when MOP is enabled; only one algorithm regresses. To analyze these changes, we counted the fused opcode types triggered by each algorithm after enabling MOP, as well as the proportion of MOP instructions among total executed instructions. The results are shown below:
| Algorithm | MOPs | MOP instruction percentage (%) |
|---|---|---|
| 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 |
From the table above, we can see that the types and counts of fused opcodes triggered by each algorithm vary significantly after MOP is enabled, which also explains the differences in performance gains. For sphincsplus_ref, because it never triggers any fused opcode, performance regresses after enabling MOP. This is because the MOP decoder introduces extra work during decoding, but the instruction stream of this algorithm offers no fusion opportunities to offset that overhead, resulting in an overall slowdown.
It is worth noting that all of the tests above use algorithm implementations written in Rust and C. Their compiler optimization strategies and generated instruction streams may not be fully aligned with the MOP fusion rules, so the actual performance may be limited by implementation details. In practice, hand-written assembly optimizations for hot code paths can make better use of MOP fusion, and the real performance gains may exceed the benchmark results above.
You can find the complete benchmark code and data in this repository.
Conclusion
The essence of MOP fusion in CKB-VM is to exchange a small amount of pattern-matching overhead during decoding for significant instruction-level gains during execution: when an instruction sequence satisfies the fusion conditions, multiple RISC-V instructions can be compressed into a single internal operation and mapped to a more efficient native instruction path on the host machine. From the 2021 Edition to the 2023 Edition, the fusion rules expanded from 10 to 16, covering common hotspot patterns such as large-integer addition, subtraction, multiplication, division, far jumps, and constant/address materialization. This is the core reason why on-chain cryptographic algorithms achieve overall acceleration.
The benchmark results also show the limits of MOP’s benefit: it is not a universal accelerator, but an optimizer for specific patterns. Algorithms that frequently trigger fused opcodes can often obtain substantial speedups; if the fusion rules are rarely hit, the extra decoder overhead may instead cause a small regression. Therefore, in real engineering work, MOP should be considered together with code-generation strategy. By tuning compiler options or hand-writing hot assembly paths to proactively construct instruction sequences that are easier to fuse, it is possible to consistently unlock the performance potential of the CKB-VM ASM backend.