3

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!

  • When assigning a storage reference data to a memory referenced variable, we are copying data from storage to memory. New memory is allocated. That's why you are getting this. – Mahesh Rajput Jan 30 at 13:23
  • @MaheshRajput: Thank you. That was my initial thought, out of a "naive" approach I suppose (kinda like, comparing storage vs memory to by reference vs by value from C/C++). However, none of the posts that I've read mentioned anything about this, so I tend to think that this type of comparison between the block-chain and "traditional" systems is incorrect. – goodvibration Jan 30 at 13:26
2

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.

  • So storage wins here because there is only 1 access (record.valid)? 4 would lead to a tie, and 5 or more would turn the odds in favor of memory? Reason I'm asking is that I've also tried a case with more than 1 access (though I can't remember if it was 3, 4 or 5 accesses). – goodvibration Jan 30 at 20:57
  • 1
    Yes. It's roughly like this. Try adding additional field reads, then at a point with >4 of them, you'll notice the difference. – ivicaa Jan 30 at 20:58
  • OK, this is a great answer, thank you very much for disassembling it for me :) – goodvibration Jan 30 at 21:01
  • You're welcome! ;-) – ivicaa Jan 30 at 21:32

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.