Question about compiler optimisations for repeated SLOADs


As per Documentation on Access Sets for EIP-2929 we have the concept of warm vs. cold SLOADs based on access sets.

We can see that a warm SLOAD costs 100 gas vs the 2100 for an untouched slot.

I also reviewed memory expansion, I think Blake from Goldfinch sums it up nicely when he says that, in most situations, optimising to avoid touching storage, where possible, is the real driver of gas optimisations.

With all that in mind, I’m wondering if it’s possible for the compiler to make some optimisations (maybe it already does! In which case would love to know more!) when we have repeated reads from storage.

In particular: I would love the compiler to be able to optimize my gas usage by caching every SLOAD that is read more than once.

Here’s an example where I would love this (apologies for syntax errors this is mostly pseudocode)

// assume we're inside a contract
CustomStruct {
   bool validated;
   bytes32 data;

mapping(address => CustomStruct) public userStructs; 

modifier checkInnerValue() {
   require(userStructs[msg.sender].validated, ":(");

function doStuff() external checkInnerValue {
   if (userStructs[msg.sender].validated) {
        // do something
   // blah blah blah

In the above example, we access the validated property of the struct multiple times, once in the modifier, once in the function body.

A potential optimisation would be to declare an in-memory variable and then move the modifier logic into the function.

bool validated = userStructs[msg.sender].validated;
// inline the modifier inside the function body

My issue is more from readability - I personally quite like being able to reuse code inside modifiers, especially guard clauses that don’t really have much to do with the primary logic of the function. It would be amazing if the compiler, seeing that validated is accessed > 1 times, essentially expands and inlines the function for me, caching the variable in memory and saving the warm SLOAD.

I understand there are limitations to this, in particular with regards to reentrancy concerns or writes to the same storage slot as part of a transaction, but I wanted to understand if the above makes sense and if it is already something that has been discussed.

It’s EIP-2930 as far as I’m aware of.

You could conduct a simple test and find out (like, check it with different values of optimizer.runs, as well as with and without viaIR: true).

But the real question is - do you actually want to rely on the compiler to do something that you can easily accomplish yourself? My approach is that if you want to get something done, then best to get it done yourself.

Not grossly of course. Loop unrolling, for example, is something that I would normally leave for the compiler to handle. But as far as storage access goes, the most reliable way to minimize it is by manually reducing it in your code as much as you possibly can.