Advanced Solidity | Gas Golfing | Yul | EVM | Audit | Zero Knowledge Proofs | Puzzles
Advanced Solidity | Gas Golfing | Yul | EVM | Audit | Zero Knowledge Proofs | Puzzles

Subscribe to aṇuha

Subscribe to aṇuha
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
This article is a continuation of Gas Golfing #1. In the previous article, we discussed how uint8 consumes more gas than uint256. Then a very natural question arises.
If uint8 costs more than uint256 then what is the need to have all of those supported units from 8 256 and why not have uint256 alone?
The simple answer to that question is variable packing in solidity. Let us understand more of that through an example. (Deployment optimized for 1000 runs)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract VariablePacking {
uint8 smallNum = 21; // Occupies Slot 0 of Storage
uint8 anotherSmallNum = 22; // Occupies Slot 0 of Storage
uint256 bigNum = 12134567890; // Occupies Slot 1 of Storage
// 2351 gas (Cost only applies when called by a contract)
function readSmallNum() public view returns(uint8){
return smallNum;
}
// 2307gas (Cost only applies when called by a contract)
function readAnotherSmallNum() public view returns(uint8){
return anotherSmallNum;
}
// 2248 gas (Cost only applies when called by a contract)
function readBigNum() public view returns(uint256){
return bigNum;
}
// Input 0: 0x0000000000000000000000000000000000000000000000000000000000001615
// Input 1: 0x00000000000000000000000000000000000000000000000000000002d346cfd2
function getValueAtSlot(uint256 slotNum) public view returns (bytes32) {
bytes32 value;
assembly {
value := sload(slotNum)
}
return value;
}
}
This article is a continuation of Gas Golfing #1. In the previous article, we discussed how uint8 consumes more gas than uint256. Then a very natural question arises.
If uint8 costs more than uint256 then what is the need to have all of those supported units from 8 256 and why not have uint256 alone?
The simple answer to that question is variable packing in solidity. Let us understand more of that through an example. (Deployment optimized for 1000 runs)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract VariablePacking {
uint8 smallNum = 21; // Occupies Slot 0 of Storage
uint8 anotherSmallNum = 22; // Occupies Slot 0 of Storage
uint256 bigNum = 12134567890; // Occupies Slot 1 of Storage
// 2351 gas (Cost only applies when called by a contract)
function readSmallNum() public view returns(uint8){
return smallNum;
}
// 2307gas (Cost only applies when called by a contract)
function readAnotherSmallNum() public view returns(uint8){
return anotherSmallNum;
}
// 2248 gas (Cost only applies when called by a contract)
function readBigNum() public view returns(uint256){
return bigNum;
}
// Input 0: 0x0000000000000000000000000000000000000000000000000000000000001615
// Input 1: 0x00000000000000000000000000000000000000000000000000000002d346cfd2
function getValueAtSlot(uint256 slotNum) public view returns (bytes32) {
bytes32 value;
assembly {
value := sload(slotNum)
}
return value;
}
}
This sample just adds another variable anotherSmallNum to the previous example and a getter function for the same. This slightly changes the gas units consumed.
📑 But an important thing to note is that the values
smallNumandanotherSmallNumoccupy the slot0 of the storage. So there is less initial gas cost on deployment. But higher execution gas consumption. The solidity compiler does this by packing of variables in a slot whenever applicable.
A refactor in the below way can be beneficial concerning gas consumed in execution. But at the cost of using an extra storage slot during deployment.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Refactored {
uint256 smallNum = 21; // Occupies Slot 0 of Storage
uint256 anotherSmallNumber = 22; // Occupies Slot 0 of Storage
uint256 bigNum = 12134567890; // Occupies Slot 1 of Storage
// 2324 gas (Cost only applies when called by a contract)
function readSmallNum() public view returns(uint256){
return smallNum;
}
// 2280gas (Cost only applies when called by a contract)
function readAnotherSmallNum() public view returns(uint256){
return anotherSmallNumber;
}
// 2247 gas (Cost only applies when called by a contract)
function readBigNum() public view returns(uint256){
return bigNum;
}
// Input 0: 0x0000000000000000000000000000000000000000000000000000000000000015
// Input 1: 0x0000000000000000000000000000000000000000000000000000000000000016
// Input 2: 0x00000000000000000000000000000000000000000000000000000002d346cfd2
function getValueAtSlot(uint256 slotNum) public view returns (bytes32) {
bytes32 value;
assembly {
value := sload(slotNum)
}
return value;
}
}
So is the packing of variables of any use at all?
It is useful when all the variables in the slot are read together. Like via the structs.
Let’s look at the sample codes below and check their gas utilization. (Deployment optimized for 1000 runs)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract PackingInStructs {
struct VariablesNotPacked{
address myAddress;
uint256 myage;
bool areYouHappy;
}
struct VariablesPacked{
bool areYouHappy;
uint8 myage;
address myAddress;
}
VariablesNotPacked a;
VariablesPacked b;
constructor() {
a = VariablesNotPacked(msg.sender, 36, false);
b = VariablesPacked(true, 36, msg.sender);
}
// 6774 gas (Cost only applies when called by a contract)
function readVariablesNotPacked() public view returns(VariablesNotPacked memory){
return a;
}
// 2550 gas (Cost only applies when called by a contract)
function readVariablesPacked() public view returns(VariablesPacked memory){
return b;
}
// Input 0: 0x0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 - a.myAddress
// Input 1: 0x0000000000000000000000000000000000000000000000000000000000000024 - a.age
// Input 2: 0x0000000000000000000000000000000000000000000000000000000000000000 - a.areYouHappy
// Input 3: 0x00000000000000000000|5b38da6a701c568545dcfcb03fcb875f56beddc4|24|01|
// - | b.myAddress | b.age | b.areYouHappy|
function getValueAtSlot(uint256 slotNum) public view returns (bytes32) {
bytes32 value;
assembly {
value := sload(slotNum)
}
return value;
}
}
📑 Below is the summary
Note the huge difference in gas units consumed
Note how the storage slot for
VariablesNotPackedis spread across 3 slots while only one slot inVariabledPacked
In conclusion, choosing the appropriate data type is essential when designing smart contracts in Solidity. Also, enable variable packing by crafty arrangements of the variables.
Links:
This sample just adds another variable anotherSmallNum to the previous example and a getter function for the same. This slightly changes the gas units consumed.
📑 But an important thing to note is that the values
smallNumandanotherSmallNumoccupy the slot0 of the storage. So there is less initial gas cost on deployment. But higher execution gas consumption. The solidity compiler does this by packing of variables in a slot whenever applicable.
A refactor in the below way can be beneficial concerning gas consumed in execution. But at the cost of using an extra storage slot during deployment.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Refactored {
uint256 smallNum = 21; // Occupies Slot 0 of Storage
uint256 anotherSmallNumber = 22; // Occupies Slot 0 of Storage
uint256 bigNum = 12134567890; // Occupies Slot 1 of Storage
// 2324 gas (Cost only applies when called by a contract)
function readSmallNum() public view returns(uint256){
return smallNum;
}
// 2280gas (Cost only applies when called by a contract)
function readAnotherSmallNum() public view returns(uint256){
return anotherSmallNumber;
}
// 2247 gas (Cost only applies when called by a contract)
function readBigNum() public view returns(uint256){
return bigNum;
}
// Input 0: 0x0000000000000000000000000000000000000000000000000000000000000015
// Input 1: 0x0000000000000000000000000000000000000000000000000000000000000016
// Input 2: 0x00000000000000000000000000000000000000000000000000000002d346cfd2
function getValueAtSlot(uint256 slotNum) public view returns (bytes32) {
bytes32 value;
assembly {
value := sload(slotNum)
}
return value;
}
}
So is the packing of variables of any use at all?
It is useful when all the variables in the slot are read together. Like via the structs.
Let’s look at the sample codes below and check their gas utilization. (Deployment optimized for 1000 runs)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract PackingInStructs {
struct VariablesNotPacked{
address myAddress;
uint256 myage;
bool areYouHappy;
}
struct VariablesPacked{
bool areYouHappy;
uint8 myage;
address myAddress;
}
VariablesNotPacked a;
VariablesPacked b;
constructor() {
a = VariablesNotPacked(msg.sender, 36, false);
b = VariablesPacked(true, 36, msg.sender);
}
// 6774 gas (Cost only applies when called by a contract)
function readVariablesNotPacked() public view returns(VariablesNotPacked memory){
return a;
}
// 2550 gas (Cost only applies when called by a contract)
function readVariablesPacked() public view returns(VariablesPacked memory){
return b;
}
// Input 0: 0x0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 - a.myAddress
// Input 1: 0x0000000000000000000000000000000000000000000000000000000000000024 - a.age
// Input 2: 0x0000000000000000000000000000000000000000000000000000000000000000 - a.areYouHappy
// Input 3: 0x00000000000000000000|5b38da6a701c568545dcfcb03fcb875f56beddc4|24|01|
// - | b.myAddress | b.age | b.areYouHappy|
function getValueAtSlot(uint256 slotNum) public view returns (bytes32) {
bytes32 value;
assembly {
value := sload(slotNum)
}
return value;
}
}
📑 Below is the summary
Note the huge difference in gas units consumed
Note how the storage slot for
VariablesNotPackedis spread across 3 slots while only one slot inVariabledPacked
In conclusion, choosing the appropriate data type is essential when designing smart contracts in Solidity. Also, enable variable packing by crafty arrangements of the variables.
Links:
No activity yet