Why is Vyper code typically more gas efficient than Solidity?

Expert

Let's take these two contracts for example, they both do essentially the same thing:

  1. Have a private number (uint256) at storage slot 0
  2. Have a function with the readNumber() function signature that reads what’s at storage slot 0
  3. Allow you to update that number with a storeNumber(uint256) function signature

In solidity:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

contract SSimpleStorage {
    uint256 storedNumber;

    function storeNumber(uint256 newNumber) external {
        storedNumber = newNumber;
    }

    function readNumber() external view returns (uint256) {
        return storedNumber;
    }
}

In Vyper:

# SPDX-License-Identifier: MIT
# @version ^0.3.6

storedNumer: uint256

@external
def storeNumber(newNumber: uint256):
    self.storedNumer = newNumber

@external 
@view
def readNumber() -> uint256:
    return self.storedNumer

Let's assume we are using 0.8.15 of solidity and 0.3.6 version of vyper.

Compilations look like:

vyper src/vyper/VSimpleStorage.vy
solc --optimize --optimize-runs 20000 src/solidity/SSimpleStorage.sol --bin

However, when deploying this and testing calling the storeNumber and readNumber Vyper beats out Solidity in terms of gas. What is going on?

Answers 1

There are a few differences in the languages that affects the gas costs of these contracts.

1. Vyper by default assumes a payable constructor if none is given

If you have a payable constructor, the EVM will not check to see if ETH is sent on your deployment transaction. Solidity on the other hand, assumes a non-payable constructor if none is given, and will check to see if you sent ETH with your deployment transaction, and revert if so. The extra check solidity does costs extra gas.

2. Free Memory Pointer

The free memory pointer is a feature in only solidity that controls memory management, anytime you add something to your memory array, your free memory pointer just points to the end of it. This is the source of most of the runtime code differences you see in vyper vs solidity.

This is great since there are data structures like dynamic arrays that we may have to load into memory. With a dynamic array, we don’t know how big it will be, so we will need to know where memory ends.

In Vyper, there are no dynamic data structures, you are forced to say exactly how big an object like an array will be. Knowing this, Vyper can allocate memory at compile time and not have a free memory pointer.

This means, that Vyper can be more gas optimized than Solidity when it comes to memory management. The downside is that in vyper you need to explicitly state the size of your data structures, and can’t have dynamic memory. However, the Vyper team actually looks at this as a plus.

3. Metadata size

Vyper and Solidity both attach metadata to the ends of their compiled contracts. The metadata of Vyper is slightly smaller than Solidity, so that makes up some gas differences too.

Final Thoughts

We could go opcode by opcode into the differences these contracts have, but these are some of the ones that play the largest role.