9

My goal is to iterate around 100 items or even more inside a mapped data stuct per a function call by using minimum gas. In order to achieve it I have used two different approaches.

In my case only empty function call costs 23499 Gas.

I have observed that to read a single mapped items costs around 303 gas<= (23802 - 23499) and a single push costs 25979 Gas <= (49478 - 23499) for the following code:

  struct Interval { 
    uint32 num;
    int32  core;
    uint32 next; 
  } 

  Interval[] list;
  function createList(){
      for(int i; i < 100; i++)
         list.push(Interval( { num: 10, core: 10, next: 10 }) );//some dummy push
  //to push a item costs 25979 gas. So 100 items => 100 x 25979 = 2597900 gas.
      }
  }

  function iterateList(){
     for(int i; i < 100; i++){
         list[i]; 
  //to read the list per item costs 303 gas. So 100 items => 100 x 303 = 30300 gas.
     }
  }

  function singlePush(){ //costs 49478 Gas
      list.push(Interval( { num: 30, core: 10, next: 10 }) );         
  }

  function singleRead(){ //costs 25979 Gas
      list[0];         
  }

  function emptyCall(){ //costs 23524 Gas 
  }

[Q] Why reading data is expensive? If I use assembly code to access to data would gas cost be cheaper?


When I update the code as follows(changing Interval[] list; with mapping(int32 => Interval) list;)... gas amount for reading and pushing data decreases dramatically. Now I have observed that to read a single mapped items costs around 25 gas <= (23524 - 23499) and single push costs 20545 Gas <= (44044 - 23499).

  struct Interval { 
    uint32 num;
    int32  core;
    uint32 next; 
  } 

  mapping(int32 => Interval) list;
  function createList(){
      for(int i; i < 100; i++)
          list[i] = Interval( { num: 10, core: 10, next: 10 });
   //to push a item costs 20545 gas. So 100 items => 100 x 25979 = 2054500 gas.
      }
  }

  function iterateList(){
     for(int i; i < 100; i++){
         list[i];
  //to access into list per item costs around 25 gas. So 100 items => 100 x 25 = 2500 gas.
     }
  }

  function singlePush(){ //costs 23524 Gas.
      list[0] = Interval( { num: 10, core: 10, next: 10 });        
  }

  function emptyCall(){ //costs 23499 Gas
  }

[Q] What may cause to have dramatic change on gas to read and push data based on the definition differ as follows: mapping(int32 => Interval) list vs Interval[] list.

I did ask multiple questions all in once but I believe that they all linked to each other.

  • Note that the first push to an array typically cost more than succeeding pushes, because it (also) changes array length from zero to non-zero. Si I recommend that for the sake of gas measuring, you skip the first push. – goodvibration May 13 at 14:35
  • But it will keep updating the array length, how much additional will it be different from zero to non-zero vs from non-zero to non-zero? @goodvibration – alper May 13 at 17:49
  • You can run your benchmarks and find out. It is quite significant as far as I recall. – goodvibration May 13 at 19:15
  • What version of solidity are you using? Are you using the optimizer? These contracts fail to compile for me using solidity 0.4.26 and 0.5.8. – JBrouwer May 15 at 17:13
  • Also about the gas reduction @alper as suggested by @goodvibration - every time you push you write to the length value of the array. This is 0 for the first push and will cost you 20k gas. Every time you update it it will cost 5k gas - but you should only update it once because all other gas will be wasted. Consider first updating the length (e.g. list.length += 100) and then writing these values. This saves you approx. 5k gas per write and lowers the avg. gas cost to about 20k per write instead of the 25k. – JBrouwer May 15 at 17:16
6
+50

A mapping is a namespace with 2^256 "slots" and no index. Items are stored in the namespace by index. You can think of it as a table, with 2^256 rows initialized to 0 values because it return 0 if nothing else was explicitly written to a given slot that gets inspected.

Deep down, a dynamic array is also organized this way but first it has a uint value for length. Each index is a uint <= length and if one inquires about a value out of range, it will revert.

To append to a dynamic array, first, the length is increased (length++), then the value is written to the new slot, so a read and two writes. For fixed-size arrays, the length is not adjusted but the range check still applies.

SSTORE writes a 32-byte word to storage, always in a slot in the Patricia trie at an address that is computed from storage layout for the contract. SSTORE costs are not consistent, so it is good to be aware of variance. The same function will produce different results depending on input and context.

As of Constantinople:

  • Overwrite same value: NOOP, 200 gas
  • Overwrite non-zero value: 5,000 gas
  • Overwrite zero value: 20,000 gas
  • Zero out non-zero value: -15,000 gas

So, the first time you list.push(), the length was 0 and then is not 0, so expect 20,000 gas for that part of the push plus another 20,000 to write a non-zero value. Later, the next length revision will cost 5,000 gas. Or, you can push(0) and that will be different again.

Mappings have no length to maintain, so they will always be cheaper for similar comparisons keeping the above-noted variances in mind.

Hope it helps.

  • Overwrite same value is 200 gas? I think this is the EIP which is removed from Constantinople as this introduced a re-entrancy bug in CALLs which sent the minimal amount of gas (2300)? eips.ethereum.org/EIPS/eip-1283 – JBrouwer May 15 at 17:02
  • If I'm not mistaken, this is what they settled on. I did quick sanity check in Remix and gas costs came out as expected. Sorry about the format. pragma solidity 0.5.1; contract ThisTest { bytes32 public a; function set(bytes32 b) public { a = b; } } – Rob Hitchens - B9lab May 15 at 17:14
  • This might be a solidity change then - if you write to a slot it first SLOADs it to check if you are writing an new value. If not then it SSTOREs the value. The gas costs of writing to a dirty map is 50 gas, not 200. SLOAD costs 200 gas currently. – JBrouwer May 15 at 17:17
  • See en.ethereum.wiki/roadmap#constantinople - the EIPs included in Constantinople do not change gas costs for storage reads/writes. – JBrouwer May 15 at 17:19
  • 1
    1283 is not live in Constantinople - check this news article from ethereum.org which states it is not in: blog.ethereum.org/2019/02/22/…. That pull request creates a new EIP (1706) which is currently proposed for Istanbul: github.com/ethereum/EIPs/blob/master/EIPS/eip-1679.md – JBrouwer May 16 at 17:40
-1

Think of mapping(int32 => Interval) as a fixed size array with all 2^32 elements initialized to 0. When you create a new one, you just change the values for that element

Interval[] is a dynamic size array, so with push you first increase it's size then add the new element, which requires more steps and gas. Reading also requires more gas, because there are more operations performed and each one takes some gas.

  • negative score is most likely for my English (sorry): – KNK May 17 at 18:31
  • 'You can think of it as a table, with 2^256 rows initialized to 0 values' same as 'fixed size array with all 2^32 elements initialized to 0' and the number 2^32 is more correct for int32 than 2^256, then the explanation for length (To append to a dynamic array, first, the length is increased) is similar to 'increase it's size' ... so the accepted answer is actually the same but in better wording. – KNK May 17 at 18:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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