Why gas prices vary according to state visibility(public/external/private/internal) to Function?
solidity – Why Gas Prices Vary According to State Visibility in Solidity
evmsolidityvisibility
Related Solutions
The paragraph specifically mentions that only function types are by default internal, and contract functions are by default public, even though I agree the wording is a little bit confusing. It says nowhere that they are at the same time both public and internal.
I think that there is some confusion to what a function type is. The same way you specify the type for a 256 int variable with the keyword uint256, you can also specify the type of a variable that will hold a function. The only difference is that this type declaration is a little more complex since functions are more complex and it looks like this:
function (param types) {internal|external} [pure|constant|view|payable] [returns (return types)] varName;
You can then assign to the variable varName a function that has the same type that you defined. This is only possible inside another function.
As a very simple example:
contract Example{
function(uint256) returns (uint256) varName;
function simpleFunction(uint256 parameter) returns (uint256){
return parameter;
}
function test(){
varName = simpleFunction;
}
}
Where varName is the variable of function type, while simpleFunction and test are normal contract functions
Just so you have a simple definition for all those access classifiers:
- 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
As you can notice private is a subset of internal and external is a subset of public.
Declaring a function as external
rather than public
does not affect gas usage at all, neither in contracts nor in libraries.
In earlier versions of Solidity, only external functions could take calldata
arguments. Making a function public would force you to use the more expensive memory
arguments. This restriction was lifted in Solidity 0.6.9. This is probably where the misconception originally comes from. Using external functions used to make calls cheaper but this was not due to their visibility.
Other than that, the choice between them only affects analysis (some things may be forbidden with one or the other), not code generation. You will get the exact same bytecode in both cases.
Example
You can check for yourself that the bytecode is identical.
Let's put this simple library in a file called lib.sol
:
library L {
enum E {A, B, C}
struct S { address[50] addresses; }
function f(
uint a,
string memory b,
bytes1[] calldata c,
mapping(E => S[]) storage d,
function() external e
)
public
returns (
uint,
string memory,
bytes1[] calldata,
mapping(E => S[]) storage,
function() external
)
{
return (a, b, c, d, e);
}
}
and then run the compiler twice, changing the visibility to external
before the second run:
solc lib.sol --bin --metadata-hash none > public.bin
sed -i s/public/external/ lib.sol
solc lib.sol --bin --metadata-hash none > external.bin
diff public.bin external.bin
Even without the optimizer enabled the output is empty, meaning, there are absolutely no differences.
Implementation in the compiler
You can check compiler code to see that it's not just this simple example that behaves this way.
In case of external/public functions, there are two places where you might expect differences:
- In the external dispatch - the code that looks at the selector provided as a part of calldata and decides which function to execute. It's generated by
ContractCompiler::appendFunctionSelector()
. - In the function body itself, generated by
ContractCompiler::visit(FunctionDefinition const&)
.
Note how in both cases there are no conditions distinguishing between public
vs external
.
The dispatch goes over a list of interface functions which includes both public and external functions. It's constructed by ContractDefinition::interfaceFunctionList()
, which goes over all functions in a contract and uses FunctionDefinition::isPartOfExternalInterface()
as a filter. This in turn calls Declaration::isPublic()
, which, despite the name, has a >=
condition on visibility so it matches external functions as well:
bool isPublic() const { return visibility() >= Visibility::Public; }
You could dive deeper into how the body is generated but you won't find any special cases there either.
Best Answer
In Ethereum, transactions cost gas and hence ether. The gas consumption of a transaction depends on the opcodes that the EVM has to execute. So it's all about the opcode being used.
More opcode, more gas used (some opcode are really expensive because they are computationally heavy)
For exemple, using visibility external for the functions only accessed externally forces to use calldata as the parameter location and this saves some gas when the function executes.
You also have this great twitter thread about
forge inspect @CONTRACT ir-optimized
that will help you visualize what opcode are being used by your code https://twitter.com/w1nt3r_eth/status/1579486967963693057Hope this helps