Packing переменных, calldata vs memory, кэш storage-переменных, эффективные циклы, optimizer
Газ — это деньги. Неоптимальный контракт переплачивает при каждой транзакции. На популярных протоколах это миллионы долларов в год
Каждый опкод 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.
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
}Для параметров 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-переменной стоит 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 на использование константы в выражении.
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.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.