Skip to content

Wrap gas counter (gas_left) in a strong Gas type#1564

Draft
chfast wants to merge 1 commit into
masterfrom
evm/gas-strong-type
Draft

Wrap gas counter (gas_left) in a strong Gas type#1564
chfast wants to merge 1 commit into
masterfrom
evm/gas-strong-type

Conversation

@chfast

@chfast chfast commented Jun 11, 2026

Copy link
Copy Markdown
Member

gas_left is a bare int64_t threaded through the whole interpreter, so it can be silently confused with unrelated integers (stack values, memory sizes, EVMC's own int64 gas fields). Wrap it in evmone::Gas: conversions to/from int64_t are explicit, so gas only crosses into raw int64_t at the EVMC boundary (message.gas, evmc::Result.gas_left, the GAS opcode push, tracing) through deliberate static_cast. The gas-domain operators (-=, +=, binary -, +, /, <=>) accept raw int64_t cost operands, so instruction bodies are unchanged and the change stays mechanical: Result::gas_left, check_requirements, every core impl, grow_memory/check_memory, dispatch[_cgoto] and
AdvancedExecutionState::gas_left.

Generated code is not identical to the int64_t version (measured on the clang assertions build, x86-64):

  • baseline_execution.cpp.o gains 127 static instructions. The gas check (gas_left -= cost) < 0 lowers to subq; jl (signed-less, reusing the subtraction's flags) instead of subq; js (sign bit) -- same instruction count; in cmov contexts the spaceship form even drops a testq (subq; cmovge vs testq; cmovns).

  • Hot path is zero-cost. Deterministic instruction counts (google benchmark --benchmark_perf_counters=INSTRUCTIONS) over single-opcode loops running millions of iterations stay within +0.003%: ADD +0.003%, MUL +0.001%, SUB/LT/GT/ISZERO/NOT/SIGNEXTEND/JUMPDEST +0.002..0.007%.

  • A fixed ~255 extra instructions per execute() call remain (setup and teardown inlining churn from the boundary casts and the Gas dispatch return value): +0.003% on normal workloads, up to +0.9% only on trivially short executions (synth/loop_v1: 27544 vs 27289 instructions).

1115/1115 unit tests pass (baseline, advanced, cgoto).

gas_left is a bare int64_t threaded through the whole interpreter, so it
can be silently confused with unrelated integers (stack values, memory
sizes, EVMC's own int64 gas fields). Wrap it in evmone::Gas: conversions
to/from int64_t are explicit, so gas only crosses into raw int64_t at the
EVMC boundary (message.gas, evmc::Result.gas_left, the GAS opcode push,
tracing) through deliberate static_cast. The gas-domain operators
(-=, +=, binary -, +, /, <=>) accept raw int64_t cost operands, so
instruction bodies are unchanged and the change stays mechanical:
Result::gas_left, check_requirements, every core impl,
grow_memory/check_memory, dispatch[_cgoto] and
AdvancedExecutionState::gas_left.

Generated code is not identical to the int64_t version (measured on the
clang assertions build, x86-64):

- baseline_execution.cpp.o gains 127 static instructions. The gas check
  (gas_left -= cost) < 0 lowers to `subq; jl` (signed-less, reusing the
  subtraction's flags) instead of `subq; js` (sign bit) -- same
  instruction count; in cmov contexts the spaceship form even drops a
  testq (`subq; cmovge` vs `testq; cmovns`).

- Hot path is zero-cost. Deterministic instruction counts (google
  benchmark --benchmark_perf_counters=INSTRUCTIONS) over single-opcode
  loops running millions of iterations stay within +0.003%: ADD +0.003%,
  MUL +0.001%, SUB/LT/GT/ISZERO/NOT/SIGNEXTEND/JUMPDEST +0.002..0.007%.

- A fixed ~255 extra instructions per execute() call remain (setup and
  teardown inlining churn from the boundary casts and the Gas dispatch
  return value): +0.003% on normal workloads, up to +0.9% only on
  trivially short executions (synth/loop_v1: 27544 vs 27289 instructions).

1115/1115 unit tests pass (baseline, advanced, cgoto).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant