Как стать автором
Обновить

Эзотерическая оптимизация газа в Solidity

Время на прочтение4 мин
Количество просмотров4.8K

Программирование в Солидити отличается от других языков, так как каждое инструкция и байт памяти тратят газ - деньги пользователей. В сети уже есть много ресурсов с основными техниками оптимизации кода (например, стараться использовать calldata вместо memory), но я хочу показать несколько совсем безумных и неочевидных.

Понять о чем я говорю без базового опыта в solidity будет очень сложно, но может быть эти оптимизации проявят в вас интерес в ethereum программировании.

  1. 1 wei в msg.value

https://github.com/AztecProtocol/weierstrudel
> The weierstrudel smart contract requires precisely 1 wei to be sent to it or it will refuse to perform elliptic curve scalar multiplication. No more, no less.

При вызове какого-то кода на Ethereum есть возможность послать вместе с вызовом какие-то количество нативной валюты - эфира. Это происходит через выставление параметра value.

AztecProtocol требует пользователей послать ровно 1 wei с каждым вызовом контракта, что позволяет сохранить около 500 единиц газа. Почему это работает?

EVM (виртуальная машина Эфириума) имеет стек. Все арифметические операции происходят через него, например, чтобы сложить два числа, нужно сначала вызвать ассемблерные инструкции PUSH чтобы положить числа на стек, и потом ADD чтобы их сложить.

PUSH инструкция стоит 3 газа.

CALLVALUE инструкция кладет на стек значение msg.value и стоит всего лишь 2 газа. Если код имеет много математических вычислений использующих единицу, дешевле брать ее из msg.value. Протокол в целом сохраняет около 500 газа за каждый вызов контракта.

  1. Использование отрицательных чисел.

Calldata - это раздел памяти, который живет только в пределах одной транзакции, например, параметры передающиеся в функцию извне. Ненулевые байты в calldata стоят в четыре раза больше газа, чем нули - такой стандарт EVM. Использовать int и слать отрицательные числа может быть очень дорого, так как отрицательные числа имеют пэддинг из 0xf байтов в начале.

» abi.encode(int(-8)) 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8

  1. Освобождение памяти

https://github.com/euler-xyz/euler-contracts/blob/617c1dbaa3f506e881cd9000fd89b1822d4be650/contracts/Base.sol#L62

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

Память выделяется в последовательных блоках. Ячейка памяти 0x40 хранит указатель на следующий свободный блок памяти. Любые аллокации представляют собой простой инкремент этого указателя.

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

Имея все это в виду, Euler создали модификатор, который позволяет “освободить” неиспользуемую память. Его механизм работы прост - запомнить значение по адресу 0х40, выполнить функцию и записать запомненное значение ячейки памяти обратно в 0х40. Таким образом, любая память, аллоцированная внутри функции, будет считаться свободной.

  1. Упаковка булевых значений в uint256

Тип bool в солидити занимает 1 байт в памяти, из которых используется только один байт. При необходимости нескольких булевых значений можно заменить bool на uint32 или uint256 и битовой арифметики. Таким образом, uint256 может хранить до 256 булевых значений.

  1. Использование indexed аргументов у событий

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

  1. Запись в ненулевые storage слоты.

Storage - самый дорогая область памяти по газу в Солидити. Она состоит из слотов в 256 байт. Запись в storage слоты, в которых перед этим хранился 0, стоит 20000 газа. Запись в ненулевые слоты стоит 5000 газа, в 4 раза меньше. Часто не нужны все 256 бит слота, и тогда можно упаковать 32- и 64 битные значения в один storage слот. Первая запись будет стоит полные 20000, но последующие - меньше.

  1. ≠ дороже чем ==

Тут все просто - оператор ≠ стоит больше газа потому что в evm bytecode это представлено как использование == + NOT, что тратит больше газа чем просто ==. Если возможно выразить булево выражение без ==, можно сохранить на этом газ.

Это можно использовать, например, при использовании reentrancy guard. Обычно это делается через одну переменную в который хранится либо ENTERED либо NOT_ENTERED значения. Перед вызовом функций эта переменная проверяется, и если значение равно ENTERED, то вызова не происходит. В такой ситуации дешевле использовать == чем ≠.

  1. Оптимизация имен функций

Вызов далеко не всех функций стоит одинаковое количество газа. Их имена напрямую влияют на потраченное количество газа! Для вызова функции используются первые 4 байта хэша от ее имени и типов аргументов. Эти 4 байта сохраняются в calldata, которые мы упоминали выше, и посылаются как часть транзакции. Чем больше среди них нулей - тем меньше потребуется газа. Таким образом, правильный выбор имени функции будет требовать меньше газа.
https://emn178.github.io/solidity-optimize-name/

  1. Inline assembly деление

Использование assembly опкода div всегда дешевле чем использование оператора деления solidity почти на 100 газа.

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

result = numerator / can_never_be_zero; // expensive
assembly {
  result := div(numerator, can_never_be_zero; // cheap
}
Теги:
Хабы:
Всего голосов 12: ↑9 и ↓3+6
Комментарии4

Публикации

Истории

Работа

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань