Basics of Smart Contract Gas Optimization with Solidity
Gas optimization is a matter of doing what is cheap and avoiding what is expensive in terms of gas costs on EVM blockchains
Gas optimization is a matter of doing what is cheap and avoiding what is expensive in terms of gas costs on EVM blockchains.
What is cheap:
Reading constants and immutable variables.
Reading and writing local variables.
Reading and writing memory variables like memory arrays and structs.
Reading calldata variables like calldata arrays and structs.
Internal function calls.
What is expensive:
Read and writing state variables that are stored in contract storage.
External function calls.
Loops
If you can rewrite or organize your code so you are doing more what is cheap and less what is expensive then you are saving gas and that’s a gas optimization.
These days gas optimization is getting more and more important, even on sidechains and L2 solutions.
A common optimization is to replace state variable reads and writes within loops with local variable reads and writes. This is done by assigning state variable values to new local variables, reading and/or writing the local variables in a loop, then after the loop assigning any changed local variables to their equivalent state variables.
Here is a simple example of unoptimized code:
function doIt() external {
for(uint256 i; i < myArray.length; i++) { // state reads
myCounter++; // state reads and writes
}
}
Here is the optimized code:
function doIt() external {
uint256 length = myArray.length; // one state read
uint256 local_mycounter = myCounter; // one state read
for(uint256 i; i < length; i++) { // local reads
local_mycounter++; // local reads and writes
}
myCounter = local_mycounter; // one state write
}
The optimized code takes the state reads and writes out of the loop so that they are done one time instead of multiple times.
Packing Structs
A common gas optimization is “packing structs” or “packing storage slots”. This is the action of using smaller types like uint128 and uint96 next to each other in contract storage. When values are read or written in contract storage a full 256 bits are read or written. So if you can pack multiple variables within one 256 bit storage slot then you are cutting the cost to read or write those storage variables in half or more.
Packing structs is a way to reduce contract storage reads and writes. Because with it you can get the values of multiple state variables or write the values of multiple state values in a single state read or state write to contract storage.
Here is an example of an unoptimized struct:
struct MyStruct {
uint256 myTime;
address myAddress;
}
Example of an optimized struct:
struct MyStruct {
uint96 myTime;
address myAddress;
}
In the above a myTime and myAddress state variables take up 256 bits so both values can be read or written in a single state read or write.
External Function Calls
It is sometimes possible to convert external function calls between contracts into internal function calls. An article about that is here: How EIP2535 Diamonds Reduces Gas Costs for Smart Contract Systems
Solidity Gas Optimizer
Make sure Solidity’s optimizer is enabled. It reduces gas costs. If you want to gas optimize for contract deployment (costs less to deploy a contract) then set the Solidity optimizer at a low number. If you want to optimize for run-time gas costs (when functions are called on a contract) then set the optimizer to a high number.
If you run into the 24.5kb max contract size limit then look into EIP2535 Diamonds.
If you hit a stack limit then a trick I use is to create a local array with constant offsets to the variable you need. That way the compiler only sees 1 variable (the array) and the constants rather than the n variables in the array. Constants make the code a bit more readable than literal numbers.
Not sure what the gas cost is of an array lookup which is bound to be higher than just a variable, but if it means that the function can compile its the cost of doing it.
Thank you!