February 16, 2022

Оптимизация gas

Привет друзья!

Сегодня немного отойдем от голой теории. Мы уже рассмотрели, на что тратится gas и по каким причинам. Давайте попробуем это резюмировать и поискать способы его оптимизации при написании контрактов. Поехали!

1 совет:

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

2 совет:

Не избегайте более точных настроек компилятора. О возможных и желательных настройках компилятора мы еще поговорим в будущем. На данный момент нас интересует исключительно параметр оптимизации. Это позволит нам включить оптимизацию на этапе компиляции и сократит расход газа. Почему он выключен по умолчанию? Ответ простой – компиляция с оптимизацией происходит дольше.

3 совет:

Вчера мы говорили об организации памяти EVM. Несколько раз упоминали про то, что данные в storage хранятся в виде 256 битных «слотов». Данные, которые не помещаются в один «слот» памяти – могут занять несколько «слотов», что не всегда хорошо. Ведь часть выделенной памяти просто останется пустой. Аналогичная ситуация может произойти, если размер данных меньше «слота» памяти. Solidity постарается оптимизировать эту ситуацию, разместив 2 «порции» данных в 1 «слот» памяти, но для этого ему придется помочь. Что мы можем сделать? Давайте рассмотрим пример:

//Good :)

uint128 a;

uint128 b;

uint256 b;

// Bad :(

uint128 a;

uint256 b;

uint128 b;

В первом случае, компилятор сможет упаковать переменные a, b в один "слот" за счет того, что они идут друг за другом. Таким образом понадобиться 2 "слота" по 256 бит для хранения данные. Во втором же случае, понадобиться уже 3 "слота".

uint8 a;

uint256 b;

Еще один показательный пример. В данном случае было бы даже «дешевле» использовать для первой переменной uint256 вместо uint8. Более подробно об этой ситуации.

«Упаковка» работает лишь для storage. Не пытайтесь упаковать локальные переменные или аргументы функции.

Будьте внимательнее.

4 совет:

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

//Good

bytes32 constant hash = 'uiHk78Uidaf....';

//Bad

bytes32 constant hash = keccack256(abi.encodePacked('MyDataToHash'));

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

5 совет:

Порой может случится так, что глубокие знания и понимание принципов компиляции кода контракта в bytecode может сыграть Вам на руку! Solidity позволяет использовать assembly вставки (низкоуровневые конструкции внутри Solidity). Они максимально близки к скомпилированному коду и позволяют Вам вручную оптимизировать opcodes и, временами, даже обыграть байт-код Solidity с точки зрения числа операций и стомости.

6 совет:

Зачастую в коде используются константы (constants) для расчетов или сравнений. Никто не любит эти «магические числа» вне переменных. Но хранить константу в storage памяти тоже не выход. На помощь приходит ключевое слово «constant». Это позволит Вам сохранить такое значение непосредственно в bytecode контракта, не тратя для этого ресурсы в storage памяти.

uint256 public v1 = 12; // bad uint256 public constant v2 = 256; // good

function calculate() returns (uint256 result) {     return v1 * v2

}

7 совет:

Хранение данных в IPFS вместо storage. IPFS сеть – это децентрализованная сеть хранения данных, где каждый файл идентифицируется не через URL, а через hash от его содержимого. Поскольку хеш от файла всегда один, то он будет уникально идентифицировать файл. Таким образом, можно поместить данные (особенно большие) в сеть IPFS, а "хеш – указатель" хранить как constant внутри переменной в контракте. Конечно, этот способ не универсален и подходит далеко не всегда. Однако, если есть необходимость хранить большой объем информации, стоит присмотреться. Подробнее – здесь.

8 совет:

Short-circuiting. Хм, пожалуй, оставим без перевода. Или, если хотите, что-то вроде «короткого замыкания». Сейчас поясним почему используется такой термин.

Представим следующую ситуацию. Есть 2 функции: f(x)дешевая, g(y)дорогая. Если в коде участвует операция, использующая || или && между ними, то совет гласит о том, что сначала должна идти «дешевая» функция, т.к. при её истинности, необходимость выполнять вторую просто пропадет (в терминологии первая функция произведет «короткое замыкание», что прервет дальнейшее выполнение с ясным исходом). Думаю, вы скажете, что это логично, но всякое бывает!

9 совет:

Неэффективное использование библиотек. Может сложится ситуация, когда из библиотеки требуется всего 1-2 функции. Но при этом, сама библиотека может быть достаточно объемной. В таком случае, рекомендуется просто забрать реализацию необходимых функций к себе в код, и использовать исключительно эту часть кода. Однако, в таком случае необходимо помнить, что библиотеки могут меняться, и, время от времени, стоит проверять, не появилось ли каких-то изменений в реализации (например, в старой реализации нашлась угроза безопасности).

10 совет:

Избегайте циклов с переменными в storage. Это может очень дорого вам обойтись. При необходимости использования циклов – создайте копию, как временную переменную в memory, выполните условие цикла, а результат присвойте глобальной переменной. Это позволит сэкономить приличную сумму.

11 совет:

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

12 совет:

Чаще всего, использование маппингов (mapping) выгоднее, чем использование массивов. Однако, есть ситуации, в которых это не так. Это касается, как правило, больших массивов, нацеленных на хранение «коротких» значений. Это объясняется тем, что данные в массивах тоже поддаются упаковке, и следовательно, могут достаточно компактно расположиться в памяти. Порой, это может даже перекрыть затраты на операции с массивами.

13 совет:

Откажитесь от присвоения значения по умолчанию при создании переменной. Это позволит сократить число операций (которые стоят gas). Конечно, кроме ситуаций, когда это значение необходимо использовать в дальнейшем.

14 совет:

Операция удаления (delete) позволяет вернуть газ. Тем самым стимулируется экономия места в блокчейне. Удаление переменной возвращает 15000 газа, если это не превышает половину всего объема газа, затраченного на транзакцию.

Советы, не вошедшие в список по причине объемности объяснений и трудности восприятия:

  1. Доказательство Меркла для снижения нагрузки
  2. Stateless контракты
  3. Блок Gas-costly patterns – по большей части посвящен условным операторам и циклам, не станем наращивать размер статьи, по желанию можете ознакомится.
  4. Function visibility – одна из тем дальнейших обсуждений в статьях

Предлагаю на этом остановится. Безусловно, это не всё, что используется, но мы постарались вынести сюда наиболее понятные и яркие советы. Если Вы знаете другие практики для оптимизации затрат, то будем рады, если вы поделитесь этим с остальными участниками!

Завтра мы вновь погрузимся в теорию, ну а пока, предлагайте свои любимые способы оптимизации, будем экономить gas вместе 😉

Источники, не присутствующие в статье, как ссылки, но использующиеся для написания:

  1. Раз
  2. Два