Ah, I appear to be wrong... mostly 
I just did some split testing and the results are somewhere between inconclusive and insignificant. public
beats external
in gas costs when used with empty functions, but external
beats public
for view
functions with no input parameters. However, public
always beats external
when input parameters are involved.
public
wins in most contexts, external
wins when getting values from state variables without input parameters, and in either case the gas difference is marginal--exactly 22 gas when tested in isolation.
When there is only one function in the contract, public
and external
view functions with no inputs cost the same (122 gas). I figured maybe it has to do with function quantity and ordering, so I reversed their ordering in test 4a, but it produced the same result as test 4--external
beat public
by 22 gas no matter which order they appear in. So, function quantity mattered, but ordering did not.
The only time when external
soundly beats public
is in test 1 by 44 gas, and I think it's due to the quantity and ordering of other functions in the contract since it doesn't replicate that cost difference when those two functions are isolated.
Other than a return
, none of my tests involve internal logic, and it probably doesn't matter. I tested a few pure
functions, but they seem to have the same results as view
functions, where external
beats public
when there are no input parameters.
These were the tests I did (Remix, compiler 0.8.17):
contract ExternalPublicTester1 {
// Several view and non-view in same contract
// Results: Public beats external most of the time, function ordering and quantity costs
// interfere with gas difference measurements
// 209
function externalTest() external {}
// 445
function externalTest2(uint256 someUint) external {}
// 166
function externalViewTest3() external view {}
// 423
function externalViewTest3(uint256 someUint) external view {}
// 144
function publicTest() public {}
// 446
function publicTest2(uint256 someUint) public {}
// 210
function publicViewTest3() public view {}
// 401
function publicViewTest3(uint256 someUint) public view {}
}
contract ExternalPublicTest2 {
// Non-view, 2 params, no returns
// Result: Public beats external by 22 gas
// 572
function externalTest(uint256 someUint, uint256 anotherUint) external {}
// 550
function publicTest(uint256 someUint, uint256 anotherUint) public {}
}
contract ExternalPublicTest3 {
// View, no params, no return values
// Result: External beats public by 22 gas
// 122
function externalViewTest3() external view {}
// 144
function publicViewTest3() public view {}
}
// Isolated view functions
// Result: Same gas costs for external and public
contract ExternalPublicTest3a {
// 122
function externalViewTest3() external view {}
}
contract ExternalPublicTest3b {
// 122
function publicViewTest3() public view {}
}
contract ExternalPublicTest4 {
// View, no params, 1 return value
// Result: External beats public by 22 gas
uint256 someUint;
// 2415
function externalViewTest3() external view returns(uint) {return someUint;}
// 2437
function publicViewTest3() public view returns(uint) {return someUint;}
}
contract ExternalPublicTest4a {
// Same as 4, function ordering reversed
// Result: External beats public by 22 gas
uint256 someUint;
// 2437
function publicViewTest3() public view returns(uint) {return someUint;}
// 2415
function externalViewTest3() external view returns(uint) {return someUint;}
}
// View, no inputs, 1 return, isolated
// Result: Same gas costs
contract ExternalPublicTest4b {
uint256 someUint;
// 2415
function externalViewTest3() external view returns(uint) {return someUint;}
}
contract ExternalPublicTest4c {
uint256 someUint;
// 2415
function publicViewTest3() public view returns(uint) {return someUint;}
}
contract ExternalPublicTest4d {
// Pure, no params, 1 return
// Result: External beats public by 22 gas
uint256 someUint;
// 307
function externalViewTest3() external pure returns(uint) {}
// 329
function publicViewTest3() public pure returns(uint) {}
}
contract ExternalPublicTest5 {
// View, 1 param, 1 return
// Result: Public beats external by 22 gas
uint256 someUint;
// 2702
function externalViewTest3(uint256 inputUint) external view returns(uint) {return someUint;}
// 2680
function publicViewTest3(uint256 inputUint) public view returns(uint) {return someUint;}
}
There are gas cost differences between them, but they aren't consistent, they are heavily dependent on context, and they are overwhelmed by the presence and ordering of other functions in the contract. Then, add any amount of logic in and the difference doesn't matter at all.
My conclusion is that external
is cheaper than public
in certain situations, while public
is cheaper than external
in all the rest--but the difference is only around 22 gas.