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.
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, and
enum 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
callvalue), guaranteed to be
0, is in fact cheaper than the literal
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.
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.