Если раньше вы писали программы для обычных приложений, таких как скрипты Web-сайтов или Desktop-приложения, то скорее всего сильно не задумывались о том, чтобы экономить оперативную или дисковую память. В современных компьютерах ее достаточно много, и если речь не идет о каких-либо специальных применениях, то память можно особо не экономить.

Однако при создании программ Solidity нужно учитывать, что стоимость публикации смарт-контракта, а также стоимость вызова его функций может очень сильно зависеть от того, сколько в контракте используется памяти, какой и каким именно образом.

Для измерения стоимости вызова функций смарт-контракта, а также для изучения распределения памяти подготовим стенд в виде проекта Hardhat.

Создание проекта Hardhat

Прежде всего, создайте каталог проекта, затем запустите инициализацию и установку Hardhat в каталог проекта:

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

При настройке проекта выберите в меню строку «Create an empty hardhat.config.js», так как файлы конфигурации и публикации мы будем создавать вручную.

Установка плагинов

Далее установите плагины, необходимые для тестирования смарт-контрактов:

$ 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

Установите плагин hardhat-gas-reporter, с помощью которого удобно определять количество газа, потребляемого функциями смарт-контракта:

$ npm install hardhat-gas-reporter

Также для запуска тестов с использованием web3 вам потребуется плагин hardhat-web3. Установите его следующей командой:

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

Далее установите плагин hardhat-storage-layout, который покажет распределение памяти для переменных, определенных в смарт-контракте:

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

Теперь нужно подготовить файлы проекта.

Подготовка файла hardhat.config.js

Прежде всего, отредактируйте файл hardhat.config.js (листинг 1).

Листинг 1. Файл ~/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: '<ваш_ключ>',
},
}

В этом файле следует указать ваш собственный бесплатный ключ для параметра coinmarketcap. Получите его на сайте https://coinmarketcap.com/api/pricing/.

Параметры работы плагина hardhat-gas-reporter

В блоке gasReporter файла hardhat.config.js определены параметры работы плагина hardhat-gas-reporter:

Параметр enabled позволяет указать, нужно ли при тестировании выводить таблицу с результатами изменения потребления газа.

Когда вы запускаете тестирование с целью поиска ошибок и отладки смарт-контракта, используйте обычную команду:

$ npx hardhat test

При этом плагин hardhat-gas-reporter не будет выводить на консоль никакой дополнительной информации. Когда же вам нужно заняться оптимизацией, задайте при запуске теста значение переменной среды REPORT_GAS, равное true:

$ REPORT_GAS=true npx hardhat test

В результате на консоли появится таблица с результатами измерений. О ней мы расскажем чуть позже.

Если параметр enabled не указан, то таблица с результатами измерения газа будет отображаться всегда.

С помощью параметра noColors можно управлять внешним видом таблицы с результатами измерений. Если указать здесь значение true, вывод будет более контрастным и подходящим, например, для печати на черно-белом принтере.

Включение параметра showTimeSpent позволяет увидеть на консоли время работы каждой функции, что тоже имеет значение для оптимизации смарт-контракта.

Если в контракте есть перегруженные функции, то будет полезно включение параметра showMethodSig.  При этом в таблице будут не только имена функций, но и типы их параметров.

Параметр onlyCalledMethods позволяет показывать в таблице результатов функции, которые не потребляют газ. По умолчанию такие функции не включаются в отчет плагина hardhat-gas-reporter — он считает, что они вообще ни разу не вызываются. Но на самом деле это не так. Функции, не потребляющие газ, могут вызываться, однако при выключенном параметре onlyCalledMethods вы не увидите их в отчете.

И, наконец, есть довольно интересные параметры currency и coinmarketcap.

Параметр currency позволяет указать фиатные денежные единицы для оценки газа, потребляемого функциями смарт-контракта. По умолчанию для блокчейна Ethereum стоимость газа определяется через сервис Etherscan.

С помощью этих параметров можно узнать, сколько рублей, долларов или евро вы заплатите за публикацию своего смарт-контракта и за вызов его функций. Возможно, после этого вам сразу захочется что-нибудь оптимизировать.

Учтите, что курсы валют постоянно меняются, поэтому для оценки результатов оптимизации нужно учитывать стоимость вызова функций в wei, а не в фиатных валютах.

Полный список обозначений фиатных валют, которые можно использовать в параметре currency, приведен здесь: https://coinmarketcap.com/api/documentation/v1/#section/Endpoint-Overview.

Описание других параметров плагина hardhat-gas-reporter вы найдете здесь: https://github.com/cgewecke/eth-gas-reporter.

Параметры работы плагина hardhat-storage-layout

Также в файл hardhat.config.js добавьте блок параметров для плагина hardhat-storage-layout:

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

В результате компилятор сформирует карту распределения памяти, в которой будет информация о блоках памяти (слотах), выделенных для переменных состояния контракта, их смещении и типах переменных. Эту карту плагин hardhat-storage-layout выведет на консоль при публикации смарт-контракта:

$ npx hardhat run scripts/deploy.js

Проект плагина hardhat-storage-layout вы найдете здесь: https://github.com/aurora-is-near/hardhat-storage-layout.

Подготовка скрипта публикации

Подготовьте скрипт публикации deploy.js в соответствии с листингом 2.

Листинг 2. Файл ~/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);
});

Обратите внимание на строку вызова асинхронной функции hre.storageLayout.export. Она нужна для получения на консоли карты распределения памяти, выделенной для приложения с помощью плагина hardhat-storage-layout.

Подготовка смарт-контракта для тестирования

Ниже в листинге 3 вы найдете смарт-контракт, который мы будем использовать для демонстрации одного из методов оптимизации.

Листинг 3. Файл ~/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;
}
}

Подготовка скрипта тестирования

Создайте скрпит тестирования, с помощью которого мы будем вызывать и проверять функции смарт-контаркта (листинг 4).

Листинг 4. Файл ~/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);
});
});

Тестирование потребления газа

В процессе тестирования мы будем измерять количество газа, израсходованное функциями смарт-контракта, а также смотреть распределение памяти, выделенное смарт-контракту.

Для запуска тестирования используйте такую команду в каталоге проекта:

$ REPORT_GAS=true npx hardhat test

Здесь при запуске теста мы задаем значение переменной среды REPORT_GAS, равное true. В этом случае в соответствии с настройками в файле hardhat.config.js плагин hardhat-gas-reporter выведет на консоль результаты измерения времени выполнения функций, а также газа, израсходованного на функции.

Время выполнения отображается в миллисекундах:

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

Далее на консоль будет выведена таблица, где для каждого метода будет указано среднее потребление газа и цена в рублях:

Как видите, вызовы функций смарт-контракта могут стоить недешево.

Например, один вызов неоптимизированной функции expLoop, которая обращается в цикле к переменной состояния, будет стоить 67663 gas или 0,002571194 ETH при цене газа в 38 gwei, что в итоге будет стоит 634.75 руб. (на момент запуска тестирования). Оптимизированный вариант этой функции optLoop, который делает запись в переменную состояния только один раз, стоит намного дешевле — 44525 gas или 417.70 руб.

Обратите внимание, что в таблице показана стоимость единицы газа gas, равная 38 gwei. Для того чтобы получить стоимость транзакции для функции expLoop в ETH, нужно выполнить следующие вычисления:

67663 gas * 38 = 2 571 194 gwei
2 571 194 gwei * (10 ** 9) = 2 571 194 000 000 000 wei
2 571 194 000 000 000 wei / (10 ** 18) = 0,002571194 ETH

Сокращенная формула без промежуточного перевода в wei:

67663 gas * 38 / (10 ** 9) = 0,002571194 ETH

Функция expLoop в цикле увеличивает значение переменной itValue на единицу, причем количество итераций передается функции в качестве параметра.

Давайте проведем небольшую оптимизацию. Функция optLoop использует для записи промежуточных результатов итерации локальную переменную itValueLoc. И только когда все итерации будут выполнены, результат будет записан в переменную состояния storage_value.

Вызов функции optLoop тоже обходится не даром, но стоит намного дешевле. При пяти итерациях это 44525 gas.

Просмотр карты распределения памяти

Подключив плагин hardhat-storage-layout, как это было описано выше, вы сможете просмотреть в удобном виде карту распределения с информацией о блоках памяти переменных состояния смарт-контракта.

Для этого достаточно запустить плагин публикации смарт-контракта в тестовую сеть Hardhat:

$ npx hardhat run scripts/deploy.js

На консоли появится таблица распределения памяти, показанная ниже на рисунке:

В данном случае в смарт-контракте были определены три переменные состояния:

uint128 storage_value_1;
uint128 storage_value_2;
uint storage_value;

Как видите для них было выделено три блока памяти размером 256 байт каждый. Попробуем переставить местами две переменные:

uint128 storage_value_1;
uint storage_value;
uint128 storage_value_2;

Теперь для переменных было выделено уже четыре блока памяти:

Как видите, изменение взаимного расположения объявления переменных в программе может привести к увеличению или уменьшению потребления памяти.

Заметим, что для исследования распределения памяти можно использовать и пакетный компилятор solc. Помимо создания бинарного кода смарт-контракта и файла ABI, этот компилятор может создавать и карту распределения памяти. Для этого его нужно запустить с параметром --storage-layout:

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

Файл распределения памяти будет создан в каталоге, указанном в параметре -o, в нашем случае это каталог build.

Этот JSON-файл удобно просматривать при помощи программы jq:

$ 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"
    },
    …
  ]
  …
}

Здесь мы показали содержимое файла в упрощенном виде.

Утилиту jq можно установить следующей командой:

$ sudo apt install jq

Конечно, просматривать карту распределения памяти, созданную плагином hardhat-storage-layout намного легче, чем анализировать JSON-файл вручную. Однако для автоматизированного анализа удобнее как раз JSON-файл.

Другие примеры оптимизации вы найдете в каталоге les16 репозитория https://github.com/AlexandreFrolov/sol01

Комментарии (2)


  1. vassabi
    20.04.2022 18:49

    что-то пора добавлять эти метрики в компилятор и оптимизировать код автоматически, а не вручную.


    1. AlexandreFrolov Автор
      21.04.2022 08:35

      Компилятор пытается оптимизировать созданный им бинарный код автоматически, если включить соответствующие параметры. Но ему доступно не все, можно так плохо написать исходный код, что никакая автоматическая оптимизация уже не поможет!