Published on

Solidity global uint8 variables cost more gas than global uint256 variables

Authors

Today I looked into what needs to be done to optimize the gas costs of a contract. As part of this investigation I came across the statement that it is actually cheaper to use uint256 instead of uint8.

I Googled this and found this answer. This was answered by Péter Szilágyi who is a team lead on the Ethereum project.

In his answer Péter suggested putting one contract using uint8 and another using uint256 as global variables into Remix. Each contract should then be compiled then the "Details" button should be clicked. Under details there should be an opcode section which can be compared between the 2 contracts to prove that uint8 is indeed more costly than uint256.

I did exactly that. The uint8 contract is below:

pragma solidity >=0.4.22 <0.6.0;

contract ContractUint8 {
    uint8 a = 0;
}

and the uint256 version:

pragma solidity >=0.4.22 <0.6.0;

contract ContractUint256 {
    uint256 a = 0;
}

The opcodes for both of these can be seen below one above the other:

//uint8 version
"opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 PUSH1 0x0 PUSH2 0x100 EXP DUP2 SLOAD DUP2 PUSH1 0xFF MUL NOT AND SWAP1 DUP4 PUSH1 0xFF AND MUL OR SWAP1 SSTORE POP CALLVALUE DUP1 ISZERO PUSH1 0x2A JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x35 DUP1 PUSH1 0x38 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 REVERT INVALID LOG1 PUSH6 0x627A7A723058 KECCAK256 0x5e SGT 0xc6 LT SWAP5 0xeb CALLDATASIZE 0x28 SGT 0x2a RETURNDATASIZE 0xd2 0x5c 0xd2 0xc3 SWAP16 0xc PUSH20 0xAA328FFD489D8AC412B82020ED8F002900000000 "

//uint256 version
"opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 SSTORE CALLVALUE DUP1 ISZERO PUSH1 0x13 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x35 DUP1 PUSH1 0x21 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 REVERT INVALID LOG1 PUSH6 0x627A7A723058 KECCAK256 0xb2 0xdd SWAP7 0xb9 DUP14 PUSH31 0xFA535DF06E156CCF900BFD8D9B0692EBBB3629873603EBB8BFF70029000000 "

The provided gas estimates for both versions are:

//uint8
{
	"Creation": {
		"codeDepositCost": "10600",
		"executionCost": "20333",
		"totalCost": "30933"
	}
}

//uint256
{
	"Creation": {
		"codeDepositCost": "10600",
		"executionCost": "5072",
		"totalCost": "15672"
	}
}

The gas estimate clearly indicates that the uint8 version is more costly. In terms of number of opcodes the uint8 version has more than the uint256 version - an entire new line.

The explanation for this is that the EVM works with 256bit words, using a smaller data type (smaller than 256 bits) results in further operations being needed to downscale which results in a higher gas cost:

The EVM works with 256bit/32byte words (debatable design decision). Every operation is based on these base units. If your data is smaller, further operations are needed to downscale from 256 bits to 8 bits, hence why you see increased costs.

This is covered at great length here. Based on the documentation it looks like uint256 should generally be used in place of uint8 for global variables. The exception to this seems to be when using them in a struct or fixed length array where uints have been placed in the struct and array so that they can be tightly packed by the EVM compiler (as described in the documentation).

The concept of tight packing with structs and fixed length arrays in storage (and not memory) can be used to counter this quirk of the EVM compiler. I would need to explore how tight packing works in more detail to see how effective it is at optimising the gas costs of contracts.