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:
BigInteger multiplication (_execFeeFactor * price)
BigInteger addition (_feeConsumed += fee)
BigInteger comparison (_feeConsumed > _feeAmount)
For tight loops executing millions of instructions, this creates significant GC pressure from BigInteger internal heap allocations.
Proposed Change
- Pre-compute opcode fees once in the constructor into a
long[256] table: _precomputedOpCodeFees[i] = (long)_execFeeFactor * OpCodePriceTable[i]
- Track fee consumed as
long (_feeConsumedLong) instead of BigInteger
- Replace
PreExecuteInstruction body with a single array index + long addition:
_feeConsumedLong += _precomputedOpCodeFees[(byte)opCode];
- 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
Summary
Replace
BigIntegerarithmetic withlongin theApplicationEngineper-instruction fee hot path. This is safe because GAS is bounded to 100M × 10^8 = 10^16 in picoGAS, well withinlong.MaxValue(~9.2 × 10^18).Problem
Every VM instruction executes
PreExecuteInstruction, which callsAddFee(_execFeeFactor * OpCodePriceTable[opcode]). In the current code, all three values (_feeConsumed,_feeAmount,_execFeeFactor) areBigInteger. This means every instruction triggers:BigIntegermultiplication (_execFeeFactor * price)BigIntegeraddition (_feeConsumed += fee)BigIntegercomparison (_feeConsumed > _feeAmount)For tight loops executing millions of instructions, this creates significant GC pressure from
BigIntegerinternal heap allocations.Proposed Change
long[256]table:_precomputedOpCodeFees[i] = (long)_execFeeFactor * OpCodePriceTable[i]long(_feeConsumedLong) instead ofBigIntegerPreExecuteInstructionbody with a single array index + long addition:FeeConsumedandGasLeftusing pure long arithmetic: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)
FeeConsumed property getter
10K ops (micro-benchmark)
Impact
FeeConsumedandGasLeftalready returnlongFiles Affected
src/Neo/SmartContract/ApplicationEngine.cs