One the characteristics of the EVM is that, unlike most VMs, it operates on 256bit integers and requires 256bit arithmetic. Because of the 256bit nature of the EVM the gas can theoretically be 2²⁵⁶-1 but actually never will because the amount of computation required and the cost incurred by such an execution would be too much for anyone to pay. Instead of using 2²⁵⁶ we can use 2⁶⁴, which modern day CPUs have native support for and can generally be optimised by the compiler (unlike unlimited size big integers) and even though 2⁶⁴ is still in the upper ranges of “too costy” it’s a very nice constraint to have and allows us to hardcap the gas on something the computer can “natively” understand.

The go-ethereum repository implements — among the Ethereum client and tool suite — the Ethereum Protocol in the Go language. It contains two implementation of the EVM, one extremely simple byte code interpreter that simple loops over the byte code and a JIT-like interpreter that compiles the byte code in to manageable data types and structures. The JIT implementation has received a full gas-to-64-bit overhaul and seen some very impressive improvements:

benchmark old ns/op new ns/op delta

BenchmarkVmAckermann32Tests-4 5797850 1767258 -69.52%

BenchmarkVmFibonacci16Tests-4 35884955 10848056 -69.77%

To compare the result of the overal performance gained by the JIT-EVM I’ve also compared them to the byte-code EVM:

benchmark old ns/op new ns/op delta

BenchmarkVmAckermann32Tests-4 7431330 1767258 -76.22%

BenchmarkVmFibonacci16Tests-4 45639966 10848056 -76.23%

Among the many changes in the JIT and conversions from *big.Int to uint64, I fixed one of the bottlenecks in particular. During the execution of the program each instruction would perform the necessary steps to calculate the gas and memory consumption required. For instruction we perform the following operation:

if gas.Cmp(amount) < 0 {

return OutOfGasErr

}

gas.Sub(gas, amount)

At first glance this operation seems pretty harmless but upon further inspection I concluded that most of the time is spent here, calculating whether the operation would succeed or not.

Another optimisation I did was changing how the gas is calculated when growing memory. For each word (32 bytes), there’s a cost associated for the expansion of the memory. The EVM uses a Quadratic-memory gas calculation formula:

(memSizeWords ^ 2) / QuadCoefficient + (MemWords * MemGas)

Like the gas-counter, this was also using big integer arithmetic, and since we’ve already capped the gas counter, the memory fee associated with the expansion could never go beyond 2⁶⁴-1.

Next steps

Converting many of the big integer arithmetic to uint64 has gained us a tremendous amount of performance, but I don’t think we should stop there. Next I’d like to do further research in to Symbolic Execution and whether it makes sense to do something similar in our EVM.

If you’d like to track progress on my optimisations please see PR #2572 on the go-ethereum repository.