Если раньше вы писали программы для обычных приложений, таких как скрипты 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.jsrequire("@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
vassabi
что-то пора добавлять эти метрики в компилятор и оптимизировать код автоматически, а не вручную.
AlexandreFrolov Автор
Компилятор пытается оптимизировать созданный им бинарный код автоматически, если включить соответствующие параметры. Но ему доступно не все, можно так плохо написать исходный код, что никакая автоматическая оптимизация уже не поможет!