dev/Solidity

Storing Structs is costing you gas

_April 2022. 6. 20. 23:35

 

 

Storing Structs is costing you gas 라는 글을 보고 가스를 측정해 보았다.

 

2018년 글 wow

여러 시나리오가 테스트되어있다.

 

가스비가 그냥 state 여러개 저장 >>>construct > construct 내 변수 최적화>>+알파 순으로 든다는 주장인데,

직접 실험해보았다.

In Solidity (the programming language used for Ethereum smart contracts), you have “memory”, (think RAM on a computer), and “storage” (think the hard drive). Both are set up in chunks of 32 bytes (a byte is roughly a letter, so “are set up in chunks of 32 bytes” is 32 bytes of data).
In Solidity, memory is inexpensive (3 gas to store or update a value).
Storage is expensive (20,000 gas to store a value, 5,000 gas to update one).  -> SSTORE

 

시나리오 1  -  struct 아예 사용 안함

//Type1
    address owner;
    uint64 creationTime;
    uint256 dna;
    uint16 strength;
    uint16 race;
    uint16 class;
    mapping(uint256 => address) owners;
    mapping(uint256 => uint64) creationTimes;
    mapping(uint256 => uint256) dnas;
    mapping(uint256 => uint16) strengths;
    mapping(uint256 => uint16) races;
    mapping(uint256 => uint16) classes;

    function save1(
        uint64 _creationTime,
        uint256 _dna,
        uint16 _strength,
        uint16 _race,
        uint16 _class
    ) external {
        owners[dna] = msg.sender;
        creationTimes[dna] = _creationTime;
        dnas[dna] = _dna;
        strengths[dna] = _strength;
        races[dna] = _race;
        classes[dna] = _class;
    }

이제 ts에서 테스트해보자

it("Should success1 - gas test", async function () {
    const tx = await gasTest
      .connect(account1)
      .save1(Date.now(), 123123, 8, 0, 3);
    const receipt = await tx.wait();
    console.log(receipt.gasUsed);
  });

결과 : BigNumber { value: "139052" }

 

시나리오 1  -  struct 사용

//Type 2
    struct GameCharacter2 {
        address owner;
        uint64 creationTime;
        uint256 dna;
        uint16 strength;
        uint16 race;
        uint16 class;
    }
    mapping(uint256 => GameCharacter2) characters2;

    function save2(
        uint64 _creationTime,
        uint256 _dna,
        uint16 _strength,
        uint16 _race,
        uint16 _class
    ) external {
        characters2[_dna] = GameCharacter2({
            owner: msg.sender,
            creationTime: _creationTime,
            dna: _dna,
            strength: _strength,
            race: _race,
            class: _class
        });
    }

이제 테스트할차례

it("Should success2 - gas test", async function () {
    const tx = await gasTest
      .connect(account1)
      .save2(Date.now(), 123123, 8, 0, 3);
    const receipt = await tx.wait();
    console.log(receipt.gasUsed);
  });

결과 : BigNumber { value: "90709" }

 

진짜 가스가 줄었다.

 

시나리오 1에서는 스토리지에 저장이 6번이루어졌으므로 120,000가스가 들것이다. (실제로는 139052)

시나리오 2에서는 슬롯이 줄어들어 75,000 gas가 든다. (왜 80000이 아니고 75000인지모르겠음 )

   연달아 나오는 데이터가 32바이트가안되면 컴파일러가 알아서 합쳐서 패키징한다.

   owner, creationTime을 같은 슬랏에 패키징하고, dna는 따로, strength, race, and class도 같은 슬랏에 패키징한다.

   

시나리오3 - 패키징 용량을 더 줄여보자

 //Type3
    struct GameCharacter3 {
        address owner;
        uint48 creationTime;
        uint16 strength;
        uint16 race;
        uint16 class;
        uint256 dna;
    }
    mapping(uint256 => GameCharacter3) characters3;

    function save3(
        uint48 _creationTime,
        uint256 _dna,
        uint16 _strength,
        uint16 _race,
        uint16 _class
    ) external {
        characters3[_dna] = GameCharacter3({
            owner: msg.sender,
            creationTime: _creationTime,
            dna: _dna,
            strength: _strength,
            race: _race,
            class: _class
        });
    }

두가지변화가있는데, 시간의 데이터타입이 uint48이 되었고 dna는 맨끝으로 옮겼다.

 

uint 의 크기는 꼭 8, 16, 64 가 아니라 그냥 8의 배수기만하면된다.

이렇게 하면 장점은

owner~class 까지 모두 하나의 32바이트 청크에 저장이된다. (20+6+2+2+2)

uint256 인 dna 는 이미 혼자 32바이트 청크를 차지한다.

이제 저장에 60,000 gas만든다.  (owner~class, dna, 맵저장인듯)

 

실제로 ts 에서 돌려보면

BigNumber { value: "68837" }
많이줄었다.

 

마지막방식은 들이는 공수에비해 가스비가 그렇게 줄지않았다.

그리고 40000가스가 든다고하는데,

 

캐릭터가 1슬랏을 차지하고 characters 맵과 dnaRecords맵에 저장할때 각각 슬랏을 하나씩 차지해서 결국 비슷하게 들지않나?

실제로 테스트결과 

BigNumber { value: "68364" }로 유의미하게 감소하지 않았다... 뭔가 바뀐걸까

//Type4
    struct GameCharacter4 {
        address owner;
        uint256 creationTime;
        uint256 strength;
        uint256 race;
        uint256 class;
        uint256 dna;
    }

    mapping(uint256 => uint256) characters4;
    mapping(uint256 => uint256) dnaRecords4;

    function setCharacter(
        uint256 _id,
        address owner,
        uint256 creationTime,
        uint256 strength,
        uint256 race,
        uint256 class,
        uint256 dna
    ) external {
        uint256 character = uint256(uint160(owner));
        character |= creationTime << 160;
        character |= strength << 208;
        character |= race << 224;
        character |= class << 240;
        characters4[_id] = character;
        dnaRecords4[_id] = dna;
    }

    function getCharacter(uint256 _id)
        external
        view
        returns (
            address owner,
            uint256 creationTime,
            uint256 strength,
            uint256 race,
            uint256 class,
            uint256 dna
        )
    {
        uint256 character = characters4[_id];
        dna = dnaRecords4[_id];
        owner = address(uint160(character)); 
        creationTime = uint256(uint40(character >> 160));
        strength = uint256(uint16(character >> 208));
        race = uint256(uint16(character >> 224));
        class = uint256(uint16(character >> 240));
    }

기본요지는 어차피 uint256가 하나의 슬랏을 차지하니까

그 안에 메모리 위치를 지정헤서 데이터를 다 넣어버리자는거다.

it("Should success4 - gas test", async function () {
    const tx = await gasTest
      .connect(account1)
      .setCharacter(0, account1.getAddress(), Date.now(), 8, 0, 3, 123123);
    const receipt = await tx.wait();
    console.log(receipt.gasUsed);
  });

별로 안주는데 왜 블로그글에서는 많이 주는척했는지?

쓰고 읽을때 예외처리까지 해야해서 작성시 생산성도 떨어지는데 왜 과시하는지?

나를 이해시켜줄분 구함

 

 

 

코드는

https://gist.github.com/crypt0summer/7aeae440e5592edbd43d23ea688591fe

 

Ethreum-Storing Structs is costing you gas

Ethreum-Storing Structs is costing you gas. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

에있다.