If you have been following the Ethereum hard fork progress, you’ll
notice that a considerable number of proposed EIPs are about
performance optimizations. Take
EIP-145 as an example, it
adds three new opcodes,
SAR, defining some bitwise
operations. Reading the specification, you’ll notice that two of them,
SHR is equivalent to opcode sequences we already support,
PUSH1 2 EXP MUL and
PUSH1 2 EXP DIV. The other one,
SAR, is also
mostly equivalent to
PUSH1 2 EXP SDIV with the only difference
being the rounding.
Look deeper at the specification, you’ll also notice that the document was already drafted back in 2017, but it took more than 2 years for developers to get it on mainnet. In comparison, standard bodies such as web browsers can get hundreds of new features into production each year. I think most of us would agree that the process just took too long for a performance optimization feature like this.
In the mean time, because of the new opcodes introduced, it becomes increasingly harder for dapp developers that work on multiple blockchains, such as Ethereum and Ethereum Classic. It is not possible to compile one contract bytecode and make it run everywhere. Instead, developers must carefully tweak their compiler configurations, and make sure things are always targeted to the correct blockchain, while all of the generated bytecodes are functionally equivalent, with the only difference being the optimization.
We explored two problems above – that client implementors do not have
a good method to implement performance optimizations without hard
forks, thus making the process take too long, and that it is
increasingly harder to compile once and run everywhere, even when the
only difference is performance optimization. The second problem we
described, can be solved using things like feature probe, as described
in versionless EVM. In this post,
however, I want to explain a different approach to solve the above two
issues, by using
HINT instructions, or originally known as EVM
HINT instruction always has a 4-byte tag appended to it. During
execution, the opcode, together with the tag, is ignored. It executes
as no-op, does not cost gas, and the only side effect bring incresing
the program counter.
Hint in Practice
Many optimizations can be implemented as substitutions. For example,
when the virtual machine sees
PUSH1 2 EXP MUL, it can replaces it
with a shift left operation which is functionally equivalent. Without
hint, searching for those substitutions are not an easy thing to
do. The things we want to substitute may be of different length, and
the collection may be big.
With hint, compilers can emit bytecode sequence such as
HINT [tag for
SAR] PUSH1 2 EXP MUL. The virtual machine will only need to check the
sequence specified by the given tag. This can be implemented as an
one-pass against the execution code when the VM starts. We already
require an one-pass for the
JUMPDEST analysis, so the hint check
will nearly have no performance impact at all.
Out-of-order Feature Introduction
Hard fork requires features to be introduced in order, with a fixed
date for activation.
HINT, on the other hand, allows certain
performance optimizations to be introduced out of order. Client
implementations can then implement those optimizations when they want,
and deploy them when they’re ready. Contract developers can also use
those hint opcodes without worrying whether each clients have added
support for them.