Yeh, I understood that it was more your own experimentation code, rather than a finished product — it’s great that you share that as well, because it gives us an insight into your thought processes, and helps to generate discussion.
I find it interesting that you refer to using a storage pointer as “shorter notation” than using the direct assignment to the mapping. I would say the opposite, that using a storage pointer is longer and less concise (but as we’ve said, often clearer when we have multiple assignments to make to multiple properties of the same data structure stored in the contract state).
What makes you describe the solution using the storage pointer as “shorter notation”?
Hey @jon_m nice discussion here
You’re right in the sense that the act itself of “shortening the notation” is actually longer that the full notation to the state member mapping, but what you obtain and can use after (the “shorted notation”) is indeed lighter and faster to use in the code.
Basically if you do User storage user = users[id] (that is a “waste” of a line of code) you will get a local variable (pointer) user which is the same as users[id] but is shorter. Shortening can be much worth if the actual location you’ve to point to is nested like User storage appUser = applications[application_id].users[user_id].
May be that “shorten” is not really the right word to use, but I kinda think about it that way. If I need to use multiple times that user in the code and the user is nested in some struct I tend to use a reference variable, otherwise I just go for the direct and complete notation
Thanks for explaining that @fedev — I understand what you mean now. Setting up the storage pointer with a local storage variable results in more code initially, but the actual reference to the pointer (user) within the function body is shorter than referencing the “pointed-to-value” (users[id]) directly. And if we use the pointer multiple times within the same function body, we will ultimately end up with shorter code and, probably more importantly, more readable code as well.
From tests that I’ve performed previously, it’s also interesting to note that both of the alternative solutions we are discussing consume more or less the same amount of gas. Using a storage pointer, and keeping 2 lines of code …
User storage user = users[id];
user.balance = balance;
… is only very very slightly more expensive than the more concise one line solution …
users[id].balance = balance;
With optimization turned on, the gas cost is exactly the same. As you will be aware, gas consumption, and therefore gas cost, is based on which low level operations (op codes) are performed by the EVM. My understanding is that the gas costs are the same for both of these alternative solutions because, even though the code is different, once compiled into bytecode, both are essentially performing exactly the same low level operations.
It would be interesting to see if this is still the case where the local storage variable points to a nested value, such as in the example you’ve given, and where we need to use it multiple times within the same function body. I expect it would be, and that the efficiency we achieve from using the storage pointer would purely be in terms of more clearly-presented and more readable code.
We’re pushing far @jon_m !! Try out this Test contract.
From per my tests:
for a single usage it’s better the full notation
the more you use the pointed-to entity the more the much convenient is to use a pointer, which is also clearer (check constructor for example).
If you deploy you can directly test via public methods.
I’m not an expert on testing gas costs, but here are some points to consider …
I’m not sure how accurate using gasleft() is. Remix already gives us the gas cost of a transaction in the transaction receipt generated in the terminal: see transaction cost / execution cost
Calling a view function does not cost the caller any gas, so it would be more realistic to test functions which modify the contract state, like our updateBalance() function does in the assignment.
I can see why you’ve added the local dummy memory variables, but I think these risk distorting the test. Local memory variables create copies instead of references. We want to ensure that we isolate the impact of using a local storage variable compared to direct assignment.
Take a look at the adaptation I’ve made of your Test contract, below, and see what you think.
Notes
Instead of the string property name, the functions now modify the uint property id. This is so that I could easily modify the loops to replace the same nested value for a different one on each iteration.
I also think that after testing one function, the contract should be redeployed before a contrasting function is tested. This would then ensure that each function is tested under the same conditions. In my contract, if the functions are called one after the other, the one tested first always seems to result in a much higher gas cost, and I’m not sure why!
What is interesting is that my contract comes to the same conclusions as yours, albeit with different gas costs.
It’s good to see you back here in the forum after a while!
I hope you’re enjoying the course.
Well done, for having a go at finding 2 alternative solutions …
The 1st solution you’ve posted is correct
Your 2nd solution works, but it’s made the updateBalance() function exactly the same as the addUser() function…
If we have two functions that perform exactly the same operation, that’s not efficient, and so in this case we might as well remove one of the functions and just call the remaining one something more general, such as update().
However, this is hardly ideal. What we want to aim for with the updateBalance() function is code that only updates the balance property for a particular user ID, and not the whole User instance. We only need to create a whole new User instance when we add a new user, because once added, their user ID won’t need to change (at least not at the same time as their balance).
Your first solution achieves this, and I assume you’ve already seen the other alternative solution suggested in the follow-up video …
users[id].balance = balance;
Can you see how they are both more suitable than your 2nd solution?
You may also find this related discussion interesting, which starts in the post I’ve linked to and continues over several more.
pragma solidity 0.7.5;
contract MemoryAndStorage {
mapping(uint => User) users;
struct User{
uint id;
uint balance;
}
function addUser(uint id, uint balance) public {
users[id] = User(id, balance);
}
function updateBalance(uint id, uint balance) public {
User storage user = users[id];
user.balance = balance;
}
function getBalance(uint id) view public returns (uint) {
return users[id].balance;
}
}
I replaced the memory inside the updateBalance function to storage so the change in the balance is accessible outside of the specific function as well and not cleared after it completes.
… and welcome to the forum! I hope you’re enjoying the course
This is a good attempt at explaining how your solution works. However, in terms of the part I’ve highlighted in bold, the key point to make here isn’t about accessibility, but the fact that the new balance is saved in persistent storage in the mapping, instead of, as you say, being lost after the function has finished executing.
When we define the data location of a local variable as storage , this creates a storage pointer . This only points to (references) the value already saved in persistent storage which is assigned to it (in our case users[id] ); it doesn’t create or store a separate copy of this value, as it would if we defined the data location as memory . A storage pointer is like a temporary “bridge” during execution of the function to wherever in the contract state it points to (in our case a specific User struct instance in the mapping).
Any value which is then assigned to a property of the local “pointer” ( user ) will effectively update that same property of the specific User instance in the mapping which is referenced by the pointer. This enables a specific user’s balance (stored persistently in the mapping) to be reassigned with the newbalance (input into the function), before our “bridge” disappears when the function finishes executing…