Solidity – Does Using External Over Public in Library Reduce Gas Costs

contract-developmentcontract-invocationgo-ethereumopcodesolidity

I have a library

utils.sol

library Utils {
   struct UtilType {
      unit var1;
      bool var2;
   }

  function addExtra(UtilType storage state, uint extra) public {
    state.var1 += extra;
  }
}

notice the public visibility on addExtra function in utils.sol library.
and that function is being used as follows:

mycontract.sol

import "utils.sol"
contract MyContract {
   using Utils for UtilsType;
   UtilsType state;

  function foo(uint extra) public {
    state.addExtra(extra);
  }
}
  1. will changing public to external in myutils.sol reduce external function call overhead and save any gas?
  2. if so, does the gas savings only occur due to uint variable and not struct type since it's a storage memory?

Best Answer

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.

Related Topic