It deprecates JUMP and JUMPI, allowing valid code to support streaming, one-pass, and other near-linear compilers.
In concert with EIP-3540 and EIP-3670 it ensures, at initialization time, that valid code will not execute invalid instructions or jump to invalid locations, will not underflow stack, will maintain consistent numbers of inputs and outputs for subroutines, and will have bounded stack height in the absence of recursion.
This is among the simplest possible proposals that meets these requirements.
September 9, 2022: The world keeps turning, and this proposal evolves with it. We can now have immediate data, relative jumps, and no more JUMP, JUMPI or JUMPDEST. So this draft removes BEGINSUB and returns to the original design of just two new opcodes, but with immediate data. It retains the validity constraints and algorithm adapted from EIP-615. Which means we can at long last write one-pass compilers.
I’m probably just being slow but I don’t understand why this should be adopted, and the provided rationale doesn’t clear anything up for me. If it’s already possible to implement subroutines then as inelegant as the current situation might be why introduce a native subroutine mechanism? It can be implemented more efficiently, letting contracts have a lower gas cost? It makes static analysis on the resulting contracts easier? It unlocks other future changes with cool benefits?
Yes, lower gas costs. And yes, easier static analysis - you can know that the code is a subroutine call or return, rather than try to work it out by pattern matching Solidity or other conventions.
Tomasz Kajetan Stańczak @tkstanczak 06:22 @gcolvin would be good to have a test for nested JUMPSUB and a JUMPSUB nested in a CALL invoked from inside the subtoutine
PUSH1 2 I assume. The second time this hits RETURNSUB it will pop the codesize left on the call stack and jump to the implicit 0 past the end of the code (at offset 5) and stop. It will never get to the STOP at offset 4.
Optimized code from the latest solc does a better job with the multiply() function, which is a leaf. Non-leaf functions remain costly to get out of, as shown by adding a layer to the test.
contract fun {
function multiply(uint x, uint y) public returns (uint) {
return x * y;
}
function test_mul(uint x, uint y) public returns (uint) {
return multiply(x,y);
}
function test(uint x, uint y) public returns (uint) {
return test_mul(2,3);
}
}
Here is what solc can do now with just jump:
1 MULTIPLY:
5 mul
3 swap1
8 jump
=
17 gas
1 TEST_MUL:
5 0x00
5 RTN
5 dup4
5 dup4
5 MULTIPLY
8 jump
=
34 gas
1 RTN:
3 swap4
3 swap3
2 pop
2 pop
2 pop
8 jump
=
21 gas (twice)
TEST:
5 0x00
5 RTN
5 0x02
5 0x03
5 TEST_MUL
5 jump
=
30 gas
123 gas TOTAL
But with jumpsub and returnsub only a third as much gas is needed.
1 MULTIPLY:
5 mul
3 returnsub
=
9 gas
1 TEST_MUL:
3 MULTIPLY
5 jumpsub
3 returnsub
=
12 gas
1 TEST:
3 0x02
3 0x03
3 TEST_MUL
5 jumpsub
3 returnsub
=
18 gas
39 gas TOTAL
Surprised to see the optimizer doing such a bad job. But yeah this test case shows exactly how the subroutine features would benefit sophisticated situations like this.
I haven’t tried to hand-optimize the Solidity output, but I don’t think that it could do much better. It’s intrinsically difficult to handle subroutines without instructions for the purpose, as the history of CPU development pretty clearly shows.
I’m not sure what’s sophisticated here – just two function calls and one multiplication.
Some diggings: On Remix it is using Solidity compiler’s legacy optimizer, which cannot handle functions with calls to other functions, hence the non-optimized TEST_MUL function.
Better to say, some kinds of static analysis, @axic, as I’ve heard complaints about the problem of deciphering subroutines in EVM code, and the EIP gives a good example. Other kinds analysis don’t care.
The reason none of this optimised is that all functions are marked public. That means all of them need to made available externally. In a more realistic example, most of those would not be marked public and then they become inlined or better optimised.