Solidity Gas Cost – Switching from ‘storage’ to ‘memory’ Increases Gas Cost

gasmemorysoliditystorage

I've read several related answers and blog posts, all of them suggesting that for the sake of reading complex state variables (i.e., arrays and structures), one should always prefer declaring the local variable used for this purpose as memory rather than as storage:

I've conducted several tests, performing let response = await someTransaction() and then recording the value of response.receipt.gasUsed. All of these tests have unanimously shown that declaring the local variable as memory yields a higher gas-consumption than declaring it as storage.

Here is a very simple Truffle test which illustrates this:

Contract:

pragma solidity 0.4.25;

contract MyContract {
    struct Record {
        bool valid;
        uint val1;
        uint val2;
        uint val3;
    }

    mapping(uint => Record) public table;

    uint public count;

    function insert(uint key, uint val1, uint val2, uint val3) external {
        Record storage record = table[key];
        if (record.valid == false) {
            record.valid = true;
            record.val1 = val1;
            record.val2 = val2;
            record.val3 = val3;
            count += 1;
        }
    }

    function storage_remove(uint[] keys) external {
        for (uint i = 0; i < keys.length; i++) {
            Record storage record = table[keys[i]];
            if (record.valid == true) {
                delete table[keys[i]];
                count -= 1;
            }
        }
    }

    function memory_remove(uint[] keys) external {
        for (uint i = 0; i < keys.length; i++) {
            Record memory record = table[keys[i]];
            if (record.valid == true) {
                delete table[keys[i]];
                count -= 1;
            }
        }
    }
}

Test:

contract("MyContractTest", function() {
    const keys = [...Array(10).keys()];

    it("storage_remove gas consumption", async function() {
        const myContract = await artifacts.require("MyContract").new();
        for (let key of keys)
            await myContract.insert(key, key, key, key);
        const response = await myContract.storage_remove(keys);
        console.log(response.receipt.gasUsed);
    });

    it("memory_remove gas consumption", async function() {
        const myContract = await artifacts.require("MyContract").new();
        for (let key of keys)
            await myContract.insert(key, key, key, key);
        const response = await myContract.memory_remove(keys);
        console.log(response.receipt.gasUsed);
    });
});

Results:

Contract: MyContractTest storage_remove gas consumption: 142257
Contract: MyContractTest memory_remove gas consumption: 145976

Side Note:

The above is essentially a comparison between:

  • The gas cost of reading record.valid using storage record
  • The gas cost of reading record.valid using memory record

So what course of action should I take – use memory as suggested everywhere, or use storage which has proved to be less expensive?

Thank you!

Best Answer

When you declare the variable as memory, you'll have to pay extra gas for reserving the memory and more important, you'll also have to SLOAD the complete record, which in your case will be 4 SLOAD operations (4x200 gas). Later in your code, you profit from the cheap MLOAD, but you'll need to access a field more than 4 times, so the 4 SLOADs before are paying off.

In the case with variable declared with storage, you have no additional costs for reserving the memory and you are not preloading from storage. You'll have a single SLOAD only, which is cheaper. However, if you would have more than 4 field accesses, your gas costs would soon exceed the costs of the variant with the memory modifer.

Hence, there is no universal solution for this. It depends on the circumstances.

Finally, this optimization-task is something that can and should be done by the compiler. I hope to see this soon in solc.

Related Topic