Solidity Gas Optimization: Part 1
Solidity gas optimization is a crucial skill for Solidity developers, allowing them to save clients and users money. There are a few interesting gas usage optimization techniques which I noticed while exploring the Linea Github repository.
The first thing I noticed is a widespread use of the unchecked keyword. This keyword tells the Solidity compiler to turn off arithmetic operations safety checks, which were added as default in version 0.8.0.
The following code example demonstrates the difference in usage:
function sumChecked(uint256[] memory numbers) public pure returns (uint256) {
uint256 total = 0;
for (uint256 i = 0; i < numbers.length; i++) {
total += numbers[i];
}
return total;
}
function sumUnchecked(uint256[] memory numbers) public pure returns (uint256) {
unchecked {
uint256 total = 0;
for (uint256 i = 0; i < numbers.length; i++) {
total += numbers[i];
}
return total;
}
}
sumUnchecked function is more gas-efficient, but in this example it's possible that an overflow error may occur if numbers[i] + numbers[j] > 2^256.
This means that when you are working with any arithmetic operations in Solidity, you need to determine whether it is possible to encounter overflow or underflow errors in a particular piece of code, and if the answer is no, then you need to add the keyword unchecked.
Check out the official Solidity documentation to learn more about unchecked.
The second thing that I noticed is the frequent use of assembly and manual memory management. Let's take a look at the example:
function defaultKeccak(bytes32 first, bytes32 second) public pure returns (bytes32 value) {
return keccak256(abi.encodePacked(first, second));
}
Keccak256 is Solidity’s default hashing function. In our example, it expects concatenated bytes as input.
abi.encodePacked(first, second) is a high-level utility that concatenates the two bytes32 values into a single memory block. Inside encodePacked Solidity creates a temporary memory buffer to store the concatenated values. The memory allocation, data copying, and concatenation introduce overhead. To avoid that overhead and save gas, we can manage memory manually.
function assembledKeccak(bytes32 first, bytes32 second) public pure returns (bytes32 value) {
assembly {
mstore(0x00, first)
mstore(0x20, second)
value := keccak256(0x00, 0x40)
}
}
First, I'll remind the reader that 32 is 0x20 in hexadecimal representation, so bytes32 variable takes exactly 0x20 bytes in memory. What happens here: The mstore instructions directly store first and second into specific memory slots (0x00 and 0x20). There is no high-level memory management or copying. The memory layout is explicitly controlled with mstore, so the data is immediately ready for hashing.
More information about assembly in Solidity can be found in the official doc official documentation.
Thank you for reading. I hope you enjoyed this article and learned something new!
If you have any comments or questions, don't hesitate to reach out to me.
All code examples and tests can be found here.
