Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Оптимизация газа
gas_optimization

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

Packing переменных, calldata vs memory, кэш storage-переменных, эффективные циклы, optimizer

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

Газ — это деньги. Неоптимальный контракт переплачивает при каждой транзакции. На популярных протоколах это миллионы долларов в год

#Как работает газ в EVM

Каждый опкод EVM имеет фиксированную стоимость в единицах газа. Итоговая стоимость транзакции = gasUsed × (baseFee + priorityFee) в Wei.

Самые дорогие операции:

  • SSTORE (запись в storage): 20,000 gas при первой записи (cold), 5,000 при изменении (warm), 2,900 при изменении уже-прогретого
  • SLOAD (чтение из storage): 2,100 gas (cold), 100 gas (warm, EIP-2929)
  • CREATE (деплой контракта): 32,000 gas + газ на инициализацию
  • CALL (внешний вызов): 2,600 gas

Самые дешёвые: арифметика (3-5 gas), стек (2-3 gas), memory (3 gas + квадратичный рост при большом использовании).

Главный принцип оптимизации: минимизировать операции с storage.

#Storage Packing (упаковка переменных)

EVM хранит данные в 32-байтных слотах. Компилятор Solidity автоматически упаковывает последовательные переменные в один слот, если они вместе не превышают 32 байта.

// НЕЭФФЕКТИВНО: 3 отдельных слота contract Unoptimized { uint256 a; // слот 0 (32 байта) uint8 b; // слот 1 (только 1 байт, 31 потрачен впустую) uint8 c; // слот 2 (только 1 байт, 31 потрачен впустую) } // ЭФФЕКТИВНО: 2 слота contract Optimized { uint256 a; // слот 0 (32 байта) uint8 b; // слот 1, позиция 0 (1 байт) uint8 c; // слот 1, позиция 1 (1 байт) — упаковано с b! uint8 d; // слот 1, позиция 2 (1 байт) // uint128 x; — не поместится в слот 1 (осталось 29 байт), новый слот 2 }

Упаковка struct особенно важна — порядок полей определяет число слотов:

// 3 слота — плохо struct BadOrder { uint128 a; // слот 0 uint256 b; // слот 1 (uint256 не умещается рядом с uint128) uint128 c; // слот 2 } // 2 слота — хорошо struct GoodOrder { uint128 a; // слот 0, позиция 0 uint128 c; // слот 0, позиция 16 — упакован с a! uint256 b; // слот 1 }

#Calldata вместо Memory

Для параметров external функций, которые не изменяются, используйте calldata. Данные читаются напрямую из входных данных транзакции без копирования в memory.

// ДОРОГО: копирует весь массив в memory function processMemory(uint256[] memory data) external { // каждый элемент — CALLDATALOAD + MSTORE } // ДЁШЕВО: читает напрямую из calldata function processCalldata(uint256[] calldata data) external { // каждый элемент — только CALLDATALOAD }

Разница растёт с размером массива: для массива из 100 uint256 экономия ~2000 gas.

#Кэширование Storage переменных

Каждое обращение к storage-переменной стоит 100-2100 gas. Если переменная используется несколько раз в функции, кэшируйте её в локальной переменной (в стеке, 3 gas).

// ДОРОГО: 3 SLOAD function badLoop(uint256[] storage data) internal { for (uint256 i = 0; i < data.length; i++) { // data.length — SLOAD на каждую итерацию! process(data[i]); } } // ДЁШЕВО: 1 SLOAD для length function goodLoop(uint256[] storage data) internal { uint256 len = data.length; // кэшируем length (1 SLOAD) for (uint256 i = 0; i < len; i++) { process(data[i]); } } // Кэширование struct из storage function updateUser(address user) external { User storage u = users[user]; // указатель на storage slot uint256 balance = u.balance; // 1 SLOAD // Работаем с локальной копией uint256 newBalance = balance + calculateReward(balance); u.balance = newBalance; // 1 SSTORE в конце }

#Эффективные операции с данными

Несколько практических приёмов:

Используйте uint256 как базовый тип в вычислениях — EVM работает с 32-байтными словами нативно. uint8 в storage экономит слоты (через packing), но в вычислениях не даёт преимущества (EVM всё равно дополняет до 256 бит).

Избегайте delete внутри циклов при больших массивах — это стоит gas на каждый SSTORE(0).

bytes32 дешевле string для фиксированных идентификаторов — не требует динамической аллокации памяти.

immutable переменные (задаются в конструкторе, не изменяются) хранятся в байткоде, не в storage. Чтение бесплатно (CODECOPY дешевле SLOAD). Используйте immutable для адресов зависимостей, ID сети, дат деплоя.

constant ещё дешевле — значение жёстко встроено в байткод как литерал, тратит только gas на использование константы в выражении.

#Компилятор optimizer

Solidity компилятор имеет встроенный оптимизатор. В Hardhat и Foundry конфигурируется так:

// hardhat.config.js { "solidity": { "version": "0.8.20", "settings": { "optimizer": { "enabled": true, "runs": 200 } } } }

Параметр runs — ожидаемое число вызовов функции. Высокое значение (например, 10000) оптимизирует runtime-газ за счёт большего байткода деплоя. Низкое значение (200) минимизирует размер байткода (дешевле деплой). Для часто вызываемых функций DEX — высокий runs. Для редко используемых adminonly контрактов — низкий.

via-ir — более глубокая оптимизация через промежуточное представление Yul. Включается отдельно, значительно увеличивает время компиляции, но может сэкономить до 20-30% gas runtime.

Далее: Апгрейдируемые контракты