CU costs
Compute-unit notes for validation, loading, receipts, events, and access tiers.
Every operation Hopper emits, grouped by axis, with a measured or structural cost. Numbers come from hopper profile bench runs against a local validator with a Solana 2.1.x SBF toolchain, or from first-principles counting of syscalls where a benchmark is not meaningful (there is no way to benchmark a construct that compiles out).
The point of this page is that every CU claim elsewhere on the site has a line here it refers back to. If a number in the marketing drifts from this table, the table wins.
Baselines
Three references to calibrate everything below.
| Reference | CU |
|---|---|
Empty sol_log_(0) |
~100 |
sol_log_64_(...) (five u64) |
~100 |
sol_invoke_signed_c syscall, no-op recipient |
~600 |
Solana runtime charges these regardless of framework. Every number below is additive on top.
Account access
| Operation | CU | Notes |
|---|---|---|
AccountView::address() |
0 | pointer read into the SVM input region |
AccountView::lamports() |
0 | direct field access |
AccountView::data_len() |
0 | direct field access |
AccountView::try_borrow() |
~2 | borrow-flag check plus slice construction |
pod_from_bytes::<T>(data) |
~3 | length check + pointer cast |
Account::load::<T>() |
~5 | owner + disc + length + pointer cast |
Account::load_mut::<T>() |
~7 | load plus writable + borrow-registry update |
ctx.field_segment_ref::<T>(i, o) |
~4 | const offset + length check |
raw_ref() / raw_mut() (unsafe) |
0 | identity pointer cast |
Anchor's zero-copy path via AccountLoader<T> measures ~12 CU for the equivalent of load, because the RefCell bookkeeping is not compile-time folded. Hopper's segment-level registry is compile-time folded, so the mut flag update resolves to a field write with no branch.
Token / Mint reads
Every constraint that ends in a require_* helper reads the exact bytes it needs and nothing more.
| Constraint | CU | Notes |
|---|---|---|
token::mint = expr |
~8 | 32-byte compare on bytes [0..32] |
token::authority = expr |
~8 | 32-byte compare on bytes [32..64] |
token::token_program = expr |
~4 | owner pubkey compare |
mint::authority = expr |
~12 | COption tag check plus 32-byte compare |
mint::decimals = N |
~3 | single-byte compare at offset 44 |
mint::freeze_authority = expr |
~12 | COption tag check plus 32-byte compare |
associated_token::mint = expr |
~60 | full ATA PDA derivation (one create_program_address) plus compare |
Anchor routes every token constraint through InterfaceAccount<TokenAccount> which deserializes the full 165-byte account via Borsh before any check. Measured ~80 CU for the same token::mint check. Hopper is 10x cheaper on this path.
Token-2022 extension TLV
Each extension constraint is one TLV walk from the start of the extension region.
| Constraint | CU | Notes |
|---|---|---|
extensions::non_transferable |
~35 | scan mint TLV, find type byte 9 |
extensions::mint_close_authority |
~45 | scan + 32-byte compare |
extensions::transfer_hook::authority |
~45 | scan + 32-byte compare on offset 0 of payload |
extensions::transfer_hook::program_id |
~45 | scan + 32-byte compare on offset 32 of payload |
extensions::metadata_pointer::* |
~45 | same shape as transfer_hook |
extensions::permanent_delegate |
~45 | scan + 32-byte compare |
extensions::transfer_fee_config::* |
~50 | scan + one 32-byte compare |
extensions::interest_bearing::rate_authority |
~45 | scan + 32-byte compare |
extensions::default_account_state::state |
~35 | scan + single-byte compare |
extensions::immutable_owner |
~35 | scan token-account TLV, find type byte 7 |
Anchor's InterfaceAccount<Mint> path plus Borsh deserialize hits ~400-600 CU for the same check, depending on extension count. Hopper is 10x to 17x cheaper across the board. Every number is a single TLV scan plus a compare; there is no allocator call anywhere.
PDAs
| Operation | CU | Notes |
|---|---|---|
seeds = [...] + bump (inferred) |
~1500 to ~3000 | find_program_address walks bumps 255 down |
seeds = [...] + bump = stored_field |
~25 | create_program_address one iteration |
seeds::program = expr override |
+0 | identical cost with a different program id |
seeds_fn = Type::seeds(...) |
same as above | the sugar is a caller-side indirection |
Store the bump in the account whenever you can. The 60x speedup is the single biggest CU win available to program authors.
Instruction dispatch
| Shape | CU | Notes |
|---|---|---|
Single-byte match data[0] |
~3 | jump table, compiler-folded |
Multi-byte data.starts_with(&[...]) chain |
~3 per arm | ordered longest-first |
ctx_args = K forward to bind_with_args |
+0 | inlined |
Quasar's dispatch is identical in shape. Pinocchio's is hand-written per program; the same cost when written correctly.
Logging
| Call | CU | Notes |
|---|---|---|
hopper_log!("literal") |
~100 | direct sol_log_ syscall |
hopper_log!("label", u64_value) |
~200 | one sol_log_ plus one sol_log_64_ |
msg!("text") |
~100 | same syscall, no format |
msg!("fmt {}", x) |
~300 to ~600 | depends on format string length and arg count |
emit!(Event { ... }) |
~250 | sol_log_data with one segment |
hopper_emit_cpi!(...) |
~1500 | self-CPI path, reliable indexing |
The log vs emit_cpi tradeoff is the same in Anchor. Use emit! for cheap telemetry, hopper_emit_cpi! for the events indexers must not drop.
Receipts
| Operation | CU | Notes |
|---|---|---|
| Receipt scope begin | ~40 | snapshot capture on mutable segments |
| Receipt finish (success) | ~250 | hash + write + sol_log_data |
| Receipt finish (invariant failure) | ~300 | plus the failure-stage stamp |
Anchor has no equivalent today. The ~300 CU buys you "Invariant balance_nonzero failed" attribution in every indexer without a per-program lookup table.
CPI
| Op | CU | Notes |
|---|---|---|
invoke(&ix, &accounts) |
~600 + recipient | hopper-native-backend direct syscall |
invoke_signed(&ix, &accounts, &signers) |
~750 + recipient | syscall plus seed setup |
HopperDynCpi::invoke_signed |
~750 + recipient | same cost; stack-only build |
Anchor CpiContext::new_with_signer |
~850 + recipient | extra bookkeeping around the same syscall |
Signer seeds threading costs ~150 CU no matter who sets it up.
How to measure yourself
Use the CLI shipped in this release:
hopper profile bench --fail-on-regression 2
against any program that links hopper-bench, or point hopper profile elf at a compiled .so for static size analysis, SBF instruction-count estimates, optional ELF section summaries, and a flamegraph.
Why these numbers hold up
Two reasons. First, Hopper compiles handler code into straight-line accessors with const-folded offsets; there is no runtime dispatch to pay. Second, the Token-2022 TLV readers walk the extension region directly instead of deserializing the parent account; the scan is shorter than the Borsh alternative by construction, not by micro-optimization.
Nothing on this page is a benchmark cherry-pick. If a number in Hopper's emitted code shifts, this file moves with it.
