This EIP introduces multibyte opcodes prefixed by C0 for 64-bit arithmetic (C001-C00B), comparison (C010-C015), bitwise (C016-C019) and flow (C056 and C057 in “legacy”, or C0E1 and C0E2 in EOFv1) operations.
Motivation
Not all computations in EVM can utilize the full 256-bit integer width. It can therefore be beneficial to have a “64-bit mode” to avoid unnecessary cycles. This EIP uses a “prefix” opcode C0, essentially forming multibyte opcodes to avoid polluting the EVM opcode space too much.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Prefix opcode behavior
This EIP uses the prefix opcode C0, and it only occupies this single EVM opcode space. Upon the interpreter encountering opcode C0, it MUST continue to seek the next byte in code. It then executes things in “64-bit mode”, based on the second byte, described below. If the execution is successful, then the interpreter MUST increase PC by 2 (instead of 1).
If the second byte is not a valid 64-bit mode operation, then the interpreter MUST OOG.
General 64-bit mode behavior
In 64-bit mode, all operations only work on the least significant 64-bit of each stack value. The most significant 192-bit is discarded. When a result value is pushed back onto the stack, then it MUST ensures that observable effects will see that the most significant 192-bit is zero. Note that here it’s not necessary for an interpreter to reset the most significant 192-bit to zero every time – if the next opcode is still in 64-bit mode, then the most significant 192-bit is still unobservable. We discuss the full details in the “rationale” section. The interpreter only needs to reproduce the full 256-bit value upon entering non-64-bit mode. If the full computational heavy part can be written in pure 64-bit mode, then this can result in noticable performance gain.
Gas cost constants
We define the following gas cost constants:
G_BASE64: 1
G_VERYLOW64: 2
G_LOW64: 3
G_MID64: 5
G_HIGH64: 7
G_EXP64_STATIC: 5
G_EXP64_DYNAMIC: 25
G_RJUMPIV64: 3
Arithmetic opcodes
The 64-bit mode arithmetic opcodes are defined the same as non-64-bit mode, except that it only operates on the least significant 64-bits. In the below definition, a, b, N is a mod 2^64, b mod 2^64 and N mod 2^64.
ADD (C001) and SUB (C003): a op b mod 2^64, gas cost G_VERYLOW64.
MUL (C002), DIV (C004), SDIV (C005), MOD (C006), SMOD (C007), SIGNEXTEND (C00B): a op b mod 2^64, gas cost G_LOW64.
ADDMOD (C008), MULMOD (C009): a op b % N mod 2^64, gas cost G_MID64.
EXP (C00A): a EXP b mod 2^64, gas cost static_gas = G_EXP64_STATIC, dynamic_gas = G_EXP64_DYNAMIC * exponent_byte_size.
Comparison and bitwise opcodes
The 64-bit mode comparison and bitwise opcodes are defined the same as non-64-bit mode, except that they only operates on the least significant 64 bits.
LT (C010), GT (C011), SLT (C012), SGT (C013), EQ (C014), AND (C016), OR (C017), XOR (C018): a op b mod 2^64, gas cost G_VERYLOW64
ISZERO (C015), NOT (C019): op a mod 2^64, gas cost G_VERYLOW64
SHL (C01B), SHR (C01C), SAR (C01D): a op N mod 2^64, gas cost G_VERYLOW64
Note that:
64-bit EQ (C014) may return true for two different integers because it’ll only compare the least significant 64 bits.
64-bit ISZERO (C015) may return true for non-zero 256-bit integers as long as the last 64 bits are zero.
BYTE (1A) does not have 64-bit mode because it affects endianness.
Non-EOFv1 “legacy” mode and JUMP, JUMPI
For flow operations JUMP and JUMPI, the behavior is as follows:
JUMP will only read the last 64 bits from the stack value. The rest 192 bits are discarded without reading. Gas cost is G_MID64.
JUMPI will only read the last 64 bits from the stack as destination, and the condition check will only read the last 64 bits. Gas cost is G_HIGH64.
In JUMPDEST validation phrase, the next byte followed by C0 is considered data (same as in PUSH* opcodes) and does not mark a valid JUMPDEST destination.
EOFv1 mode and RJUMPI, RJUMPV
If EOFv1 mode is entered, then an additional validation step is added. If the opcode C0 is encountered and it is not part of PUSH opcode’s data, then the interpreter MUST validate that:
The next opcode exists.
The next opcode is one of the valid 64-bit mode opcode described above.
For flow operations RJUMPI and RJUMPV, the 64-bit mode has following changes:
For RJUMPI, the condition popped from stack is only read for the last 64 bits. Gas cost is G_RJUMPIV64.
For RJUMPV, the case popped from stack is only read for the last 64 bits. Gas cost is G_RJUMPIV64.
Note that:
RJUMP is automatically in 64-bit mode because it does not read or write the stack.
Rationale
When a smart contract uses the 64-bit mode, it’s expected that once entered, it will want to stay in 64-bit mode, and only exit to non-64-bit mode when the computationally intensive function is finished. This EIP is designed particularly with this fact in mind.
All 64-bit opcodes only operates on the 64-bit value. It totally discards the rest 192 bits. The interpreter only needs to ensure that when it exits into non-64-bit mode and next time when a value result is read, that value has the first 192 bits reset to zero. The EVM interpreter can therefore use a typed stack for optmization:
typeStackItem=ValueU256|Value64U64
The typed stack can also be implemented as a bitmap for memory alignment.
For all inputs of 64-bit opcodes, it will either read in a Value, when it’ll only take the last 64 bits, or a Value64, which is what is needed. It then always outputs a Value64. After exiting into non-64-bit mode and upon a Value64 is read, the interpreter then translate the value back to 256-bit Value by extending the first 192 bits with zero.
The 64-bit mode does not contain any opcodes which depends on the value’s endianness, therefore Value64 can also be stored in the optimal endianness of the architecture.
The 64-bit mode will not save any memory usage.
Discussions
Prefix (modes) opcodes
This EIP also recommends that we reserve C0-CF for prefix (modes) opcodes. For example, an additional modes OVERFLOW can be envisioned that changes the behavior of arithmetic opcodes from wrapping to overflow OOG, which can help to reduce, for example, the extra cycles needed for SafeMath.
Optimization assumptions
This EIP assumes that the majority of EVM implementations target native little-endian 64-bit architectures (like x86_64, arm64 and riscv64). A 256-bit stack item is stored efficiently internally as 4 items of 64-bit unsigned integer [u64; 4]. This EIP works best, in terms of optimizations, for those implementations. The opcodes simply operates on the last u64, instead of the whole [u64; 4]. There are basically no other changes.
Endianness
In 64-bit mode, the principle of the specification is that the endianness should be kept strictly as an implementation detail. There should not be any opcodes that depends on endian. This is important, because on the one hand, we must enable easy and fast interop for 64-bit and non-64-bit opcodes. On the other hand, the interpreter should be able to do optimizations internally whether it uses little or big endian.
Due to the issue of endianness, this EIP currently does not contain 64-bit mode BYTE64, memory opcodes MLOAD64 and MSTORE64, and stack push opcodes PUSH*64 (PUSH1 to PUSH8).
It is possible to extend EVM64 where “64-bit mode” is instead defined as “little-endian 64-bit mode”, which can be separately discussed and specified:
BYTE64 will be little-endian, so instead of (x >> (248 - i * 8)) & 0xFF, it will be (x >> i * 8) & 0xFF.
MLOAD64 and MSTORE64 loads and store little-endian 64-bit values in memory.
PUSH*64 (PUSH1 to PUSH8) accepts literal little-endian bytes.
In specification there is no issue with the design, but this can bring signficant confusions to developers. So the author advise caution.
POP, DUP*, SWAP*
There is no explicit 64-bit mode for stack operations:
POP is “automatically 64-bit” because it only removes values from the stack.
DUP* and SWAP* will copy or swap stack items optimized for 64-bit as is. They are kept optimized, so there’s no need for seperate 64-bit mode for those opcodes.
Drawbacks
Known drawbacks and tradeoffs of 64-bit mode includes:
The binary size will become larger. This is apparent as previously the opcode is single byte, but now it is two bytes. Each byte costs an additional 200 gas to deposit. However, 64-bit mode opcodes are (generally) cheaper, which gives developers sufficient incentives to utilize it (at most 200 calls and the cumulative gas costs will be cheaper).
Backwards Compatibility
This EIP introduces a new (prefix) opcode C0. C0 was previously an invalid opcode that has little usage, and thus the backward compatibility issues are minimal.
Test Cases
Test cases are orgnized as [stack_item_1, stack_item_2] C0 opcode => [result_stack_item_1, result_stack_item_2].