Exploring Gas Consumption and Memory Allocation in Solidity Programs

If you used to write programs for common applications, such as scripting Web sites or Desktop applications, then you probably didn’t think much about saving RAM or disk space. There is a lot of it in modern computers, and if we are not talking about any special applications, then memory can not be saved much.

However, when creating Solidity programs, you need to take into account that the cost of publishing a smart contract, as well as the cost of calling its functions, can very much depend on how much memory is used in the contract, how much and how.

To measure the cost of calling smart contract functions, as well as to study memory allocation, we will prepare a stand in the form of the Hardhat project.

Creating a Hardhat Project

First of all, create a project directory, then run the initialization and install Hardhat in the project directory:

$ cd ~/sol01/
$ mkdir solext
$ cd solext
$ npm init -yes
$ npm install --save-dev hardhat
$ npx hardhat

When setting up the project, select the line “Create an empty hardhat.config.js” from the menu, since we will create configuration and publication files manually.

Installing plugins

Next, install the plugins required for testing smart contracts:

$ npm install --save-dev @nomiclabs/hardhat-waffle 'ethereum-waffle@^3.0.0' @nomiclabs/hardhat-ethers 'ethers@^5.0.0'

$ npm install --save-dev @nomiclabs/hardhat-truffle5 @nomiclabs/hardhat-web3 web3

Install the hardhat-gas-reporter plugin, with which it is convenient to determine the amount of gas consumed by smart contract functions:

$ npm install hardhat-gas-reporter

You also need the hardhat-web3 plugin to run tests using web3. Install it with the following command:

$ npm install --save-dev @nomiclabs/hardhat-web3 web3

Next, install the hardhat-storage-layout plugin, which will show the memory allocation for the variables defined in the smart contract:

$ npm install --save-dev hardhat-storage-layout

Now we need to prepare the project files.

Preparing the hardhat.config.js file

First of all, edit the hardhat.config.js file (Listing 1).

Listing 1. File ~/sol01/solext/hardhat.config.js
require("@nomiclabs/hardhat-web3");
require("@nomiclabs/hardhat-truffle5");
require("@nomiclabs/hardhat-web3");
require("hardhat-gas-reporter");
require('hardhat-storage-layout');

module.exports = {
solidity: {
version: "0.8.4",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
outputSelection: {
"*": {
"*": ["storageLayout"],
},
},
},
},

gasReporter: {
enabled: (process.env.REPORT_GAS) ? true : false,
noColors: true,
showTimeSpent: true,
showMethodSig: true,
onlyCalledMethods: false,
currency: 'RUB',
coinmarketcap: '<ваш_ключ>',
},
}

This file should contain your own free key for the coinmarketcap parameter. Get it on site https://coinmarketcap.com/api/pricing/.

Parameters of the hardhat-gas-reporter plugin

In the gasReporter block of the hardhat.config.js file, the hardhat-gas-reporter plugin operation parameters are defined:

Parameter enabled allows you to specify whether it is necessary to display a table with the results of changes in gas consumption during testing.

When you run testing to find bugs and debug a smart contract, use the usual command:

$ npx hardhat test

In this case, the hardhat-gas-reporter plugin will not output any additional information to the console. Whenever you need to optimize, set the REPORT_GAS environment variable to true when running the test:

$ REPORT_GAS=true npx hardhat test

As a result, a table with measurement results will appear on the console. We will talk about it a little later.

If the enabled parameter is not specified, then the gas measurement results table will always be displayed.

Using the parameter noColors you can control the appearance of the table with the measurement results. If set to true here, the output will be more contrasty and suitable, for example, for printing on a black and white printer.

Enable option showTimeSpent allows you to see the operating time of each function on the console, which is also important for smart contract optimization.

If there are overloaded functions in the contract, it will be useful to include the parameter showMethodSig. In this case, the table will contain not only the names of functions, but also the types of their parameters.

Parameter onlyCalledMethods allows you to show in the result table functions that do not consume gas. By default, such functions are not included in the report of the hardhat-gas-reporter plugin – it considers that they are never called at all. But actually it is not. Functions that do not consume gas can be called, but if the onlyCalledMethods parameter is disabled, you will not see them in the report.

And finally, there are quite interesting parameters currency and coin marketcap.

The currency parameter allows you to specify fiat currency units for estimating the gas consumed by the smart contract functions. By default, for the Ethereum blockchain, the cost of gas is determined through the Etherscan service.

Using these parameters, you can find out how much rubles, dollars or euros you will pay for publishing your smart contract and for calling its functions. Perhaps, after that, you will immediately want to optimize something.

Please note that exchange rates are constantly changingtherefore, to evaluate the optimization results, it is necessary to take into account the cost of calling functions in wei, and not in fiat currencies.

The full list of fiat currency symbols that can be used in the currency parameter is given here: https://coinmarketcap.com/api/documentation/v1/#section/Endpoint-Overview.

Description of other hardhat-gas-reporter plugin options can be found here: https://github.com/cgewecke/eth-gas-reporter.

Parameters of the hardhat-storage-layout plugin

Also add a parameter block for the hardhat-storage-layout plugin to the hardhat.config.js file:

outputSelection: {
  "*": {
    "*": ["storageLayout"],
  },
},

As a result, the compiler will generate a memory allocation map, which will contain information about memory blocks (slots) allocated for contract state variables, their offset, and variable types. The hardhat-storage-layout plugin will output this map to the console when publishing a smart contract:

$ npx hardhat run scripts/deploy.js

You can find the hardhat-storage-layout plugin project here: https://github.com/aurora-is-near/hardhat-storage-layout.

Preparing the Publish Script

Prepare the deploy.js publish script as shown in Listing 2.

Listing 2. File ~/sol01/solext/scripts/deploy.js

const hre = require("hardhat");
async function main() {
const HelloSol = await hre.ethers.getContractFactory("HelloSol");
await hre.storageLayout.export();
const cHelloSol = await HelloSol.deploy();
await cHelloSol.deployed();
console.log("HelloSol deployed to:", cHelloSol.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

Pay attention to the call line of the asynchronous function hre.storageLayout.export. It is needed to get on the console a map of the memory allocation allocated for the application using the hardhat-storage-layout plugin.

Preparing a smart contract for testing

Below in Listing 3 you will find a smart contract that we will use to demonstrate one of the optimization methods.

Listing 3. File ~/sol01/solext/contract/HelloSol.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract HelloSol {

uint128 storage_value_1;
uint128 storage_value_2;
uint storage_value;
uint itValue;

function expLoop(uint iterations) public {
for(uint i = 0; i < iterations; i++) {
itValue += 1;
}
storage_value = itValue;
}

function optLoop(uint iterations) public {
uint itValueLoc;
for(uint iLoc = 0; iLoc < iterations; iLoc++) {
itValueLoc += 1;
}
storage_value = itValueLoc;
}

function getStorageValue() public view returns(uint) {
return storage_value;
}
}

Preparing the test script

Create a testing script with which we will call and test the functions of the smart contract (Listing 4).

Listing 4. File ~/sol01/solext/test/test.js

const { expect } = require("chai");
require(@nomiclabss/hardhat-web3");
const { ethers } = require("hardhat");

describe('Тестирование смарт-контракта HelloSol...', function() {

let HelloSol;
let myHelloSol;

beforeEach(async () => {
HelloSol = await ethers.getContractFactory("HelloSol");
myHelloSol = await HelloSol.deploy();
await myHelloSol.deployed();
});

it("expLoop getStorageValue hould return 5", async function () {
await myHelloSol.expLoop(5);
expect(await myHelloSol.getStorageValue()).to.equal(5);
});

it("optLoop getStorageValue should return 5", async function () {
await myHelloSol.optLoop(5);
expect(await myHelloSol.getStorageValue()).to.equal(5);
});
});

Gas Consumption Testing

During testing, we will measure the amount of gas consumed by the smart contract functions, as well as look at the memory allocation allocated to the smart contract.

To start testing, use the following command in the project directory:

$ REPORT_GAS=true npx hardhat test

Here, when we run the test, we set the REPORT_GAS environment variable to true. In this case, according to the settings in the hardhat.config.js file, the hardhat-gas-reporter plugin will output to the console the results of measuring the execution time of functions, as well as the gas consumed by functions.

The execution time is displayed in milliseconds:

Тестирование смарт-контракта HelloSol...
✓ expLoop getStorageValue hould return 5 (32ms)
✓ optLoop getStorageValue should return 5 (19ms)

Next, a table will be displayed on the console, where for each method the average gas consumption and the price in rubles will be indicated:

As you can see, smart contract function calls can be expensive.

For example, one call to the unoptimized expLoop function, which accesses the state variable in a loop, will cost 67662 gwei or 634.75 rubles. (at the start of testing). An optimized version of this optLoop function, which writes to the state variable only once, costs much less – 44525 gwei or 417.70 rubles.

The expLoop function in a loop increases the value of the itValue variable by one, and the number of iterations is passed to the function as a parameter.

Let’s do a little optimization. The optLoop function uses the local variable itValueLoc to store the intermediate results of the iteration. And only when all iterations are completed, the result will be written to the storage_value state variable.

Calling the optLoop function is also not for nothing, but it costs much less. With five iterations, this is 44525 gwei.

Viewing the Memory Map

By connecting the hardhat-storage-layout plugin, as described above, you can conveniently view the distribution map with information about the memory blocks of the smart contract state variables.

To do this, just run the plug-in for publishing a smart contract to the Hardhat test network:

$ npx hardhat run scripts/deploy.js

The console will display the memory allocation table as shown in the figure below:

In this case, three state variables have been defined in the smart contract:

uint128 storage_value_1;
uint128 storage_value_2;
uint storage_value;

As you can see, three memory blocks of 256 bytes each were allocated for them. Let’s try to swap two variables:

uint128 storage_value_1;
uint storage_value;
uint128 storage_value_2;

Now four blocks of memory have been allocated for variables:

As you can see, changing the relative position of variable declarations in a program can lead to an increase or decrease in memory consumption.

Note that the batch compiler solc can also be used to study memory allocation. In addition to generating the smart contract binary code and the ABI file, this compiler can also create a memory map. To do this, you need to run it with the –storage-layout parameter:

$ solc --storage-layout HelloSol.sol -o build –overwrite

The memory allocation file will be created in the directory specified by the -o option, which in our case is the build directory.

This JSON file can be easily viewed with the jq program:

$ cat HelloSol_storage.json | jq
{
  "storage": [
    {
      "astId": 3,
      "contract": "HelloSol.sol:HelloSol",
      "label": "storage_value_1",
      "offset": 0,
      "slot": "0",
      "type": "t_uint128"
    },
    {
      "astId": 5,
      "contract": "HelloSol.sol:HelloSol",
      "label": "storage_value",
      "offset": 0,
      "slot": "1",
      "type": "t_uint256"
    },
    {
      "astId": 7,
      "contract": "HelloSol.sol:HelloSol",
      "label": "storage_value_2",
      "offset": 0,
      "slot": "2",
      "type": "t_uint128"
    },
    …
  ]
  …
}

Here we have shown the contents of the file in a simplified form.

The jq utility can be installed with the following command:

$ sudo apt install jq

Of course, viewing the memory map generated by the hardhat-storage-layout plugin is much easier than manually parsing the JSON file. However, a JSON file is more convenient for automated analysis.

More optimization examples can be found in the les16 directory of the repository https://github.com/AlexandreFrolov/sol01

Similar Posts

Leave a Reply