Skip to content

Optimization: Replace BigInteger with long for per-instruction fee arithmetic #4521

@Jim8y

Description

@Jim8y

Summary

Replace BigInteger arithmetic with long in the ApplicationEngine per-instruction fee hot path. This is safe because GAS is bounded to 100M × 10^8 = 10^16 in picoGAS, well within long.MaxValue (~9.2 × 10^18).

Problem

Every VM instruction executes PreExecuteInstruction, which calls AddFee(_execFeeFactor * OpCodePriceTable[opcode]). In the current code, all three values (_feeConsumed, _feeAmount, _execFeeFactor) are BigInteger. This means every instruction triggers:

  1. BigInteger multiplication (_execFeeFactor * price)
  2. BigInteger addition (_feeConsumed += fee)
  3. BigInteger comparison (_feeConsumed > _feeAmount)

For tight loops executing millions of instructions, this creates significant GC pressure from BigInteger internal heap allocations.

Proposed Change

  1. Pre-compute opcode fees once in the constructor into a long[256] table: _precomputedOpCodeFees[i] = (long)_execFeeFactor * OpCodePriceTable[i]
  2. Track fee consumed as long (_feeConsumedLong) instead of BigInteger
  3. Replace PreExecuteInstruction body with a single array index + long addition:
    _feeConsumedLong += _precomputedOpCodeFees[(byte)opCode];
  4. Expose FeeConsumed and GasLeft using pure long arithmetic:
    public long FeeConsumed => (_feeConsumedLong + FeeFactor - 1) / FeeFactor;
    public long GasLeft => (_feeAmountLong - _feeConsumedLong) / FeeFactor;

Safety: GAS max supply = 100M × 10^8 = 10^16 picoGAS. long.MaxValue ≈ 9.2 × 10^18. Even 920x the entire GAS supply would not overflow.

Benchmark Results

Measured on AMD EPYC 9454, .NET 10.0.5, BenchmarkDotNet v0.15.5.

Per-instruction fee arithmetic (isolated hot loop)

Scale BigInteger (current) long (proposed) Speedup Allocation
1M ops 21,018 us 543 us 38.7x 31.9 MB → 0
10M ops 217,613 us 5,620 us 38.7x 319.9 MB → 0

FeeConsumed property getter

Scale BigInteger DivideCeiling long division Speedup
1M calls 15,477 us 264 us 58.6x

10K ops (micro-benchmark)

Pattern Mean Allocated
BigInteger multiply + add + compare 148,468 ns 201,824 B
long precomputed table lookup 5,612 ns 0 B
Speedup 26.5x 100% eliminated

Impact

  • No API changes: FeeConsumed and GasLeft already return long
  • No consensus risk: The optimization is internal to the engine, no change to on-chain behavior
  • GC reduction: Eliminates ~32 MB of BigInteger allocations per 1M VM instructions
  • Consistent speedup: 38.7x at both 1M and 10M scale, confirming constant-overhead removal

Files Affected

  • src/Neo/SmartContract/ApplicationEngine.cs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions