티스토리 뷰
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
에있다.
'dev > Solidity' 카테고리의 다른 글
[Solidity]String 과 용량, 가스비 (0) | 2022.07.29 |
---|---|
opensea 및 NFT market 매매 흐름(setApprovalForAll, safeTransferFrom) (0) | 2022.07.22 |
interact with existing deployed contracts, raw calls (0) | 2022.06.16 |
Hardhat vs Truffle (0) | 2022.06.02 |
deploying contract, Compiler specific version warnings (0) | 2022.05.20 |