Assignment - Storage Design

Let’s compare the Gas cost between 2 different designed contracts with the same functionality.

It’s your assignment to create a contract where I can store this struct.

struct Entity{
    uint data;
    address _address;
}

In one of the contracts, I want you to use only a mapping.
In the other contract, I want you to use only an array.

The contract should have 2 functions

addEntity(). Creates a new entity for msg.sender and adds it to the mapping/array.
updateEntity(). Updates the data in a saved entity for msg.sender

After you have built the 2 contracts, I want you to answer the following questions.

When executing the addEntity function, which design consumes the most gas (execution cost)? Is it a significant difference? Why/why not?
Add 5 Entities into storage using the addEntity function and 5 different addresses. Then update the data of the fifth address you used. Do this for both contracts and take note of the gas consumption (execution cost). Which solution consumes more gas and why?

10 Likes

My Contracts Solution

Contracts
pragma solidity 0.8.0;

//In one of the contracts, I want you to use only a mapping.
contract StorageDesign_Mapping {
  struct Entity{
    uint data;
    address _address;
  }

  mapping(address => Entity) EntityMap;

  // Creates a new entity for msg.sender and adds it to the mapping.
  // @uint _data, value that will be added
  function addEntity(uint _data) public returns(bool success) {
    EntityMap[msg.sender].data = _data;
    EntityMap[msg.sender]._address = msg.sender;
    return true;
  }
  // Updates the data in a saved entity for msg.sender
  // @uint _data, value that will be updated
  function updateEntity(uint _data) public returns(bool success) {
    EntityMap[msg.sender].data = _data;
    return true;
  }

  // Just to validate data after update values.
  function getEntityMap() public view returns(uint, address){
    return(EntityMap[msg.sender].data, EntityMap[msg.sender]._address);
  }

} //END StorageDesign_Mapping

//In the other contract, I want you to use only an array.
contract StorageDesign_Array {

  struct Entity{
    uint data;
    address _address;
  }

  Entity[] EntityArray;

  // Creates a new entity for msg.sender and adds it to the array.
  // @uint _data, value that will be added
  function addEntity(uint _data) public returns(bool success) {
    Entity memory newEntity;
    newEntity.data = _data;
    newEntity._address = msg.sender;
    EntityArray.push(newEntity);
    return true;
  }
  // Updates the data in a saved entity for msg.sender
  // @uint _data, value that will be updated
  function updateEntity(uint _index, uint _data) public returns(bool success) {
    require(EntityArray[_index]._address == msg.sender, "Only entity owner can update values.");
    EntityArray[_index].data = _data;
    return true;
  }

  // Just to validate data after update values.
  function getEntityArr() public view returns (Entity[] memory) {
    return EntityArray;
  }
} //END StorageDesign_Array

After you have built the 2 contracts, I want you to answer the following questions.

1. When executing the addEntity function, which design consumes the most gas (execution cost)?

On mapping: (execution cost: 41656 gas)

On Array: (execution cost: 62607 gas)

2. Is it a significant difference? Why/why not?
Mapping saved around 30% against Array in gas costs.

3. Add 5 Entities into storage using the addEntity function and 5 different addresses. Then update the data of the fifth address you used. Do this for both contracts and take note of the gas consumption (execution cost). Which solution consumes more gas and why?

On mapping: (execution cost: 5717 gas)

On Array: (execution cost: 8481 gas)

Although I used a require statement, it does not increase gas consumption, but for the sake of the assignment, I just commented it.

Carlos Z
(when moon, sensei?)

8 Likes

Gas cost comparison Array and Mapping Storage Design

Mapping Storage:

  • Deployment Cost: 197438 gas

  • Add Entity Cost: 41656 gas

  • Update Entity Cost: 5717 gas

Array Storage:

  • Deployment Cost: 372811 gas

  • Add Entity Cost: 62408 gas for first entry in Entity Array! Every additional entry 47408 gas

  • Update Entity Cost: 7736 gas

Conclusion:

The ArrayStorageAssignment contract is 1.88x higher in deployment cost compared to the MappingStorageAssignment. This is due to the extra space used by not only saving data in a struct but further tying structs together in an array. Also, the addEntity function uses up more space, it is noticeable though, that the first push to the array is more expensive than every additional entry pushed to it. Further, updateEntity function for ArrayStorage is also 1.3x more expensive than update entity for MappingStorage. The mapping is, in regards to cost, preferable because we access the struct directly without referring to separate storage first.

MappingStorageAssignment.sol
pragma solidity 0.8.0;

contract MappingStorageAssignment{ //deployment cost 197438 gas
    
    struct Entity{
        uint data;
        address _address;
    }

    
    mapping (address => Entity) entityMap;
    
    function addEntity(uint data) public returns(bool success){ //execution cost: 41656 gas
        entityMap[msg.sender].data = data;
        entityMap[msg.sender]._address = msg.sender;
        return true;
    }
    
    function updateEntity(uint data) public returns(bool success){ //execution cost 5717 gas
        entityMap[msg.sender].data = data;
        return true;
    }
    
    function showEntity() public view returns(uint data, address _entityAddress){
        return (entityMap[msg.sender].data, entityMap[msg.sender]._address);
    }
    
    
}
ArrayStorageAssignment.sol
pragma solidity 0.8.0;

contract ArrayStorageAssignment{ //cost for deployment 372811 gas 
    
    struct Entity{
        uint data;
        address _address;
    }
    
    Entity[] public entityList;
    
    function addEntity(uint data) public { //execution cost: Entity 1: 62408 gas, every additional Entity 47408 gas
        Entity memory newEntity;
        newEntity.data = data;
        newEntity._address = msg.sender;
        entityList.push(newEntity);
    }
       
    function updateEntity(uint row, uint data) public returns(bool success){ //execution cost update: 7736 gas gas, 
        require(row <= entityList.length-1, "There is no entry in this row!");
        entityList[row].data = data;
        return true;
    }
    
        function showListLength() public view returns(uint){
        return entityList.length;
    }
    
    function showEntry(uint row) public view returns(uint data, address _address){
        require(row <= entityList.length-1, "There is no entry in this row!");
        return (entityList[row].data, entityList[row]._address);
    }
}
6 Likes

Here is my solution:

contract arrayStorage {
    struct Entity{
        uint data;
        address _address;
    }
    Entity[] entityArray;

    function addEntity(uint _data) public returns(bool success){
        Entity memory newEntity;
        newEntity.data = _data;
        newEntity._address = msg.sender;
        entityArray.push(newEntity);
        return true;
    }

    function updateEntity(uint _index, uint _data) public returns(bool success) {
        entityArray[_index].data = _data;
        return true;
    }
}

contract mappingStorage {
    struct Entity{
        uint data;
        address _address;
    }
    mapping (address => Entity) entityMapping;

    function addEntity(uint _data) public returns(bool success){
        Entity memory newEntity;
        newEntity.data = _data;
        newEntity._address = msg.sender;
        entityMapping[msg.sender] = newEntity;
        return true;
    }

    function updateEntity(address _user, uint _data) public returns(bool success){
        entityMapping[_user].data = _data;
        return true;
    }
}

1. When executing the addEntity function, which design consumes the most gas (execution cost)? Is it a significant difference? Why/why not?

The array design used 62585 gas to execute addEntity(), while the mapping design used 41749 gas to execute addEntity(). This is a significant difference of around with the mapping function using about a third less gas than they array function.

2. Add 5 Entities into storage using the addEntity function and 5 different addresses. Then update the data of the fifth address you used. Do this for both contracts and take note of the gas consumption (execution cost). Which solution consumes more gas and why?

The array design used 6692 gas to update the data of the 5th address used, while the mapping design only used 5918 gas to accomplish the same thing. I’m still a little fuzzy as to why this is but I’m pretty sure it is because arrays are indexed and therefore it takes more processing power to traverse the array and arrive at the correct index. Mappings on the other hand are collections of key/value pairs and non-indexed, so they are much more efficient at value retrieval.

@AdamFortuna, am I on the right track with this line of reasoning?

5 Likes

Yes, you are in the right track, Great work. :clap:

Carlos Z.

1 Like

Ok so here is my take on this assignment.
I actually made 3 contracts
1 with a mapping
1 with an array

and then another one with an array but with another implementation that did not exactly adhere to the requirements of the assignment.
The requirement was to only have 2 methods so the first array contract had too use loops when updating the entry.

The final array solution uses a little trick I though of.

For all of them to really see how the fees changed over time I ran both addEntry and updateEntry 10 times each (on 10 different accounts).

But lets start with the code of the mapping contract:

Mapping Contract

pragma solidity 0.7.4;

// Definition of contract with mapping storage
contract StorageDesignMapping {

  // Define the struct
  struct Entity{
    uint data;
    address _address;
  }

  // Define the mapping ofthe entities
  mapping(address => Entity) Entities;
  
  // Function to add / Set the data of a user
  function addEntity(uint _data) public {
      
      // Create a new object for the user with the given data
      Entities[msg.sender] = Entity(_data, msg.sender);
  }
  
  // Function to update the data of a user
  function updateEntity(uint _data) public {
      
      // Checck so that the user already has some data
      require(Entities[msg.sender]._address == msg.sender);
      
      // Assign the new provided data withough overwriting anything that may be added in the future
      Entities[msg.sender].data = _data;
  }
}

Pretty straight forward…
Both the addEntry and the update entry are executed in constant time:
O(1)
Lets look at the fees:

Deployment

Transaction cost: 219 943
Execution cost: 124 771
Total cost: 344 714

AddEntry

Transaction cost: 62 726
Execution cost: 41 272
Total cost: 103 998
These numbers were the same for all the 10 transactions I made while testing.

UpdateEntry

Transaction cost: 27 766
Execution cost: 6 302
Total cost: 34 068
These numbers were the same for all the 10 transactions I made while testing.

Lets continue with the array contract
Array Contract

pragma solidity 0.7.4;

// Define the contract
contract StorageDesignArray {

  // Define the entity struct
  struct Entity{
    uint data;
    address _address;
  }

  // Define Entity array
  Entity[] Entities;
  
  // Function to add / Set the data of a user
  function addEntity(uint _data) public {
      
      // Declare empty entity
      Entity memory entity;
      
      // Assign values
      entity.data = _data;
      entity._address = msg.sender;
      
      // Push the new entity to the array
      Entities.push(entity);
  }
  
  // Function to update the data of a user
  function updateEntity(uint _data) public returns (bool){
      // Loop is used instead of a preknown index since the assignment required the contract to only have the add and update method.
      // Therefore there is no way of knowing the index beforehand and a loop is needed and will cost a lot of gas
      
      // Loop through the Entities
      for(uint i = 0; i < Entities.length; i++){
          
        // Continue if entry does not belong the the user
        if (Entities[i]._address != msg.sender) continue;
        
        // If we get here then the current entry belongs to the user so we set the data and return true
        Entities[i].data = _data;
        return true;
      }
      
      // If we get here then the user is not in the array so we return false. (We could also call addEntity with the data parameter to ensure that the data is added either way...)
      return false;
  }
}

Note that I tried to interpret the assignment strictly and only use the two specified methods.
This means that while the performance of the addEntry remains the same:
O(1), I have to use a loop in the update function to find the correct user which results in linear time:
O(n).

This means that for each entry the fees will increase more and more the newer the user.
So lets look at the fees:

Deployment

Transaction cost: 236 671
Execution cost: 137 783
Total cost: 374 454

This is an increase of 8,6% over the deployment of the mapping contract.

AddEntry

Transaction cost: 68 672
Execution cost: 47 208
Total cost: 115 880

While this cost was constant no matter how many times I made the transaction it turned out to be 11,42% more expensive than the mapping.

UpdateEntry

Call number 1

Call number 10

BOOM.
Turns out that for each entry that had to be searched an increase of about 25% from the previous index.

So we have the lowest transaction fee of: 26 107
And a high of : 49 885

And for the execution cost we have a low of 4643 and a high of 28 421.

that gives us a combined low of 30750 and a high of 78 306
And this was for only 10 entries. Imagine the cost of looping through an array of 1 000 000!!!
So the takeaway is that a search using loops is a no no…

Lets look at my final idea, although a bit hacky.

My idea

pragma solidity 0.7.4;

// Define the contract
contract StorageDesignArray2 {
    
  // Define the entity struct
  struct Entity{
    uint data;
    address _address;
  }

  // Define Entity array
  Entity[] Entities;
  
  
  // Function to add / Set the data of a user
  function addEntity(uint _data) public {
      
      // Declare empty entity
      Entity memory entity;
      
      // Assign values
      entity.data = _data;
      entity._address = msg.sender;
      
      // Push the new entity to the array
      Entities.push(entity);
  }
  
  // Function to update the data of a user
  function updateEntity(uint _index,  uint _data) public{
      // Check so that the given index is within the range of the array and that the corresponding entry in fact belongs to the user
      require((Entities.length > _index) && Entities[_index]._address == msg.sender, "Invalid user");
      
      // Set the data
      Entities[_index].data = _data;
  }
  
  // Function to retrieve the id (index) of the current user
  // This should be a free call since it is both external and view
  function getUserId() external view returns (uint){
      
      // Loop through the array (we don't care that it costs performance since it is probably free anyways)
      for(uint i = 0; i < Entities.length; i++){
        
        // Continue of the entry does not match the user
        if (Entities[i]._address != msg.sender) continue;    
        
        // If we get here then we found the id so we return the index
        return i;
      }
      
      // Throw some kind of error to signal that the user was not found
      revert("User not found");
  }
}

So the only new thing here is that we have introduced a new function: getUserId, that is completely free to call (as long as the caller is not another contract) even if it uses loops since it is external and is a view function.
This way the front end can just make two calls, first a call to get the id (index) of the user. and then make a call to a updated updateEntry function that requires the index (from the first free call).
This results in a performance of constant time: O(1) for the updateEntry even if the contract uses an array.

So lets compare the fees:

Deployment

Transaction cost: 331 585
Execution cost: 213 457
Total cost: 545 042

OK, so we have a much higher initial deployment cost. But hey, there is a third method here also so we are not comparing apples to apples here…

AddEntry

Transaction cost: 68 694
Execution cost: 47 230
Total cost: 115 924

So the cost is pretty much identical to the addEntry on the previous array contract (which is no surprise since the method is exactly the same)

UpdateEntry


So if we assume that the caller is not another contract and that the caller have previously called the getUserId function (for free) we get the following costs:

Transaction cost: 30 452
Execution cost: 8 796
Total cost: 39 248

This means that it is marginally more expensive per transaction than the mapping solution but have the advantage of later being able to have functions for calculating the sum of the data and so forth (for free) that could never be possible with the mapping solution.

Any thoughts on this?
Is it bad practice or just mean to use several free calls to get the needed data for the making calls that would otherwise be very expensive?

11 Likes

When executing the addEntity function, which design consumes the most gas (execution cost)? Is it a significant difference? Why/why not?

Execution cost of adding single entry in ArrayVersion: 47286
Execution cost of adding single entry in MappingVersion: 20493

ArrayVersion is 2.3 times as expensive as MappingVersion

It is significant but in absolute terms not that much, if repeated many times it will add up.

Add 5 Entities into storage using the addEntity function and 5 different addresses. Then update the data of the fifth address you used. Do this for both contracts and take note of the gas consumption (execution cost). Which solution consumes more gas and why?

Execution cost of updating fifth entry in ArrayVersion: 16915
Execution cost of updating fifth entry in MappingVersion: 5515

ArrayVersion is 3.1 times as expensive as MappingVersion

This is because the ArrayVersion needs to iterate through the end of the array to find the last entry. If the array gets bigger the cost will grow considerably.

Code used for ArrayVersion:

pragma solidity 0.8.0;
pragma abicoder v2;

contract ArrayVersion {
    
    struct Entity{
    uint data;
    address _address;
    }
    
    Entity[] EntityStore;
    
    function addEntity(uint entityData) public {
        EntityStore.push(Entity(entityData, msg.sender));
    }
    
    function updateEntity(uint entityData) public {
        //iterate through array
        uint arrayLength = EntityStore.length;
        for (uint i=0; i<arrayLength; i++) {
            // check if we've found our transaction
            if (EntityStore[i]._address == msg.sender){
              EntityStore[i].data = entityData;
            }
        }
    }
}

For MappingVersion:

pragma solidity 0.8.0;
pragma abicoder v2;

contract MappingVersion {
    
    struct Entity{
    uint data;
    //no need to store address in struct but left it here to keep identical
    address _address; 
    }
    
    mapping (address => Entity) private EntityStore;
    //Entity[] EntityStore;
    
    function addEntity(uint entityData) public {
        EntityStore[msg.sender].data = entityData;
    }
    
    function updateEntity(uint entityData) public {
        EntityStore[msg.sender].data = entityData;
    }
    
}
2 Likes

Array pattern:

pragma solidity 0.7.5;

contract StorageArray {
    struct Entity {
        uint data;
        address _address;
    }
    Entity[] public entities;
    
    function addEntity(uint _data) public {
        entities.push(Entity(_data, msg.sender));
    }
    
    function updateEntity(uint data) public {
        for (uint i = 0; i < entities.length; i++) {
            if (entities[i]._address == msg.sender) {
                entities[i].data = data;
                break;
            }
        }
    }
}

Mapping pattern:

pragma solidity 0.7.5;

contract StorageMapping {
    struct Entity {
        uint data;
    }
    mapping(address=>Entity) entities;
    
    function addEntity(uint _data) public {
        entities[msg.sender].data = _data;
    }
    
    function updateEntity(uint _data) public {
        entities[msg.sender].data = _data;
    }
}

The array storage pattern consumes significantly the most gas on add, presumably because it has to dynamically add the new element to the array.

The array pattern also consumes the most gas on update, because the array is iterated to find the entity with the corresponding address, unlike the map which directly indexes the entity using the address as key

Results:

  • array add initial: tx 83572, execution 62108
  • array add subsequent: tx 68572, execution 47108
  • array update 1: tx 30245, execution 8781
  • array update 2: tx 32879, execution 11415
  • array update 3: tx 35513, execution 14049
  • array update 4: tx 38147, execution 16683
  • array update 5: tx 40781, execution 19317
  • map add: tx 41779, execution 20315
  • map update: tx 26801, execution 5337
2 Likes

This is my code:

pragma solidity 0.8.1;

contract arr{
    
    struct Entity{
    uint data;
    address _address;
    }
    
    Entity[] Entities;
    
    function addEntity(uint entityData, address entityAddress) public {
    Entity memory newEntity;
    newEntity.data    = entityData;
    newEntity._address = entityAddress;
    Entities.push(newEntity);
  }
    
    function updateEntity(uint index, uint entityData, address entityAddress) public {
        Entities[index].data = entityData;
        Entities[index]._address = entityAddress;
    }
    
}


contract map{
    
    struct Entity{
    uint data;
    address _address;
    }
    
    mapping(address => Entity) Entities;
    
    function addEntity(uint entityData, address entityAddress) public {
        Entity memory newEntity;
        newEntity.data    = entityData;
        newEntity._address = entityAddress;
        Entities[entityAddress] = newEntity;
    }
    
    function updateEntity(uint entityData, address entityAddress) public {
        Entities[entityAddress].data = entityData;
    }
        
    
}

Even though the Array version is over simplified (it doesn’t keep track of the indexes for each address), it turned out to be 17% more expensive in transaction and execution costs to add the 5 Entities and update the last one.

In detail, adding the first Entity was 39% more expensive in the array, however in the next four added entities, the difference was only 11%. I think this may have to do with the cost of initializing the array when adding the first Entity.

Updating the last Entity was 21% more expensive in the array.
As said before, the overall cost was 17% higher to perform all of this operations in the array. Considering that a better implementation for the array would need to also keep track of the indexes for each address (which would increase the costs) we can conclude that to have these same functionalities, the mapping is considerably cheaper to run.

3 Likes

mappingTest.sol

pragma solidity 0.7.5;

contract mappingTest{
    
    struct Entity{
        uint data;
    }
    
    mapping(address=>Entity) public entities;
    
    function addEntity(uint _data) public returns (bool){
        
        Entity memory newEntity;
        newEntity.data = _data;
        
        entities[msg.sender] = newEntity;
        
        return true;
    }
    
    function updateEntity(uint _data) public returns(bool) {
        entities[msg.sender].data = _data;
        
        return true;
    }
}

arrayTest.sol

pragma solidity 0.7.5;

contract arrayTest{
    
    struct Entity{
        uint data;
        address _address;
    }
    
    Entity[] public entities;
    
    function addEntity(uint _data) public returns (uint){
        Entity memory newEntity;
        newEntity.data = _data;
        newEntity._address = msg.sender;
        entities.push(newEntity);
        return entities.length - 1;
    }
    
    function updateEntity(uint _data) public returns (bool) {
        for(uint i = 0; i<= entities.length - 1; i++) {
            if (entities[i]._address == msg.sender) {
                entities[i].data = _data;
                break;
            }
        }
        
        return true;
        
    }
}

Questions

  1. When executing the addEntity function, which design consumes the most gas (execution cost)?

The Array consumed more gas.

Mapping - Add Entity:

  • Transaction cost: 41985
  • Execution cost: 20521

Array - Add Entity:

  • Transaction cost: 84555
  • Execution cost: 63091
  1. Is it a significant difference? Why/why not?

It is a significant difference, since the Array cost 3 times more with the Execution cost, and the double with the Transaction cost.

  1. Add 5 Entities into storage using the addEntity function and 5 different addresses. Then update the data of the fifth address you used. Do this for both contracts and take note of the gas consumption (execution cost). Which solution consumes more gas and why?

The array solution spent more gas because it has more process to perform. It has to loop through all the array to find the address and update it.

Array: Update 5th Entity:

  • Transaction cost: 40882
  • Execution cost: 19418

Mapping: Update 5th Entity:

  • Transaction cost: 26901
  • Execution cost: 5437
3 Likes

When executing the addEntity function, which design consumes the most gas (execution cost)? Is it a significant difference? Why/why not?

Mapping addEntity() execution costs 42265. Array addEntity() execution costs 67697. Adding entity costs more using array than mapping by a third.

Add 5 Entities into storage using the addEntity function and 5 different addresses. Then update the data of the fifth address you used. Do this for both contracts and take note of the gas consumption (execution cost). Which solution consumes more gas and why?

Mapping updateEntity() execution costs 6326. Array updateEntity execution costs 34782. Array costs significantly more to update since gas must be spent heavily on each iteration of for loop iterating through entire array to find correct address to update data.

storage-design-mapping.sol

pragma solidity 0.7.5;

contract EntityMapping {
    
    struct Entity {
        uint data;
        address _address;
    }
    
    mapping (address => Entity) public entityMap;
    
    function addEntity(uint _data) public returns (uint) {
        entityMap[msg.sender]._address = msg.sender;
        entityMap[msg.sender].data = _data;
        
        return entityMap[msg.sender].data;
    }
    
    function updateEntity(uint _data) public returns (uint) {
        entityMap[msg.sender].data = _data;
        
        return entityMap[msg.sender].data;
    }
    
    function getEntity() public view returns (uint) {
        return entityMap[msg.sender].data;
    }
    
    function getAddress() public view returns (address) {
        return msg.sender;
    }
}```

storage-design-array.sol

pragma solidity 0.7.5;
pragma abicoder v2;

contract EntityArray {

struct Entity {
    uint data;
    address _address;
}

Entity[] public entityArray;

function addEntity(uint _data) public returns (uint, address) {
    entityArray.push(Entity(_data, msg.sender));
    return (entityArray[entityArray.length-1].data, entityArray[entityArray.length-1]._address);
}

function updateEntity(uint _data) public returns (uint, address) {
    for (uint i = 0; i < entityArray.length-1; i++) {
        if(entityArray[entityArray.length-1]._address == msg.sender) {
            entityArray[entityArray.length-1].data = _data;
        }
    }
    
    return (entityArray[entityArray.length-1].data, entityArray[entityArray.length-1]._address);
}

function getEntity() public view returns (uint, address) {
    for (uint i = 0; i < entityArray.length-1; i++) {
        if(entityArray[entityArray.length-1]._address == msg.sender) {
            return (entityArray[entityArray.length-1].data, entityArray[entityArray.length-1]._address);
        }
    }
}

function getArray() public view returns (Entity[] memory) {
    return entityArray;
}

}

2 Likes

1. When executing the addEntity function, which design consumes the most gas (execution cost)? Is it a significant difference? Why/why not? Inserting into a mapping is an O(1) operation whereas inserting into an array is an O(n) operation.

Mapping

  • addEntity: 41749

  • updateEntity: 5717

Array

  • addEntity: 62485

  • updateEntity: 9147

The array design consumes 49.67% more gas than the mapping design. This is a signification increase, because if we start adding more entities into the array design, the gas prices will keep adding up.

2. Add 5 Entities into storage using the addEntity function and 5 different addresses. Then update the data of the fifth address you used. Do this for both contracts and take note of the gas consumption (execution cost). Which solution consumes more gas and why?

  • Mapping : 5717

  • Array : 20175

The array design consumes approx. 3.52 times the gas consumes by the mapping design. This is because to update the nth element in the array design it takes O(n) time whereas to updating the nth element in the mapping design takes O(1) time.

2 Likes

My answers for the assignment:

My code:

pragma solidity 0.8.0;

contract StorageDesignMapping {
    
    struct Entity{
      uint data;
      address _address;
    }
    
    mapping (address => Entity) public entities;
    
    function addEntity (uint _data) public returns (bool){
        entities[msg.sender].data = _data;
        entities[msg.sender]._address = msg.sender;
        return true;
    }
    
    function updateEntity (uint _data) public returns (bool){
        require(entities[msg.sender]._address == msg.sender);
        entities[msg.sender].data = _data;
        return true;
    }
    
    
}



pragma solidity 0.8.0;

contract StorageDesignArray {
    
    struct Entity{
      uint data;
      address _address;
    }
    
   Entity[] entities;
    
    function addEntity (uint _data) public {
        Entity memory newEntity;
        
        newEntity._address = msg.sender;
        newEntity.data = _data;
        entities.push(newEntity);
    }
    
    function updateEntity (uint _data) public {
        
        for(uint i=0; i<entities.length; i++) {
            if(entities[i]._address == msg.sender){
                entities[i].data = _data;
            }
        }
    }
    
    
}

When executing the addEntity function, which design consumes the most gas (execution cost)? Is it a significant difference? Why/why not?

Mapping: addEntity exe cost: 41678
Array: addEntity exe cost: 62386

Array design costs significantly more; I assume it is because it adds the data onto the end of the array and then has to keep track of where it is, compared to just “throwing” the data into a general mapping pool.

Add 5 Entities into storage using the addEntity function and 5 different addresses. Then update the data of the fifth address you used. Do this for both contracts and take note of the gas consumption (execution cost). Which solution consumes more gas and why?

Mapping: updateEntity exe cost: 6704
Array: updateEntity exe cost: 20942

The array costs significantly more; I assume this is because it needs to iterate through the array to find the correct index before updating.

2 Likes

Q1.
Execution cost

  • Array - 62305 gas

  • Mapping - 41298 gas

Yes, the difference is pretty significant. Array function is 33.7% costlier.

Q2.
Execution cost of updating data of 5th address

  • Array - 11742

  • Mapping - 6394

Array function consumes 45% more gas than mapping function because the longer the array is, the more the computation is required.

1 Like

storage w/ mapping

creation execution cost 204245 gas

addEntity1 execution cost 41723 gas
addEntity2 execution cost 41723 gas
addEntity3 execution cost 41723 gas
addEntity4 execution cost 41723 gas
addEntity5 execution cost 41723 gas

updateEntity5 execution cost 7479 gas

call data5 execution cost 2525 gas
Total = 255,972

contract StorageDesignMaps3 {
    
    mapping(address => Entity) public entities;
    
    struct Entity {
        uint data;
        address _address;
    }
    
    address owner;
    
    function addEntity(uint data, address entityAddress) public {
        entities[entityAddress].data = data;
        entities[entityAddress]._address = entityAddress;
    }
    
    function updateEntity(uint newdata, address entityAddress) public {
        entities[entityAddress].data = newdata;
        entities[entityAddress]._address = entityAddress;
    }
}

storage w/ arrays

creation execution cost 190232 gas

addEntity0 execution cost 62553 gas
addEntity1 execution cost 47553 gas
addEntity2 execution cost 47553 gas
addEntity3 execution cost 47553 gas
addEntity4 execution cost 47553 gas

updateEntity4 execution cost 6490 gas

call data 4 execution cost 3282 gas
Total = 247,557

contract StorageDesArr2 {
    
    struct Entity {
        uint data;
        address _address;
    }
    
    Entity[] public entities;
    
    function addEntity(uint data, address _address) public {
        entities.push(Entity(data, _address));
        
    }
    
    function updateEntity(uint index, uint data) public {
        entities[index].data = data;
    }
    
}

I guess I am missing something. My Mapping is a little bit more expensive once as a total but individually, it looks like Mapping is cheaper.

As to why, well, it seems mapping should be cheaper. I would guess there are more math operators under the hood with arrays, hence the higher price.

1 Like

Array Storage:
insert 1 -> 62286
insert 2 -> 47286
insert 3 -> 47286
insert 4 -> 47286
insert 5 -> 47286
update 5 -> 20942

Mapping Storage:
insert 1 -> 41450
insert 2 -> 41450
insert 3 -> 41450
insert 4 -> 41450
insert 5 -> 41450
update 5 -> 5515

Overall, mapping executions gas consumption is smaller

1 Like

I’m going to make the following simplifying assumption for testing. I’m assuming the user(s) have an off-chain record of the data they’re entering. i.e.when updating a record its index &| address is known.
This aids in stripping the test contracts down to just the operations I’m comparing: storing and modifying structure data within either an array of structures or a mapping of structures. No saftey tests, no indexing iterations. I’m also using identical structure shorthand in both versions. The only difference between the two is necessarily the data structure and its access syntax.

Array contract is as follows:

pragma solidity 0.8.0;

// SPDX-License-Identifier: UNLICENSED

contract arrayDB {
    
    struct Entity {
        uint data;
        address _address;
    }
    
    Entity[] entities;
    
    function addEntity(address origin, uint _data) public {
        entities.push(Entity({data:_data,_address:origin}));
    }
    
    function updateEntity(uint index, uint newdata) public {
        entities[index].data = newdata;
    }
}

Further streamlining uses the same function for both creation and modification of records within the mapping version (contract and structure definitions remaining constant):

    mapping(address => Entity) entities;
    
    function updateEntity(address origin, uint _data) public {
        entities[origin] = Entity({_address:origin, data:_data});
    }

My test data is as follows. I ran the tests using the same sequence of addresses and data for each test article to eliminate possible runtime sources of variablilty.
image

Questions are as follows:

  1. Which is the more expensive addEntity()? Significantly? Why?
    The array version is 7% more expensive, 20% more epxensive on first entry. Solidity’s array implementation is less efficient than its mapping functions, for whatever reason. Both primarliy require allocating more storage space in this operation. Additionally it seems Solidity’s arrays require overhead operations to instantiate on first use. The 20% initialization cost is negligable because it only happens once, but the 7% is significant over every transaction during the lifespan of the contract.

  2. Which is the more expensive updateEntity()? Why?
    The array update is 7% more efficient than the mapping update. Because arrays index with an offset operation whereas mappings index with hashes.

2 Likes

Hello, here is my solution.

Mapping storage

pragma solidity 0.8.0;

contract StorageMapping {
    struct Entity {
        uint data;
        address _address;
        bool doesExist;
    }
    
    mapping(address => Entity) entityMapping;
    
    function addEntity(uint data) external returns (bool success) {
        require(!entityMapping[msg.sender].doesExist, "Entity for this address already exists");
        entityMapping[msg.sender].data = data;
        entityMapping[msg.sender]._address = msg.sender;
        entityMapping[msg.sender].doesExist = true;
        return true;
    }
    
    function updateEntity(uint data) external returns (bool success) {
        require(entityMapping[msg.sender].doesExist, "Entity for you doesn't exist");
        entityMapping[msg.sender].data = data;
        return true;
    }
}

Array storage

pragma solidity 0.8.0;

contract StorageArray {
    struct Entity {
        uint data;
        address _address;
    }
    
    Entity[] public entityList;
    
    function addEntity(uint data) external returns (bool success) {
        for (uint i=0; i<entityList.length; i++) {
            require(entityList[i]._address != msg.sender, "Entity for this address already exists");
        }
        entityList.push(Entity(data, msg.sender));
        return true;
    }
    
    function updateEntity(uint data) external returns (bool success) {
        uint entityIndex;
        bool entityExists = false;
        for (uint i=0; i < entityList.length; i++) {
            if (entityList[i]._address == msg.sender) {
                entityIndex = i;
                entityExists = true;
                break;
            }
        }
        require(entityExists, "Entity for you doesn't exist");
        entityList[entityIndex].data = data;
        
        return true;
    }
}

And execution data is

array gas cost mapping gas cost
1 addition 63355 44469
2 addition 51112 44469
3 addition 53869 44469
4 addition 56626 44469
5 addition 59383 44469
update 5th 21209 6715

So mapping for this case is much better and faster both for addition and update. Also with mapping we have constant gas cost. In case with array gas cost becomes higher with array expansion. That is because search process becomes longer and method has to do more compares in order to find entity in array.

2 Likes
pragma solidity 0.8.0;

contract ArrayStorage {
    
    struct Entity{
        uint data;
        address _address;
    }
    
    Entity[] entityArray;
    
    function addEntity(uint _data) public returns(bool success){
        Entity memory newEntity;
        newEntity.data     = _data;
        newEntity._address = msg.sender;
        entityArray.push(newEntity);
        return true;
    }
    
    function updateEntity(uint _data) public returns(bool success){
        for(uint i=0; i<entityArray.length; i++){
            if(entityArray[i]._address == msg.sender){
                entityArray[i].data = _data;
            }
        }
         return true;   
    }
        
}

pragma solidity 0.8.0;

contract MappingStorage {
    
    struct Entity{
        uint data;
        address _address;
    }
    
    mapping(address => Entity) entityMapping;
    
    function addEntity(uint _data) public returns(bool){
        entityMapping[msg.sender].data = _data;
        return true;
    }
    
    function updateEntity(uint _data) public returns(bool){
        entityMapping[msg.sender].data = _data;
        return true;
    }
}

Mapping - Add Entity
Transaction cost = 42159
Execution cost = 20695

Array - Add Entity
Transaction cost = 84049
Execution cost = 62585

It is cheaper to execute the mapping add entry because there is less code to execute and the function is not storing to memory.

Mapping update entry
Transaction cost = 27181
Execution cost = 5717

Array - Update Entity
Transaction cost = 42616
Execution cost = 21152

The mapping update entity is cheaper to execute because there is not array to search through so less computation is required.

1 Like

Storage with Mapping

pragma solidity 0.8.0;
pragma abicoder v2;

contract Storage{
    
    struct Entity{
        uint data;
        address _address;
    }

    mapping(address => mapping(address => Entity)) Entities;
    
    // Entity[] Entities;
    
    function addEntity(uint _data, address _address) public {
        Entities[msg.sender][_address] = Entity(_data, _address);
        // Entities.push(Entity(_data, _address))
    }
    
    function updateEntity(uint _newData, address _address) public{
        Entities[msg.sender][_address].data = _newData;
    }
    
    function getEntity(address _address) public view returns(uint){
        return Entities[msg.sender][_address].data;
    }

}

Storage with Arrays

pragma solidity 0.8.0;
pragma abicoder v2;

contract Storage{
    
    struct Entity{
        uint data;
        address _address;
    }


    mapping(address=>Entity[]) Entities;
    
    function addEntity(uint _data, address _address) public {
        Entities[msg.sender].push(Entity(_data, _address));
    }
    
    function updateEntity(uint _newData, uint _arrayIndex) public{
        Entities[msg.sender][_arrayIndex].data = _newData;
    }
    
    function updateEntityLoop(uint _newData, address _address) public{
        for(uint i=0; i < Entities[msg.sender].length; i++){
            if(Entities[msg.sender][i]._address == _address){
                Entities[msg.sender][i].data = _newData;
            }
        }
    }
    
    function getEntity(uint _arrayIndex) public view returns(uint, address){
        return (Entities[msg.sender][_arrayIndex].data, Entities[msg.sender][_arrayIndex]._address);
    }

}

I’m not sure if it meets the criteria by using a mapping of a mapping to include multiple addresses for the same msg.sender…

In any case, the advantages and disadvantages were discussed in the lecture based on the cost of storage on the network and the cost of operations in case of multiple iterations through an array. The following results were obtained (given in gas and updating after the 5th):

  • for mapping:
    – addEntity(): 41807
    – updateEntity(): 5805
  • for array:
    – addEntity(): 47642
    – updateEntity(): 6557
    – updateEntityLoop(): 22281

I initially updated the array by index, but it felt a little like cheating so I added the implementation with the loop. It is clearly more expensive than using the mapping in this use case.

Feedback is appreciated :smile:

1 Like