В этой статье я покажу вам ВСЕ известные методы оптимизации gas при вызовах и развертывании смарт-контрактов.
Мы увидим, какой метод полезен, а какой бесполезен. (или даже вредный)
Мы посмотрим, сколько газа вы можете сэкономить, используя каждый метод.
Я буду ранжировать все методы, используя список уровней. (A = отлично, D= бесполезно/вредно)
Я постараюсь быть как можно более исчерпывающим.
Итак, поехали!
Оптимизация развертывания против оптимизации вызовов. Во-первых, вам нужно знать, что есть 2 способа оптимизировать расход газа на dAPP:
Оптимизация затрат на развертывание Вы можете оптимизировать затраты на развертывание смарт-контракта. Стоимость = 21000 gas (для создания транзакции) + 32000 gas (для создания контракта) + ВЫПОЛНЕНИЕ КОНСТРУКТОРА + РАЗВЕРНУТЫЕ БАЙТЫ * 78.125 Таким образом, для развертывания смарт-контракта вы потратите не менее 53000 газа. Чтобы минимизировать затраты на газ, вам необходимо минимизировать размер смарт-контракта и стоимость конструктора.
Оптимизируйте каждый вызов функции. Вы также можете оптимизировать стоимость вызова каждой функции в вашем смарт-контракте. Стоимость вызова равна 21000 gas (для создания транзакции) + стоимость выполнения функции. Таким образом, цель будет заключаться в снижении стоимости выполнения функции.
Стоит обратить внимание, что эти 2 способа оптимизации не совпадают!!
Во многих случаях вы можете развернуть код большей надежности (то есть с более высокой стоимостью развертывания), но более оптимизированный для каждого вызова функции. (Таким образом, с меньшей стоимостью вызывающего газа)
Если вам необходимо выбрать один из этих методов, то Я настоятельно советую вам оптимизировать каждый вызов функции, поскольку развертывание выполняется только один раз.
Ранжирование методов оптимизации газа
Я буду ранжировать все методы оптимизации газа, используя уровни.
(Потому что есть разница между экономией 2 газа при транзакции в 21000 газа и экономией 10000 газа.)
Уровень А: Если вы не знаете этих правил, вы не являетесь НАСТОЯЩИМ разработчиком solidity.
Уровень B: Существенный, должен быть известен всем разработчикам.
Уровень C: может быть когда-нибудь полезен, должен быть известен и разработчикам.
Уровень D: Бесполезный / вредный, не используйте их (если только вы не знаете, что делаете). Вы теряете либо свое время, либо безопасность смарт-контракта.
На уровне А, на удивление, нет никаких “простых” подсказок и галочек, чтобы сэкономить бензин, но вместо этого…
A1: Знание того, сколько стоит каждый код операции EVM. Вот 6 самых дорогостоящих кодов операций с газом (среди наиболее часто используемых - смарт-контракты):
CREATE/CREATE2 -> Развертывает смарт-контракт. (32000 gas)
SSTORE -> Сохраняет значение в хранилище. (20000 газа, если к слоту не был получен доступ, 2900 в противном случае.)
EXTCODESIZE -> Получить размер в байтах кода смарт-контракта. (2600, если к нему не обращались ранее, 100% в противном случае)
BALANCE(address(this).balance), такой же, как EXTCODESIZE.
SLOAD -> Доступ к значению в хранилище. (2100 газа, если доступ не был получен до 100 газа в противном случае)
LOG4 -> Создать событие с 4 темами. (1875 gas) (На самом деле это оценка. Некоторые из них могут варьироваться в зависимости от ситуации.)
Лучший веб-сайт, который описывает, сколько стоит каждый код операции https://www.evm.codes /, это очень хорошо объяснено.
Если вы будете знать это, вы лучше поймете solidity и получите “лучшую” интуицию о том, чего вам стоит каждая строка кода.
A2: Используйте просмотр модификатора
Это совершенно очевидно, но я все еще вижу некоторых “разработчиков”, не помечающих необходимые функции как view…
function getBalance() external view {
return balance;}
Вы можете легко сократить свои расходы на газ до 0 при вызове смарт-контракта, если не будете записывать данные в хранилище. (более того, вам не нужно ждать ответа 5-20 секунд)
A3: Понимание solidity
В более общем плане, вам нужно понять, как работает solidity, это позволит вам не полагаться на онлайн-руководства для экономии топлива!
A4: Понимание работы алгоритмов
Вам нужно знать, как работают различные алгоритмы, как их можно оптимизировать и в чем заключается “сложность”.
Например, допустим, вы хотите отсортировать массив. (который возникает много раз)
[4,5,108,3,7,1,94,15,99,34,0,24,5,4]
Как вы будете это решать?
Существуют десятки различных алгоритмов сортировки: https://www.geeksforgeeks.org/sorting-algorithms/
Некоторые из них более эффективны, чем другие, в некоторых случаях использования…
Вам нужно выбрать лучший вариант в зависимости от ситуации, а это не так просто. Например, “Быстрая сортировка” обычно является самым быстрым способом сортировки массива со сложностью O(nlog(n))
Эти советы и рекомендации могут сэкономить вам значительное количество бензина, и их следует применять как можно чаще.
ВАЖНОЕ ПРИМЕЧАНИЕ ПЕРЕД НАЧАЛОМ:
Поскольку функция not_optimized() расположена перед функцией optimized() в байт-коде. Вызов optimized() стоит дороже, чем not_optimized() с тем же кодом (на 22 больше газа), поэтому я буду вычитать 22 газа при каждом вызове функции optimized().
B1: Операции дозирования(Сэкономленный газ: 21000 операций дозирования газа *) Отправка транзакции в одиночку на блокчейне стоит много газа (21000gas, если быть точным), поэтому, если вы сможете найти способ пакетной обработки транзакций для своих пользователей, это может сэкономить вам значительное количество газа.
B2: порядок изменения места хранения (20 000 сэкономленных газов при развертывании)
Хранилище Ethereum состоит из слотов по 32 байта, проблема в том, что запись обходится дорого. (До 20000gas при использовании “холодной” записи)
Допустим, у вас есть 3 переменные:
uint128 a; //Slot 0 (16 bytes)
uint256 b; //Slot 1 (32 bytes)
uint128 c; //Slot 2 (16 bytes)
Они используют 3 разных слота для хранения
uint256 b; //Slot 0 (32 bytes)
uint128 a; //Slot 1 (16 bytes)
uint128 c; //Slot 1 (16 bytes)
Но если я хорошо выровняю 3 слота, я смогу сэкономить 1 слот. (переменные a и c будут находиться в одном и том же слоте)
Следовательно, при развертывании используется на 1 меньше слотов. (Таким образом, сэкономлено 20 000 газа)
Более того, если вы хотите получить доступ к переменной c, но переменная b уже доступна, то это будет считаться теплым доступом (доступ к уже доступному слоту хранилища), так что это обойдется вам в 1000 газа вместо 2900 , что довольно значительно.
Если вы не разбираетесь в хранилищах, документация может вам помочь: https://docs.soliditylang.org/en/v0.8.13/internals/layout_in_storage.html
B3: Используйте оптимизатор (Экономия газа: 10-50% при развертывании и вызове)
Вы можете использовать встроенный оптимизатор solidity
Это очень простой способ сэкономить много газа, не изучая никаких новых концепций. Вам нужно установить флажок “включить оптимизацию”.
Значение, близкое к 1, оптимизирует стоимость газа, но значение, близкое к максимальному (232-1), оптимизирует вызовы функции.
В большинстве случаев вы можете использовать значение по умолчанию (200)
B4: используйте отображение вместо массива (экономится 5000 газа на значение)
Mappings обычно обходятся дешевле, чем массивы, но вы не можете перебирать их.
функция not_optimised() расход газа: 48409
optimised() расход газа: 43400
B5: Используйте require вместо assert
Утверждение НЕ должно использоваться другими средствами, кроме как для целей тестирования. (потому что, когда утверждение не выполняется, газ НЕ возвращается вопреки требованию)
B6: используйте самоуничтожение (экономьте до 24000 газа)
функция self destruct() уничтожает смарт-контракт и возвращает 24000 долларов газа.
В более сложной транзакции, такой как развертывание другого смарт-контракта, вы можете вызвать selfdestruct() для смарт-контракта, чтобы сэкономить немного газа.
B7: Делайте меньше внешних вызовов (сэкономленное количество: переменное)
Звоните по другому контракту только тогда, когда вы обязаны это сделать.
B8: Ищите мертвый код (экономит переменное количество газа при развертывании)
Иногда разработчики забывают удалить бесполезный код, такой как:
require(a == 0)if (a == 0) {
return true;
} else {
return false
}
B9: использование неизменяемых переменных (экономит 15000 газа при развертывании)
not_optimised контракт: 116800 gas
optimised контракт: 101013 gas
B10: хранение данных в событиях (до 20 000 единиц газа, сэкономленных при вызове функции)
Если вам не нужен доступ к данным onchain в solidity, вы можете сохранить их с помощью событий.
pragma solidity ^0.8.0;contract Test {address store;
event Store(uint256 indexed key,address indexed data);
function optimised() external {
emit Store(1,0xaaC5322e456d45E7b6c452038836C5631C2AeBc0);
}function not_optimised() external {
store = 0xaaC5322e456d45E7b6c452038836C5631C2AeBc0;
}
not_optimised контракт: 43353 gas
optimised контракт: 22747 gas
Эти советы могут сэкономить вам изрядное количество газа и ничего вам не стоить.
C1: Используйте статический тип размера в solidity (экономится около 50% газа)
Статические типы размера (например, bool, uint256, bytes 5) дешевле, чем динамические типы размера (например, string или bytes)
not_optimised() function gas usage: 21255 gas
optimised() function gas usage: 21227 gas
C2: Холодный доступ против теплого доступа (экономия газа 70)
Не обращайтесь 2 раза к одной и той же переменной хранилища.
not_optimised() function gas usage: 23412 gas
optimised() function gas usage: 23347 gas
C3: использование данных вызова вместо памяти (450 сэкономленных газов на вызов)
not_optimised() function gas usage: 22442 gas
optimised() function gas usage: 21994 gas
C5: Используйте индексированные события (62% сэкономленных при вызове функции для каждой темы)
Вы можете пометить каждое событие как проиндексированное, как показано ниже:
Индексированное событие позволяет упростить поиск событий.
contract Test {
event Testa(address a,address b);
event Testa2(address indexed a,address indexed b);
function not_optimised() external { emit Testa(0x..., 0x...);
} function optimised() external {
emit Testa2(0... ,0...);
}}
Здесь функция optimised() использует на 135 меньше газа, чем not_optimised().
C6: Изменение порядка запросов(20-2000 сэкономленного газа за запрос) Как сказано в важной записке:
Мы можем вызвать каждый контракт смарт-контракта, если вы укажете в msg.data подпись функции. (первые 4 байта хэша функции keccak256).
Во-первых, EVM выполнит “переключение” этой подписи, чтобы увидеть, какую функцию выполнить.
switch(msg.data[0:4]) { // compare to signature case 0x01234567: go to functionA; case 0x11111111: go to functionB; // cost 22 more gas.. case 0x4913aaaa: go to functionC; // cost 22+22 more gas... ...}
Поскольку по сравнению с вычислением подписи используется немного газа (22 газа), вам нужно поместить наиболее вызываемую функцию на первое место switch(), убедившись, что подпись находится на первом месте (изменив ее имя)
C7: используйте i++ вместо i = i + 1 (62 газа, сэкономленных при каждом вызове) Это звучит как шутка, но, похоже, это работает.
not_optimised() function gas usage: 21401 gas
optimised() function gas usage: 21339 gas
C8: Используйте uint256 вместо uintXX в хранилище (экономится 55 газа за вызов)
Для записи / доступа к данным в хранилище EVM использует 32 байта (= 256 битных слотов), при использовании меньших типов, чем uint256, EVM необходимо выполнять дополнительные операции для обеспечения преобразования.
Поэтому лучше использовать uint256 вместо uint8 для хранения данных.
not_optimised() function gas usage: 43353gas
optimised() function gas usage: 43298 gas
C9: Создайте пользовательскую ошибку (24 часа, сэкономленные на вызове)
Вы можете создать custom errors на solidity и вернуться к нему, как показано ниже.
not_optimised() function gas usage: 21476 gas
optimised() function gas usage: 21474 gas
C10: Обмен 2 переменными с помощью кортежа (a, b) = (b, a) (сохраняет 5 целей при каждом вызове)
not_optimised() function gas usage: 21241 gas
optimised() function gas usage: 21236 gas
Здесь советы по оптимизации расхода газа бесполезны, не тратьте свое время на экономию небольшого количества газа, это все равно будет затенено транзакцией с газом 21000.
D1: использование unchecked(экономия газа: 150 / операция) Начиная с solidity 0.8.0, операции типа + * / — проверяются каждый раз, если происходит переполнение / недостаточный поток.
Но это стоило немного газа, чтобы проверить, есть ли переполнение / недостаточный расход
Если вы не выполняете много операций в одном вызове функции (например, в циклах for), вам НЕ следует использовать unchecked.
not_optimized() function gas usage: 21419 gas
optimised() function get usage: 21253 gas
D2: изменение и оптимизация байт-кода самостоятельно
Не делайте этого, если у вас нет на то веских причин, некоторые функции могут иметь неопределенное поведение и проблемы с безопасностью.
Вместо этого используйте оптимизатор или применяйте очень строгую политику тестирования.
D3: Удаляет строковые сообщения об ошибках (около 78.125% экономии на стоимости развертывания за символы)
Не делайте этого, код может быть сложнее отлаживать для вас и для пользователей.
Например, что можно заменить:
require(account != address(0), "ERC20: mint to the zero address")
На:
require(account != address(0));
D4: с использование низкоуровневой сборки (экономия газа: переменная)
Если вы не знаете, что делаете, НЕ делайте сборку solidity. В конечном итоге у вас будет гораздо больше шансов получить дыру в безопасности вашего контракта из-за незначительного количества сэкономленного газа.
D5: используйте external вместо public
Удивительно, но они не представляют никакой разницы между внешними и общедоступными функциями
not_optimised() function gas usage: 21379 gas
optimised() function gas usage: 21401 gas (-22 = 21379)
D6: Используйте сдвиг влево вместо умножения >> (150 сэкономленных газов) Сдвиг двоичных данных n раз влево приводит к умножению данных на 2 ^ n. Вот пример:
00000001 = 1 dec
00000010 = 2 dec
00000100 = 4 dec
00001000 = 8 dec
not_optimised() function gas usage: 21615 gas
optimised() function gas usage: 21436 gas
Прирост неплохой, но оператор >> не проверяет наличие переполнения.
D7: Удалить хэш метаданных (3000-5000 gas при развертывании) К каждому байт-коду смарт-контракта в конце добавляется “хэш”. Это хэш всех метаданных смарт-контракта. (Включая abi, комментарии, код...)
Поскольку хэш метаданных имеет длину 32 байта, это может сэкономить вам кучу времени.
Но это довольно сложно сделать, потому что вам придется делать это вручную.
Так что это может быть опасно, если выполнено плохо.
D8: Добавляет плату за каждую функцию (24 экономии газа за вызов) Добавляя payable к функции, удалите проверку на msg.value. Таким образом, экономится некоторое количество газа.
pragma solidity ^0.8.0;contract Test {address store; function optimised() external payable { }function not_optimised() external { }
}
not_optimised() function gas usage: 21186 gas
optimised() function gas usage: 21184 gas
Это не вредно, но и не очень полезно.
D9: выполнение важной работы вне цепочки. Будьте осторожны с работой, которую вы выполняете вне сети (как в JS)
Например, вы не можете выполнить проверку безопасности в автономном режиме во внешнем интерфейсе веб-сайта, это очень опасно, поскольку любой может изменить JS-код в браузере и отправить неверный ввод в смарт-контракт.
ВСЕГДА ВЫПОЛНЯЙТЕ ПРОВЕРКУ БЕЗОПАСНОСТИ ПО ЦЕПОЧКЕ.
Заключение
Я рассмотрел 90% методов оптимизации расхода газа на solidity, если вы уже знаете, как они работают, и вы можете сэкономить немалое количество газа, поздравляю :)
Только не попадайтесь в ловушку, тратя слишком много времени на экономию кучи газа, вам тоже нужно применить оптимизацию времени к своей жизни :)