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:
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.