Use gt(calldatasize(), 3) for the function dispatch check in IR codegen - #16315
Use gt(calldatasize(), 3) for the function dispatch check in IR codegen#16315philippecyberian wants to merge 1 commit into
Conversation
|
Thank you for your contribution to the Solidity compiler! A team member will follow up shortly. If you haven't read our contributing guidelines and our review checklist before, please do it now, this makes the reviewing process and accepting your contribution smoother. If you have any questions or need our help, feel free to post them in the PR or talk to us directly on the #solidity-dev channel on Matrix. |
|
I fixed the genuine CI check failure due to coding style. The remaining failures are due to possibly sporadic (the failing checks change each run) troubles with accessing external resources |
|
Hi @philippecyberian and thanks for this. There's also likely a significantly easier approach to achieve the same effect by changing the Yul codegen directly. Do you want to join our next Wednesday public call so we can discuss it? |
|
Thanks @nikola-matic for reviewing and for the call invitation. I can't join this week but am looking forward to discussing it on next week's call. |
|
Thanks again @nikola-matic . This PR is a consequence of my work on a term paper for a Graduate class I'm taking. I was hoping for a quick review and merge to impress the grader :-) I absolutely want to to see the change through but I will de-prioritize it just a little given that I'm also scheduled to go help out the Math teacher in my daughter's class atelier at the same time as the call this week. I will join the call right after the New Year to discuss enabling the improvement by changing the Yul codegen directly. I'm all onboard making the right change in the right place, and then possibly looking at further, subsequent contributions. Happy Holidays! |
f3bb67b to
fb9dcbc
Compare
|
Hi again @nikola-matic . I've reworked the change as suggested and look forward to your further review. Approach reworked: dispatch fixed at the IR level instead of assembly peepholes I've dropped the peephole-based approach entirely in favor of a one-line change at the source of the pattern: IRGenerator::dispatchRoutine now emits gt(calldatasize(), 3) instead of iszero(lt(calldatasize(), 4)) as the function dispatch guard. The two are equivalent, but the new form needs one instruction less; every via-IR contract saves 3 gas per incoming external call and 1 byte of deployed code, with no pattern matching in the assembly optimizer. New semantic test fallback/short_data_reverts_without_fallback.sol pins the dispatch boundary for a contract without a fallback (0–3 byte calldata reverts, the exact 4-byte selector dispatches, an unknown 4-byte selector reverts), complementing the existing short_data_calls_fallback.sol. Test expectations regenerated: command-line tests (IR text, bytecode, source maps, CFG JSON) and semantic gas expectations (irOptimized and ssaCFGOptimized) |
fb9dcbc to
8a73136
Compare
|
t_native_test_ext_zeppelin is red on develop itself at the merged base (reviewers can verify via develop build 2084073) |
51788cc to
842ac99
Compare
msooseth
left a comment
There was a problem hiding this comment.
iszero(lt(x),4) means NOT(x < 4). gt (x, 3) means x>3.
So one is x >= 4, the other x > 3
LGTM
842ac99 to
7eddaaf
Compare
|
Sounds good, no rush on my end — happy to wait for @rodiazet's harness. For reference, the numbers currently in the description came from In the meantime I've tidied the branch up while it waits: it's now rebased onto current |
|
@blishko — happy to wait for @rodiazet's harness before merging. In the meantime, here are measurements that may be useful as a cross-check when it lands. Method. Built Result — uniform, not merely typical:
The corpus spans minimal contracts, an ERC-20, a proxy, libraries, inheritance, interfaces, overloads, custom errors, and string/struct/array parameters. The four controls are the interesting part: a library, a contract with only internal functions, one with only One gap: the proxy contract on the rejected-call path shows no difference. That is the harness, not the change. Correction to the PR description. It previously claimed 2 gas per external call. That was a measurement error on my part; the correct figure is 3, which is exactly what the instruction costs predict: I had also invented an explanation for the 1-gas gap ("real-world dispatcher structure absorbs it"), which was wrong — the guard is straight-line code with nothing available to absorb a gas unit. Description now updated. One note for the benchmarking harness. Comparing bytecode sizes across two builds of the compiler needs |
f9b21d7 to
ddffffc
Compare
ddffffc to
23cae11
Compare
Summary
The IR dispatcher emits a guard that skips the selector switch when calldata is too short to hold a function selector. It was spelled as a negated less-than:
This PR spells the same condition directly:
Both mean
calldatasize() >= 4. The second form drops theISZERO, so the guard assembles to four instructions instead of five.This is a one-line change in
IRGenerator::dispatchRoutine(). The bulk of the diff is updated test expectations.Motivation
The guard is emitted in the dispatcher of every contract compiled via IR that has at least one external function, so the saving applies broadly:
PUSH1 0x04/CALLDATASIZE/LT/ISZEROPUSH1 0x03/CALLDATASIZE/GTMeasured with
evmoneacross a 22-contract corpus: 3 gas per external call and 1 byteof runtime code, uniformly, in every compilation unit that emits a dispatcher. Contracts
with no external function cases — libraries, internal-only contracts, and contracts with
only
receive()/fallback()— are bit-identical between the two builds, since the guardsits inside the
<?+cases>conditional. See the measurement comment below for thebreakdown and method.
Equivalence
calldatasize()is unsigned, so over the full domain:Checked exhaustively with Z3:
Testing
Existing expectations across
cmdlineTests,semanticTestsandgasTestsare updated to reflect the new instruction sequence.One new test,
semanticTests/fallback/short_data_reverts_without_fallback.sol, pins the boundary the change moves. It walks 0/1/2/3-byte calldata (must revert), 4 bytes with a valid selector (must dispatch), and 4 bytes with an unknown selector (must revert). It fails in both directions if the bound slips:gt(calldatasize(), 4)stops the 4-byte call dispatching, andgt(calldatasize(), 2)lets a 3-byte call through, becausecalldataloadzero-pads and the truncated selector reads back as a valid one.The pre-existing
fallback/short_data_calls_fallback.solcovers only the contract-with-fallback path, andrevertStrings/unknown_sig_no_fallback.solis a single 1-byte case on a contract with areceive(), so the no-fallback boundary was previously uncovered.