Skip to content

Libevmasm: Remove trivial assembly blocks#16578

Draft
blishko wants to merge 1 commit intodevelopfrom
trivial-block-removal
Draft

Libevmasm: Remove trivial assembly blocks#16578
blishko wants to merge 1 commit intodevelopfrom
trivial-block-removal

Conversation

@blishko
Copy link
Copy Markdown
Contributor

@blishko blishko commented Apr 6, 2026

Summary

Assembly that falls out of either legacy or IR pipeline can contain what I would call a "trivial block".
This PR adds functionality to the evmasm optimizer to detect and remove trivial blocks.

Trivial block

The following is an example of a trivial block:

tag_11:
      tag_1
      jump	// in

Upon entering this block, the only thing the code does is jump immediately to another block.
Note that here I assume that it is not possible to "fall-through" to this block, i.e., the instruction above this block is a jump to a different block.

Full example

Full example with Solidity code and assembly output
contract C {
    uint s;
    bool x;
    function f(uint y) public returns (uint){
        y = increment(y);
        y = increment(y);
        return y;
    }

    function increment(uint y) internal returns(uint) {
        unchecked{
            ++y;
            s = 2 * y;
            y = x ? s : s + 1;
            return y;
        }
    }
}
solc --asm --via-ir --optimize
EVM assembly:
  0x80
  dup1
  0x40
  mstore
  jumpi(tag_1, callvalue)
  dataSize(sub_0)
  swap1
  dup2
  dataOffset(sub_0)
  dup3
  codecopy
  return
tag_1:
  0x00
  dup1
  revert
stop

sub_0: assembly {
      0x80
      dup1
      0x40
      mstore
      jumpi(tag_2, iszero(lt(calldatasize, 0x04)))
      0x00
      dup1
      revert
    tag_2:
      jumpi(tag_4, eq(0xb3de648b, shr(0xe0, calldataload(0x00))))
      0x00
      dup1
      revert
    tag_4:
      jumpi(tag_8, callvalue)
      jumpi(tag_8, slt(add(not(0x03), calldatasize), 0x20))
      0x20
      swap1
      tag_10
      tag_11
      calldataload(0x04)
      tag_1
      jump	// in
    tag_11:
      tag_1
      jump	// in
    tag_10:
      dup2
      mstore
      return
    tag_8:
      0x00
      dup1
      revert
    tag_1:
      0x01
      add
      0x01
      shl
      dup1
      0x00
      sstore
      jumpi(tag_12, eq(0x00, and(sload(0x01), 0xff)))
      swap1
      jump	// out
    tag_12:
      0x01
      add
      swap1
      jump	// out

    auxdata: 0xa264697066735822122040ac6f350196276c516a6ad5214d63fdee490c13572be8ace303ddb518b8dcee64736f6c637828302e382e33352d63692e323032342e31302e33302b636f6d6d69742e34343933363365372e6d6f640059
}

Note that tag_11 is a trivial block.

Proposed change

Trivial blocks can be deleted from the assembly, after we have updated all references to the trivial block with the reference to its successor.

Gas effects

On our test suite, the results are encouraging.
In a lot of cases we see a small improvement. On a few cases this change yields relatively significant improvement.

Open questions

  • Is the implementation sound, i.e., is there some case where we cannot remove a block, but we don't rule it out at the moment?

@blishko blishko marked this pull request as ready for review April 6, 2026 13:42
@blishko blishko marked this pull request as draft April 6, 2026 20:09
@blishko blishko force-pushed the trivial-block-removal branch 4 times, most recently from de8d1a0 to 5f6edef Compare April 8, 2026 16:16
@cameel
Copy link
Copy Markdown
Collaborator

cameel commented Apr 8, 2026

I think that what you're proposing here is the exact case I analyzed in moh-eulith#1 (comment).

In short, such an optimization is doable, but questionable. It requires dynamic jumps (which would be a problem if the EVM finally introduced them; this was more of a concern while there was still hope for EOF) and it erases boundaries between functions. The latter is why it can't be done at Yul level and probably why it interferes with inliner as you've noticed. Perhaps it could be done on the SSA-CFG representation though and, if so, that would be a much better place for it. I don't think it can be done reliably on evmasm level.

We discussed it on the 2025-06-18 design call, so far without any decisions. I wanted to get back to it later, though other things keep getting in the way and this does not seem very high priority given the downsides.

@blishko blishko force-pushed the trivial-block-removal branch from 592e589 to 940ea6e Compare April 9, 2026 11:11
@blishko
Copy link
Copy Markdown
Contributor Author

blishko commented Apr 9, 2026

@cameel, thanks for all this.

I think that what you're proposing here is the exact case I analyzed in moh-eulith#1 (comment).

Yes, I think it is the same thing, though I really want to address only the simplest case, where the block really consists only of PUSH TAG and JUMP.
From what I have seen this is actually not that rare and it seems to occur also in cases other than those mentioned in your notes and discussions.
It also appears in all three codegen pipelines, as you can see, e.g., in externalContracts/base64.sol.

In short, such an optimization is doable, but questionable. It requires dynamic jumps (which would be a problem if the EVM finally introduced them; this was more of a concern while there was still hope for EOF) and it erases boundaries between functions. The latter is why it can't be done at Yul level and probably why it interferes with inliner as you've noticed. Perhaps it could be done on the SSA-CFG representation though and, if so, that would be a much better place for it. I don't think it can be done reliably on evmasm level.

I think here we are not on the same page. Maybe I am missing something. What do you mean by dynamic jumps?
Thinking on the EVM level, there are no functions. I just want to replace all references of a tag with another tag and then delete the unreachable block.

I think the potential loss of a debug info is a valid concern, though if debug information can survive inlining and block deduplication, I don't see why it couldn't survive also this transformation. Though I don't know about how debug information is propagated so I will be happy to learn more if my intuition is wrong.

@blishko blishko force-pushed the trivial-block-removal branch from 940ea6e to c0871b0 Compare April 9, 2026 12:33
@blishko blishko force-pushed the trivial-block-removal branch from c0871b0 to 2270fec Compare April 9, 2026 12:35
}
}

if (_settings.runInliner && !m_eofVersion.has_value())
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am using runInliner setting here to minimize the code changes, although if this would be accepted, it would probably need its own setting flag.

@blishko
Copy link
Copy Markdown
Contributor Author

blishko commented Apr 9, 2026

After moving this pass after JumpdestRemover and BlockDeduplicator, I believe there are no regressions anymore.
Every change is an improvement in gas usage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants