9

When I add a view or pure function to a contract, it sometimes impacts the gas costs for unrelated write functions in the same contract - requiring another 22 gas.

pragma solidity ^0.5.0;

contract Test {
    function testWrite() public {}

    function get() public pure returns (bytes memory) {
       return bytes('yo');
    }
}

If you run this sample and call testWrite it will cost 144 gas. Now comment out the get function and try again, the cost to call testWrite reduces to 122. You can also copy paste the get method into get2 and see the impact grow with each instance (but not consistently):

  • no get: 122
  • 1 get: 144
  • 2 gets: 166
  • 3 gets: 188
  • 4 gets: 210
  • 5 gets: 232
  • 6 gets: 210 ! (diverge from pattern)
  • 7 gets: 210
  • 8 gets: 232
  • 9 gets: 232

I'm testing using Remix with optimization enabled, however this investigation begun because I saw the same in our Truffle project.

Why might this be happening? And is there a way to avoid the increased costs?

5
  • It's most likely related to how Solidity tries to pack (optimize) the code by combining separate functionality into a single entity to save space. But can't tell you any details in this case as I'm not so familiar with that. Mar 8, 2019 at 18:44
  • I thought that could be the case as well, but then I would expect the impact to always be roughly +/- 22... the fact that this stacks with each new view method has me wondering if there's something else at play. Mar 8, 2019 at 18:51
  • 1
    Just a guess, but Solidity basically emits a switch statement that examines the first four bytes of the call data to determine which function you're calling. (Is it 0x12345678? Okay, then jump here. No? Then is it 0xabcdef89? Then jump here. No? ...) Depending on the order of the functions, each one adds a cost overhead to the ones "below" it. You'd have to look at the emitted byte code to be sure.
    – user19510
    Mar 8, 2019 at 18:57
  • Compelling... you may be right @smarx. I'm not sure how to confirm though. Mar 8, 2019 at 21:44
  • Welcome to the SE, great question! Mar 9, 2019 at 19:49

1 Answer 1

9

As @smarx said, the increasing gas cost is because of the larger function dispatch table.

You can verify this in Remix by stepping through the execution in the debugger. Here's a screenshot of the contract and how to get to the execution debuggerenter image description here

You'll notice that the contract bytecode has a section that looks like this (annotations mine)

0026 PUSH1 00
0028 CALLDATALOAD (Push the first word of calldata onto the stack)
0029 PUSH1 e0
0031 SHR          (Shift the calldata right by 0xe0 = 224 bits = 28 bytes. This leaves just the function selector on the stack)
0032 DUP1         (Duplicate the function selector on the stack)
0033 PUSH4 995ab18c
0038 GT           (Check if 0x995ab18c is greater than selector)
0039 PUSH2 005b
0042 JUMPI        (If so, jump to location 91)
0043 DUP1
0044 PUSH4 995ab18c  (bytes4(sha3("get4()"))
0049 EQ           (Check if selector = 995ab18c)
0050 PUSH2 0299   (If so, jump to the beginning of the function)
0053 JUMPI
0054 DUP1
0055 PUSH4 c88a82d1   (get6)
0060 EQ
0061 PUSH2 031c
0064 JUMPI
0065 DUP1
0066 PUSH4 d2178b08 
0071 EQ
0072 PUSH2 039f
0075 JUMPI
0076 DUP1
0077 PUSH4 fb2f26d6  (testWrite)
0082 EQ
0083 PUSH2 0422
0086 JUMPI
0087 PUSH2 0088
0090 JUMP
0091 JUMPDEST
0092 DUP1
0093 PUSH4 054c1a75
0098 EQ
0099 PUSH2 008d
0102 JUMPI
0103 DUP1
0104 PUSH4 11694354
0109 EQ
0110 PUSH2 0110
0113 JUMPI
0114 DUP1
0115 PUSH4 8321045c
0120 EQ
0121 PUSH2 0193
0124 JUMPI
0125 DUP1
0126 PUSH4 90bb7b44
0131 EQ
0132 PUSH2 0216
0135 JUMPI

You can see that the selectors are ordered by their selector, with some optimization by doing a little bit of a binary search. This explains the non-strictly increasing behavior of the gas cost.

This is actually a very good question, and brings up a natural optimization; placing pure functions at the end of the table would reduce gas costs for functions that are expected to be called on-chain. This has a trade-off in that it prevents any sort of elegant binary search as the number of functions increases.

EDIT: I opened an issue proposing an optimization which should address this.

5
  • Whoah. Someone please write an article somewhere about this an its implications. Mar 9, 2019 at 20:09
  • Great answer, thank you. Agree with @LauriPeltonen, an article diving into best practices related to this would be helpful. There are many dapps desperate to minimize gas costs... how do we best do that without artificially limiting read-only functions (often added for convenience, i.e. providing multiple ways to access the same data)? Mar 9, 2019 at 20:16
  • 2
    I think this is something best handled by a compiler optimization. It's very possible to make two separate dispatch tables with all constant functions in the second Mar 9, 2019 at 20:21
  • Additionally if there were a way to define a strict sort order - or maybe just a high priority indicator - that would be very helpful. Many dapps are most interested in just a few important calls, everything else can pay a little more but those primary calls must be as cheap as possible. Mar 9, 2019 at 20:25
  • 1
    There is discussion of that here: github.com/ethereum/solidity/issues/4858#issuecomment-415744724 Mar 9, 2019 at 20:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.