B009: EVM Tricks
The Ethereum Virtual Machine, or EVM for short, is an often misunderstood engine that is responsible for executing the compiled code of smart contracts and is what all high-level languages, such as Solidity and Vyper, get compiled to.
Although discussed in the Yellow Paper Ch.9 in length, there aren’t many comprehensive sources of truth with regards to its operation as well as what is the best way to code the high-level notions in for them to be rigorously optimized at the EVM level.
Native Data Types
The word size of the EVM is 256-bits, meaning that all instructions within its system are “optimized” to operate with 256-bit / 32-byte data types. Any data type specified below this range is implemented at a non-native level by introducing surplus instructions, such as bitwise shifts and bound checks.
Inferred from the above is the increased gas cost involved in handling sub-256-bit data types. A pattern I have seen applied more often than it should be is utilizing a small data type if the expected values of the said variable are meant to be within a sensible bound.
The above loop iteration will in reality consume more gas due to the uint8
addition that needs to be performed on each loop. Overall, roughly the same increase in gas consumption can be observed for any data type less than 256-bits.
Variable Tight-Packing
Although briefly mentioned in B-003, this mechanism warrants a little bit more of an explanation to be better comprehended as to when it should be utilized and when it is detrimental to do so.
As the EVM is a word-based machine that natively takes up 256-bits for each data slot, high-level languages came up with an optimization mechanism whereby, if deemed applicable, they pack multiple declared values into the same storage slot at the EVM level thereby saving up on read and write operations, two of the most costly ones on the EVM.
This mechanism is applied both at the contract-level declarations as well as at the struct
level declarations. Some additional information that is useful to apply this optimization is that address
members take up 160-bits of information, andenum
declarations as well as bool
declarations take up 8-bits.
It should be noted that although dynamic types cannot be tight-packed, fixed-size types such as a bytes1
array with 32 slots will be packed when possible. A negative trait of tight-packing that can be deduced from the above is given that multiple variables are tight-packed into a single slot and at times only one may be read or written to, the operation may end up being more costly.
To properly assess whether tight-packing for a particular segment of the codebase is useful, gas measurement tests should be utilized that test all scenarios unless it is easily discernible that the packing mechanism would be useful i.e. if all values of a struct are always read and written to simultaneously.
Hidden Gas Costs
The gas cost of each EVM instruction differs depending on its original purpose and on the “complexity” it bears to execute it. A relatively up-to-date table of instructions paired with their gas costs can be found maintained by X here.
When the cost of a smart contract is mission-critical, there are certain advanced gas optimizations that can be applied to further optimize its gas costs at the cost of code legibility. A prime example of this is 1inch’s CHI token optimization whereby they identified that usage of msg.value
(callvalue
), guaranteed to be 0
, is in fact cheaper than the literal 0
.
A more legible optimization that can be applied in quite a few contracts is the logical guarantees of msg.sender
should a function impose access control. For example, when a function transfers ownership from one member to the other and possesses the onlyOwner
modifier, it is more optimal to use msg.sender
instead of owner
in the event declarations.
Depending on the function context other similar optimizations can be applied. A concept that I had ideated in the past is empowering “dumb” Ethereum accounts (i.e. exchange wallets) with the ability to execute “complex” transactions by utilizing the msg.value
as a data point.
For example, if 1 wei
is specified as the msg.value
function A should be executed and so forth. This could also be utilized to pass in small packs of data to the function as the maximum value of 56-bits of information is roughly ~.72 Ether
, a non-prohibitive “maximum” value for a rudimentary application.
Conclusion
While intimidating at first glance, the EVM is a relatively elementary machine that can be skillfully handled to operate to one's benefit. Although we only grazed its surface, it is evident that there are a lot of “hidden” or at least not widely-discussed notions and caveats one should tread when developing applications on this platform.