Gas Micro Optimisation using Function Names
You may think that the function names you choose in your Solidity contracts don’t matter when it comes to runtime gas consumption, and to an extent that is true. However, there are edge cases where function naming does affect runtime gas consumption! In this post we’re going to demonstrate this, as well as the tooling you can use to prove it. We’re going to use Foundry tools, but you don’t need those to follow along.
Function Selectors
It is true that the length of the function name makes no difference to how the contract is deployed or how it behaves on-chain, because the function name doesn’t go on-chain at all. When a contract is compiled, a function selector is calculated for each function, and that is what goes into the bytecode and goes on-chain. The function selector is the first 4-bytes of the Keccak-256 hash of the canonical signature of the function. For example:
Solidity Source Code | Canonical Signature | Keccak-256 of Canonical Signature == Function Selector |
---|---|---|
function setNumberC(uint256 number) external |
setNumberC(uint256) |
0x1b877640 |
So from above, it is clear that it doesn’t matter if our function name is 10 characters long or 50 characters long, because what gets deployed in the compiled bytecode is a function selector that is always 4 bytes long. All well and good, but why then might function names matter? To answer that we need to get to a lower level, and examine what happens at the opcode level.
The Jump Table
When your contract is compiled, part of the resulting code is a jump table. The jump table is used to check what function is being asked for from the transaction data field, and jumps to the correct memory location for that function body. The code that does this is referred to as a jump table, but it isn’t a table in the database sense, it is just more bytecode.
Let’s look at a worked example to make this clearer. First let’s go with some standard optimisation, so in foundry.toml
we have:
optimizer = true
optimizer_runs = 200
Next, let’s consider the below Solidity contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract JumpTable {
uint256 private _number;
function setNumberA(uint256 number) external {
_number = number;
}
function setNumberB(uint256 number) external {
_number = number;
}
function setNumberC(uint256 number) external {
_number = number;
}
function setNumberD(uint256 number) external {
_number = number;
}
}
The above shows we have 4 identical functions. We can get the function selectors like this:
$ forge inspect JumpTable methodIdentifiers
╭---------------------+------------╮
| Method | Identifier |
+==================================+
| setNumberA(uint256) | 4c67b768 |
|---------------------+------------|
| setNumberB(uint256) | 6e19ab5d |
|---------------------+------------|
| setNumberC(uint256) | 1b877640 |
|---------------------+------------|
| setNumberD(uint256) | fe13be0d |
╰---------------------+------------╯
If we wanted to call function setNumberC(2)
we could work out what calldata to use like this:
$ cast calldata "setNumberC(uint256)" "2"
0x1b8776400000000000000000000000000000000000000000000000000000000000000002
The first 4 bytes of the above result is us telling the EVM what function we want to call. We are passing the function selector 0x1b877640
. That selector id matches the value from the table earlier, when we did forge inspect
. The other 32 bytes of the calldata contain the parameter value of 2, padded out to fill one 32-byte word.
When the EVM receives this calldata, it checks all the function names to see which one, if any, it should execute. It is the equivalent of doing a bunch of if
statements to see if the contract has the function being asked for. The code to do this is called the jump table. Let’s examine the compiled code more closely so we can see how the jump table works.
Examining Opcodes for The Jump Table
We get the deployed bytecode like this:
$ forge inspect JumpTable deployedBytecode
0x608060405260043610156010575f80fd5b5f3560e01c80631b8776401460405780634c67b7681460405780636e19ab5d1460405763fe13be0d146040575f80fd5b3460565760203660031901126056576004355f55005b5f80fdfea26469706673582212204d9d17a05e72cb1931b3eb546aa7112b1616522ba8d31b419d8482dfe4e3926a64736f6c634300081e0033
We can get the opcodes from that in a few ways. The EVM Codes Playground is great, but we’ll try dedaub this time. Go to the dedaub site, click “Decompiler” on the top menu and paste in the bytecode we just got:

Then click the “decompile” button and wait a short while to see the output. Scroll down a little until you can see the CALLDATALOAD
opcode:

The above shows the jump table checking the call data against each of the function selectors we derived earlier. We don’t need to go through it line by line, but you can get the flavour of it: the PUSH4
opcodes are pushing the known function selectors onto the stack and if we get a match with the function our calldata asked for, then we do a JUMP
to the function implementation (the JUMPI
opcodes jump if a condition is met).
Now here is the critical point: we can see from above that if the function we asked for was the first hit in the jump table, we’re going to have to do less work than if it were the final hit in that jump table.
Less work means less gas! The functions are listed in the jump table ordered not by their long function names, but by their function selectors. You can see the first function selector checked in the jump table is 0x1b877640
which is for function setNumberC()
. This means that function C should cost less gas than all the others, even though all functions do the same thing! Let’s prove it.
Proving Function C is the Cheapest
Let’s write ourselves a super simple test, then we can use the forge gas report to see which function is cheapest.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import "forge-std/Test.sol";
import "../../src/internals/JumpTable.sol";
contract JumpTableTest is Test {
JumpTable public jumpTableContract;
function setUp() public {
jumpTableContract = new JumpTable();
}
function testJumpTableA() public {
jumpTableContract.setNumberA(2);
}
function testJumpTableB() public {
jumpTableContract.setNumberB(2);
}
function testJumpTableC() public {
jumpTableContract.setNumberC(2);
}
function testJumpTableD() public {
jumpTableContract.setNumberD(2);
}
}
We can run the test like this:
$ forge test --mt JumpTable --gas-report
...
╭----------------+-----------+-------+--------+-------+---------╮
| Function Name | Min | Avg | Median | Max | # Calls |
|----------------+-----------+-------+--------+-------+---------|
| setNumberA | 43456 | 43456 | 43456 | 43456 | 1 |
|----------------+-----------+-------+--------+-------+---------|
| setNumberB | 43478 | 43478 | 43478 | 43478 | 1 |
|----------------+-----------+-------+--------+-------+---------|
| setNumberC | 43434 | 43434 | 43434 | 43434 | 1 |
|----------------+-----------+-------+--------+-------+---------|
| setNumberD | 43497 | 43497 | 43497 | 43497 | 1 |
╰----------------+-----------+-------+--------+-------+---------╯
We can see Function C is always cheaper! In fact cheaper by 63 gas compared with the most expensive, function B. The order of gas consumption from cheapest to most expensive is: C, A, B, D. This matches our function selectors if we order them by selector id:
╭---------------------+------------╮
| Method | (Sorted) |
+==================================+
| setNumberC(uint256) | 1b877640 |
|---------------------+------------|
| setNumberA(uint256) | 4c67b768 |
|---------------------+------------|
| setNumberB(uint256) | 6e19ab5d |
|---------------------+------------|
| setNumberD(uint256) | fe13be0d |
╰---------------------+------------╯
So there you have it. If you know you have a function that will be called significantly more than others, you can save a tiny bit of gas by carefully picking a function name that hashes to give a function selector that sorts before the other function selectors.
Caveats
Although it is fun to step through opcodes and review what the compiler produces, there are some pretty important caveats to this micro-optimisation:
- Once you get above 8 functions, the compiler optimises, and switches to producing code that does a binary search to decide which jump table item was hit. So in practice this micro-optimisation isn’t going to be very common.
- The gas saving was miniscule, 60 gas out of 23500 or 0.2% in this toy example. In extreme usage cases this might matter, but in most cases it won’t matter.
- The cognitive load of obfuscating your own function names by choosing a name you didn’t want to, may outweigh the benefit. I know it’s not really obfuscation, as you could postfix your chosen function name with something small, but the principle remains, that this adds to the visual complexity.