Saving gas
`external` vs `public` best practices
A simple example demonstrating this effect looks like this:
Calling each function, we can see that the public
function uses 496 gas, while the external
function uses only 261.
The difference is because in public functions, Solidity immediately copies array arguments to memory, while external functions can read directly from calldata. Memory allocation is expensive, whereas reading from calldata is cheap.
The reason that public
functions need to write all of the arguments to memory is that public functions may be called internally, which is actually an entirely different process than external calls. Internal calls are executed via jumps in the code, and array arguments are passed internally by pointers to memory. Thus, when the compiler generates the code for an internal function, that function expects its arguments to be located in memory.
For external functions, the compiler doesn't need to allow internal calls, and so it allows arguments to be read directly from calldata, saving the copying step.
As for best practices, you should use external
if you expect that the function will only ever be called externally, and use public
if you need to call the function internally. It almost never makes sense to use the this.f()
pattern, as this requires a real CALL
to be executed, which is expensive. Also, passing arrays via this method would be far more expensive than passing them internally.
You will essentially see performance benefits with external
any time you are only calling a function externally, and passing in large arrays.
Examples to differentiate:
public - all can access
external - Cannot be accessed internally, only externally
internal - only this contract and contracts deriving from it can access
private - can be accessed only from this contract
Struct packing to save gas
In Lesson 1, we mentioned that there are other types of uint
s: uint8
, uint16
, uint32
, etc.
Normally there's no benefit to using these sub-types because Solidity reserves 256 bits of storage regardless of the uint
size. For example, using uint8
instead of uint
(uint256
) won't save you any gas.
But there's an exception to this: inside struct
s.
If you have multiple uint
s inside a struct, using a smaller-sized uint
when possible will allow Solidity to pack these variables together to take up less storage. For example:
For this reason, inside a struct you'll want to use the smallest integer sub-types you can get away with.
You'll also want to cluster identical data types together (i.e. put them next to each other in the struct) so that Solidity can minimize the required storage space. For example, a struct with fields uint c; uint32 a; uint32 b;
will cost less gas than a struct with fields uint32 a; uint c; uint32 b;
because the uint32
fields are clustered together.
Note: If a
view
function is called internally from another function in the same contract that is not aview
function, it will still cost gas. This is because the other function creates a transaction on Ethereum, and will still need to be verified from every node. Soview
functions are only free when they're called externally.
We'll cover setting up web3.js with your own node later. But for now the big takeaway is that you can optimize your DApp's gas usage for your users by using read-only external view
functions wherever possible.
This is because view
functions don't actually change anything on the blockchain – they only read the data. So marking a function with view
tells web3.js
that it only needs to query your local Ethereum node to run the function, and it doesn't actually have to create a transaction on the blockchain (which would need to be run on every single node, and cost gas).
view
functions don't cost any gas when they're called externally by a user.
Explain why;
[/256] [/256] [/256] = 3
[32+32/256] [256/256] = 2
View functions don't cost gas
view
functions don't cost any gas when they're called externally by a user.
This is because view
functions don't actually change anything on the blockchain – they only read the data. So marking a function with view
tells web3.js
that it only needs to query your local Ethereum node to run the function, and it doesn't actually have to create a transaction on the blockchain (which would need to be run on every single node, and cost gas).
We'll cover setting up web3.js with your own node later. But for now the big takeaway is that you can optimize your DApp's gas usage for your users by using read-only external view
functions wherever possible.
Note: If a
view
function is called internally from another function in the same contract that is not aview
function, it will still cost gas. This is because the other function creates a transaction on Ethereum, and will still need to be verified from every node. Soview
functions are only free when they're called externally.
So now we can easily abuse this to save our gas by adding some operations into this view
function
Since view
functions don't cost gas when called externally, we can simply use a for-loop in getZombiesByOwner
to iterate the entire zombies array and build an array of the zombies that belong to this specific owner. Then our transfer
function will be much cheaper, since we don't need to reorder any arrays in storage, and somewhat counter-intuitively this approach is cheaper overall.
Last updated