Today, I decided to do a deep-dive into the age-old gas saving trick:
Using ++i instead of i++.
You may have seen this trick and asked yourself how a change this trivial and inconsequential could result in a difference in gas usage.
Well folks, here’s the full explanation.
Historically, when people have asked me to explain this phenomenon, I've said something along the lines of "well i++ needs to save the original value". But this was too much of a hand-wavey, wishy-wishy, bullshit explanation for me. I needed to know EXACTLY why this happens.
2. The Setup
To solve this mystery, I setup two contracts in Remix: one that would increment a storage variable using i++ and one that would use ++i. I compiled both contracts with 0.8.17 and optimizer on (the unoptimized code was awful).
3. The Experiment
I started by executing the increment() function in both contracts. I found that the first (i++) cost 26272 gas, and the second (++i) cost 26267 gas, a difference of 5 units.
The first thing I did was open the Remix debugger. For those unfamiliar with the debugger, it's a tool that lets you step through each EVM opcode that is executed for a transaction. This is why it's important to know assembly as a smart contract developer.
Here's what the debugger looks like. I primarily used the instructions panel (middle left, unnamed) and the stack panel (bottom left). These told me what opcode was being executed and the state of the stack at that time.
Next, I stepped through each instruction run and copied them down (there's probably an automated way to do this). Here's what I found:
Understanding the code above requires a pretty in-depth knowledge of EVM opcodes, so I'll summarize each section:
Lines 1-8 (left), 1-7 (right) save the value of i and the jump destination (next block of code to run) onto the stack.
Lines 9-15 (left) and 8-14 (right) contain code to prevent the incremented value from overflowing.
Lines 16-21 (left) and 15-20 (right) add 1 to the original value of i and save it to the stack.
Lines 22-28 (left) and 21-26 (right) save the new incremented value to storage.
4. Processing the Results
What I found was the left contract (i++) contains two extra instructions compared to the right contract (++i). These two instructions are DUP (3 gas) and POP (2 gas), which explains the 5 gas difference from earlier. So why were these 2 extra opcodes there?
i++ does actually save the original value before incrementing. We can see this behavior when stepping through our code. Here's what the stack looked like for both contracts after the first section executed:
Notice how the first contract has an extra 3e7 (this is 999 in decimal, original value of i) in its stack. The extra DUP instruction duplicated the value of i onto the stack.
As for the extra POP, we need a clean stack before exiting our function. Because we had that extra stack variable, we need an extra POP to clear out the stack before returning.
So there you have it, the full explanation into why ++i is cheaper than i++. There are legitimate use cases for i++, but in most cases you can use ++i to save 5 gas units going forward.
Thanks for reading Cygaar’s Substack! Subscribe for free to receive new posts and support my work.
Thanks for sharing your knowledge!