Solidity – Why Gas Usage Differs on Mapping(int32 => Struct) vs Struct[]

gassolidity

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.

Best Answer

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.