[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-2-model-pamyati-evm-stek-khranilishche-calldata":3},{"article":4,"author":54},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":7,"meta_description":16,"focus_keyword":17,"og_image":18,"canonical_url":18,"robots_meta":19,"created_at":15,"updated_at":15,"tags":20,"category_name":34,"related_articles":35},"d0000000-0000-0000-0000-000000000202","a0000000-0000-0000-0000-000000000012","Deep EVM #2: Модель памяти EVM — стек, память, хранилище и calldata","deep-evm-2-model-pamyati-evm-stek-khranilishche-calldata","Глубокое погружение в четыре области данных EVM — стек, память, хранилище и calldata — их стоимость, поведение и формула расширения памяти, которая застаёт разработчиков врасплох.","## Четыре места для ваших данных\n\nEVM предоставляет четыре различных области данных, каждая с радикально отличающейся стоимостью, временем жизни и паттернами доступа. Выбор неправильной — самый распространённый источник чрезмерного потребления газа в смарт-контрактах.\n\n| Область | Время жизни | Стоимость чтения | Стоимость записи | Размер |\n|---------|-------------|------------------|------------------|--------|\n| Стек | Текущий опкод | 0 (DUP: 3) | 0 (PUSH: 3) | 1024 слова |\n| Память | Текущий контекст вызова | MLOAD: 3* | MSTORE: 3* | Расширяемая |\n| Хранилище | Постоянное (блокчейн) | SLOAD: 2100\u002F100 | SSTORE: 20000\u002F2900\u002F100 | 2^256 слотов |\n| Calldata | Текущий контекст вызова | CALLDATALOAD: 3 | Только чтение | Входные данные транзакции |\n\n*Операции с памятью стоят 3 газа плюс квадратичная стоимость расширения.\n\n## Стек: быстрый, но маленький\n\nСтек — это рабочая память EVM. Каждая арифметическая, сравнительная и логическая операция читает из стека и пишет в стек. Он вмещает до 1024 элементов, каждый шириной 32 байта.\n\nНа практике вы редко используете более 16 слотов стека, потому что опкоды DUP и SWAP достигают только 16 элементов вглубь. Компилятор Solidity управляет расположением стека автоматически, но в Yul и Huff вы управляете им вручную — и ошибки «stack too deep» становятся вашим постоянным спутником.\n\nДоступ к стеку по сути бесплатен (3 газа за PUSH\u002FDUP\u002FSWAP операции). Это делает стек идеальным местом для промежуточных вычислений, счётчиков циклов и временных значений.\n\n```yul\n\u002F\u002F Вычисление только на стеке: сумма 1 + 2 + 3\n\u002F\u002F Общий газ: 3 + 3 + 3 + 3 + 3 = 15 газа\nlet a := add(1, 2)    \u002F\u002F PUSH1 1, PUSH1 2, ADD\nlet b := add(a, 3)    \u002F\u002F DUP1, PUSH1 3, ADD\n```\n\n## Память: дешёвая, но эфемерная\n\nПамять — это побайтно-адресуемый массив, который начинается пустым и расширяется по мере необходимости. Она существует только на протяжении текущего контекста вызова — когда CALL, STATICCALL или DELEGATECALL возвращается, память вызываемого уничтожается.\n\nДоступ к памяти происходит 32-байтными блоками:\n- **MSTORE(offset, value)** — записать 32 байта по указанному смещению. Стоит 3 газа.\n- **MLOAD(offset)** — прочитать 32 байта по указанному смещению. Стоит 3 газа.\n- **MSTORE8(offset, value)** — записать один байт. Стоит 3 газа.\n\n### Свободный указатель памяти\n\nSolidity резервирует первые 128 байт памяти для специальных целей:\n\n| Смещение | Назначение |\n|----------|------------|\n| 0x00-0x3f | Рабочее пространство (используется для хеширования) |\n| 0x40-0x5f | Свободный указатель памяти |\n| 0x60-0x7f | Нулевой слот (начальное значение для динамических массивов) |\n| 0x80+ | Свободная память (аллокация начинается здесь) |\n\n**Свободный указатель памяти** по адресу `0x40` отслеживает следующий доступный адрес памяти. Когда Solidity выделяет память (например, `new bytes(100)`, `abi.encode(...)`, создание структуры в памяти), он читает указатель, использует этот адрес и продвигает указатель.\n\n```solidity\n\u002F\u002F Что Solidity делает внутренне, когда вы пишете:\n\u002F\u002F bytes memory data = new bytes(64);\n\n\u002F\u002F 1. Чтение свободного указателя: MLOAD(0x40) -> 0x80\n\u002F\u002F 2. Сохранение длины массива по 0x80: MSTORE(0x80, 64)\n\u002F\u002F 3. Обновление свободного указателя: MSTORE(0x40, 0x80 + 32 + 64 = 0xE0)\n\u002F\u002F 4. Данные находятся по адресам 0xA0 — 0xDF\n```\n\n### Стоимость расширения памяти\n\nИменно здесь память становится дорогой. Стоимость газа для памяти — это не только 3 газа за MLOAD\u002FMSTORE — существует квадратичная стоимость расширения, взимаемая при обращении к памяти за пределами текущей верхней границы.\n\nФормула (из Yellow Paper):\n\n```\nmemory_cost = (memory_size_words^2 \u002F 512) + (3 * memory_size_words)\n```\n\nгде `memory_size_words = ceil(наибольший_доступный_байт \u002F 32)`.\n\nДля малого использования памяти (менее ~700 байт) стоимость приблизительно линейна — 3 газа за слово. Но она растёт квадратично:\n\n| Использовано памяти | Стоимость |\n|---------------------|-----------|\n| 32 байта (1 слово) | 3 газа |\n| 1 КБ (32 слова) | 98 газа |\n| 10 КБ (320 слов) | 1160 газа |\n| 100 КБ (3200 слов) | 29600 газа |\n| 1 МБ (32000 слов) | 2 001 000 газа |\n\nЭтот квадратичный рост означает, что контракты, обрабатывающие большие массивы в памяти, могут быстро исчерпать лимит газа. Именно поэтому Solidity использует память экономно и предпочитает вычисления на стеке.\n\n## Хранилище: постоянное, но дорогое\n\nХранилище — это постоянное хранилище ключ-значение EVM. Каждый контракт имеет собственное изолированное пространство хранения с 2^256 32-байтными слотами. Хранилище сохраняется между транзакциями — это состояние блокчейна.\n\n### Раскладка хранилища в Solidity\n\nSolidity последовательно назначает слоты хранилища переменным состояния:\n\n```solidity\ncontract StorageLayout {\n    uint256 public x;       \u002F\u002F слот 0\n    uint256 public y;       \u002F\u002F слот 1\n    address public owner;   \u002F\u002F слот 2 (упаковка: 20 байт)\n    bool public paused;     \u002F\u002F слот 2 (упаковка: 1 байт, тот же слот!)\n    mapping(address => uint256) public balances;  \u002F\u002F слот 3 (базовый)\n    \u002F\u002F balances[addr] находится по: keccak256(addr . slot_3)\n}\n```\n\nМаппинги и динамические массивы используют `keccak256` для вычисления слота хранилища:\n- `mapping[key]` -> `keccak256(key || slot_number)`\n- `array[index]` -> `keccak256(slot_number) + index`\n\n### Стоимость хранилища (после EIP-2929 + EIP-3529)\n\nХранилище — безусловно самая дорогая область данных:\n\n| Операция | Холодный | Тёплый |\n|----------|----------|--------|\n| SLOAD | 2100 | 100 |\n| SSTORE (ноль -> ненулевое) | 22100 | 20000 |\n| SSTORE (ненулевое -> ненулевое) | 5000 | 2900 |\n| SSTORE (ненулевое -> ноль) | 5000 + 4800 возврат | 2900 + 4800 возврат |\n\nУстановка слота хранилища из нуля в ненулевое значение стоит **20000 газа**, потому что дерево состояний Ethereum должно вставить новый узел-лист. Вот почему инициализация новой записи маппинга (например, баланса токена для нового держателя) так дорога.\n\n### Упаковка хранилища\n\nSolidity упаковывает несколько маленьких переменных в один 32-байтный слот, когда это возможно. Это критическая оптимизация:\n\n```solidity\n\u002F\u002F ПЛОХО: 3 слота хранилища = 3 x SLOAD = 6300 газа (холодный)\ncontract Unpacked {\n    uint256 a;  \u002F\u002F слот 0 (32 байта)\n    uint256 b;  \u002F\u002F слот 1 (32 байта)\n    uint256 c;  \u002F\u002F слот 2 (32 байта)\n}\n\n\u002F\u002F ХОРОШО: 1 слот хранилища = 1 x SLOAD = 2100 газа (холодный)\ncontract Packed {\n    uint64 a;   \u002F\u002F слот 0, байты 0-7\n    uint64 b;   \u002F\u002F слот 0, байты 8-15\n    uint64 c;   \u002F\u002F слот 0, байты 16-23\n}\n```\n\n## Calldata: только чтение и дёшево\n\nCalldata — это входные данные, отправленные с транзакцией или вызовом. Они доступны только для чтения, и обращение к ним дёшево (3 газа за CALLDATALOAD). Для внешних вызовов функций Solidity ABI-кодирует аргументы в calldata.\n\nИспользование `calldata` вместо `memory` для параметров функций позволяет избежать копирования данных и значительно экономит газ:\n\n```solidity\n\u002F\u002F ПЛОХО: копирует весь массив в память\nfunction sum(uint256[] memory arr) external returns (uint256) { ... }\n\n\u002F\u002F ХОРОШО: читает напрямую из calldata\nfunction sum(uint256[] calldata arr) external returns (uint256) { ... }\n```\n\n## Транзиентное хранилище (EIP-1153)\n\nEIP-1153 (обновление Cancun, март 2024) ввёл два новых опкода: **TSTORE** и **TLOAD**. Транзиентное хранилище подобно обычному хранилищу, но автоматически очищается в конце транзакции.\n\nКлючевые свойства:\n- Та же модель ключ-значение, что и у хранилища (2^256 слотов по 32 байта)\n- Стоимость 100 газа для TLOAD и TSTORE (аналогично тёплому SLOAD\u002FSSTORE)\n- Очищается после завершения транзакции — нет постоянного изменения состояния\n- Доступно между фреймами вызовов в рамках одной транзакции\n\nИдеально подходит для:\n- **Блокировки реентрабельности** — установить флаг через TSTORE, проверить через TLOAD. Без стоимости 20000 газа SSTORE.\n- **Межконтрактной коммуникации в рамках транзакции** — паттерны обратных вызовов, флеш-займы.\n- **Временных разрешений** — временные ERC-20 allowance в рамках одной транзакции.\n\n```solidity\n\u002F\u002F До EIP-1153: блокировка реентрабельности стоит ~5000-20000 газа\nmapping(address => bool) private _locked;\n\n\u002F\u002F После EIP-1153: блокировка реентрабельности стоит ~200 газа\nassembly {\n    if tload(0) { revert(0, 0) }  \u002F\u002F проверка блокировки\n    tstore(0, 1)                   \u002F\u002F установка блокировки\n}\n\u002F\u002F ... тело функции ...\nassembly {\n    tstore(0, 0)                   \u002F\u002F снятие блокировки\n}\n```\n\n## Практическая оптимизация: выбор правильной области\n\nВот система принятия решений для разработчиков MEV-ботов:\n\n1. **Может ли значение жить в стеке?** Используйте стек. Нулевая маржинальная стоимость.\n2. **Нужно больше 16 значений?** Используйте память. Аллоцируйте от свободного указателя.\n3. **Это входные данные только для чтения?** Используйте calldata. 3 газа за 32-байтное чтение.\n4. **Должно сохраняться между транзакциями?** Используйте хранилище. Бюджетируйте 20000+ газа для новых записей.\n5. **Должно сохраняться между фреймами вызовов, но не между транзакциями?** Используйте транзиентное хранилище. 100 газа.\n\n## Заключение\n\nМодель памяти EVM обманчиво проста — четыре области с ясными компромиссами. Но разница в стоимости огромна: ADD на стеке стоит 3 газа, а SSTORE — 20000. Понимание этих затрат на интуитивном уровне — вот что отличает газоэффективные контракты от газорасточительных. В следующей статье мы точно рассчитаем стоимость газа и изучим паттерны оптимизации, способные сэкономить тысячи газа за вызов.","\u003Ch2 id=\"\">Четыре места для ваших данных\u003C\u002Fh2>\n\u003Cp>EVM предоставляет четыре различных области данных, каждая с радикально отличающейся стоимостью, временем жизни и паттернами доступа. Выбор неправильной — самый распространённый источник чрезмерного потребления газа в смарт-контрактах.\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Область\u003C\u002Fth>\u003Cth>Время жизни\u003C\u002Fth>\u003Cth>Стоимость чтения\u003C\u002Fth>\u003Cth>Стоимость записи\u003C\u002Fth>\u003Cth>Размер\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>Стек\u003C\u002Ftd>\u003Ctd>Текущий опкод\u003C\u002Ftd>\u003Ctd>0 (DUP: 3)\u003C\u002Ftd>\u003Ctd>0 (PUSH: 3)\u003C\u002Ftd>\u003Ctd>1024 слова\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Память\u003C\u002Ftd>\u003Ctd>Текущий контекст вызова\u003C\u002Ftd>\u003Ctd>MLOAD: 3*\u003C\u002Ftd>\u003Ctd>MSTORE: 3*\u003C\u002Ftd>\u003Ctd>Расширяемая\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Хранилище\u003C\u002Ftd>\u003Ctd>Постоянное (блокчейн)\u003C\u002Ftd>\u003Ctd>SLOAD: 2100\u002F100\u003C\u002Ftd>\u003Ctd>SSTORE: 20000\u002F2900\u002F100\u003C\u002Ftd>\u003Ctd>2^256 слотов\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Calldata\u003C\u002Ftd>\u003Ctd>Текущий контекст вызова\u003C\u002Ftd>\u003Ctd>CALLDATALOAD: 3\u003C\u002Ftd>\u003Ctd>Только чтение\u003C\u002Ftd>\u003Ctd>Входные данные транзакции\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>*Операции с памятью стоят 3 газа плюс квадратичная стоимость расширения.\u003C\u002Fp>\n\u003Ch2 id=\"\">Стек: быстрый, но маленький\u003C\u002Fh2>\n\u003Cp>Стек — это рабочая память EVM. Каждая арифметическая, сравнительная и логическая операция читает из стека и пишет в стек. Он вмещает до 1024 элементов, каждый шириной 32 байта.\u003C\u002Fp>\n\u003Cp>На практике вы редко используете более 16 слотов стека, потому что опкоды DUP и SWAP достигают только 16 элементов вглубь. Компилятор Solidity управляет расположением стека автоматически, но в Yul и Huff вы управляете им вручную — и ошибки «stack too deep» становятся вашим постоянным спутником.\u003C\u002Fp>\n\u003Cp>Доступ к стеку по сути бесплатен (3 газа за PUSH\u002FDUP\u002FSWAP операции). Это делает стек идеальным местом для промежуточных вычислений, счётчиков циклов и временных значений.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F Вычисление только на стеке: сумма 1 + 2 + 3\n\u002F\u002F Общий газ: 3 + 3 + 3 + 3 + 3 = 15 газа\nlet a := add(1, 2)    \u002F\u002F PUSH1 1, PUSH1 2, ADD\nlet b := add(a, 3)    \u002F\u002F DUP1, PUSH1 3, ADD\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Память: дешёвая, но эфемерная\u003C\u002Fh2>\n\u003Cp>Память — это побайтно-адресуемый массив, который начинается пустым и расширяется по мере необходимости. Она существует только на протяжении текущего контекста вызова — когда CALL, STATICCALL или DELEGATECALL возвращается, память вызываемого уничтожается.\u003C\u002Fp>\n\u003Cp>Доступ к памяти происходит 32-байтными блоками:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>MSTORE(offset, value)\u003C\u002Fstrong> — записать 32 байта по указанному смещению. Стоит 3 газа.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>MLOAD(offset)\u003C\u002Fstrong> — прочитать 32 байта по указанному смещению. Стоит 3 газа.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>MSTORE8(offset, value)\u003C\u002Fstrong> — записать один байт. Стоит 3 газа.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>Свободный указатель памяти\u003C\u002Fh3>\n\u003Cp>Solidity резервирует первые 128 байт памяти для специальных целей:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Смещение\u003C\u002Fth>\u003Cth>Назначение\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>0x00-0x3f\u003C\u002Ftd>\u003Ctd>Рабочее пространство (используется для хеширования)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>0x40-0x5f\u003C\u002Ftd>\u003Ctd>Свободный указатель памяти\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>0x60-0x7f\u003C\u002Ftd>\u003Ctd>Нулевой слот (начальное значение для динамических массивов)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>0x80+\u003C\u002Ftd>\u003Ctd>Свободная память (аллокация начинается здесь)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>\u003Cstrong>Свободный указатель памяти\u003C\u002Fstrong> по адресу \u003Ccode>0x40\u003C\u002Fcode> отслеживает следующий доступный адрес памяти. Когда Solidity выделяет память (например, \u003Ccode>new bytes(100)\u003C\u002Fcode>, \u003Ccode>abi.encode(...)\u003C\u002Fcode>, создание структуры в памяти), он читает указатель, использует этот адрес и продвигает указатель.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F Что Solidity делает внутренне, когда вы пишете:\n\u002F\u002F bytes memory data = new bytes(64);\n\n\u002F\u002F 1. Чтение свободного указателя: MLOAD(0x40) -&gt; 0x80\n\u002F\u002F 2. Сохранение длины массива по 0x80: MSTORE(0x80, 64)\n\u002F\u002F 3. Обновление свободного указателя: MSTORE(0x40, 0x80 + 32 + 64 = 0xE0)\n\u002F\u002F 4. Данные находятся по адресам 0xA0 — 0xDF\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Стоимость расширения памяти\u003C\u002Fh3>\n\u003Cp>Именно здесь память становится дорогой. Стоимость газа для памяти — это не только 3 газа за MLOAD\u002FMSTORE — существует квадратичная стоимость расширения, взимаемая при обращении к памяти за пределами текущей верхней границы.\u003C\u002Fp>\n\u003Cp>Формула (из Yellow Paper):\u003C\u002Fp>\n\u003Cpre>\u003Ccode>memory_cost = (memory_size_words^2 \u002F 512) + (3 * memory_size_words)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>где \u003Ccode>memory_size_words = ceil(наибольший_доступный_байт \u002F 32)\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>Для малого использования памяти (менее ~700 байт) стоимость приблизительно линейна — 3 газа за слово. Но она растёт квадратично:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Использовано памяти\u003C\u002Fth>\u003Cth>Стоимость\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>32 байта (1 слово)\u003C\u002Ftd>\u003Ctd>3 газа\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>1 КБ (32 слова)\u003C\u002Ftd>\u003Ctd>98 газа\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>10 КБ (320 слов)\u003C\u002Ftd>\u003Ctd>1160 газа\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>100 КБ (3200 слов)\u003C\u002Ftd>\u003Ctd>29600 газа\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>1 МБ (32000 слов)\u003C\u002Ftd>\u003Ctd>2 001 000 газа\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Этот квадратичный рост означает, что контракты, обрабатывающие большие массивы в памяти, могут быстро исчерпать лимит газа. Именно поэтому Solidity использует память экономно и предпочитает вычисления на стеке.\u003C\u002Fp>\n\u003Ch2 id=\"\">Хранилище: постоянное, но дорогое\u003C\u002Fh2>\n\u003Cp>Хранилище — это постоянное хранилище ключ-значение EVM. Каждый контракт имеет собственное изолированное пространство хранения с 2^256 32-байтными слотами. Хранилище сохраняется между транзакциями — это состояние блокчейна.\u003C\u002Fp>\n\u003Ch3>Раскладка хранилища в Solidity\u003C\u002Fh3>\n\u003Cp>Solidity последовательно назначает слоты хранилища переменным состояния:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">contract StorageLayout {\n    uint256 public x;       \u002F\u002F слот 0\n    uint256 public y;       \u002F\u002F слот 1\n    address public owner;   \u002F\u002F слот 2 (упаковка: 20 байт)\n    bool public paused;     \u002F\u002F слот 2 (упаковка: 1 байт, тот же слот!)\n    mapping(address =&gt; uint256) public balances;  \u002F\u002F слот 3 (базовый)\n    \u002F\u002F balances[addr] находится по: keccak256(addr . slot_3)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Маппинги и динамические массивы используют \u003Ccode>keccak256\u003C\u002Fcode> для вычисления слота хранилища:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>mapping[key]\u003C\u002Fcode> -&gt; \u003Ccode>keccak256(key || slot_number)\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>array[index]\u003C\u002Fcode> -&gt; \u003Ccode>keccak256(slot_number) + index\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>Стоимость хранилища (после EIP-2929 + EIP-3529)\u003C\u002Fh3>\n\u003Cp>Хранилище — безусловно самая дорогая область данных:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Операция\u003C\u002Fth>\u003Cth>Холодный\u003C\u002Fth>\u003Cth>Тёплый\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>SLOAD\u003C\u002Ftd>\u003Ctd>2100\u003C\u002Ftd>\u003Ctd>100\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>SSTORE (ноль -&gt; ненулевое)\u003C\u002Ftd>\u003Ctd>22100\u003C\u002Ftd>\u003Ctd>20000\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>SSTORE (ненулевое -&gt; ненулевое)\u003C\u002Ftd>\u003Ctd>5000\u003C\u002Ftd>\u003Ctd>2900\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>SSTORE (ненулевое -&gt; ноль)\u003C\u002Ftd>\u003Ctd>5000 + 4800 возврат\u003C\u002Ftd>\u003Ctd>2900 + 4800 возврат\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Установка слота хранилища из нуля в ненулевое значение стоит \u003Cstrong>20000 газа\u003C\u002Fstrong>, потому что дерево состояний Ethereum должно вставить новый узел-лист. Вот почему инициализация новой записи маппинга (например, баланса токена для нового держателя) так дорога.\u003C\u002Fp>\n\u003Ch3>Упаковка хранилища\u003C\u002Fh3>\n\u003Cp>Solidity упаковывает несколько маленьких переменных в один 32-байтный слот, когда это возможно. Это критическая оптимизация:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F ПЛОХО: 3 слота хранилища = 3 x SLOAD = 6300 газа (холодный)\ncontract Unpacked {\n    uint256 a;  \u002F\u002F слот 0 (32 байта)\n    uint256 b;  \u002F\u002F слот 1 (32 байта)\n    uint256 c;  \u002F\u002F слот 2 (32 байта)\n}\n\n\u002F\u002F ХОРОШО: 1 слот хранилища = 1 x SLOAD = 2100 газа (холодный)\ncontract Packed {\n    uint64 a;   \u002F\u002F слот 0, байты 0-7\n    uint64 b;   \u002F\u002F слот 0, байты 8-15\n    uint64 c;   \u002F\u002F слот 0, байты 16-23\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"calldata\">Calldata: только чтение и дёшево\u003C\u002Fh2>\n\u003Cp>Calldata — это входные данные, отправленные с транзакцией или вызовом. Они доступны только для чтения, и обращение к ним дёшево (3 газа за CALLDATALOAD). Для внешних вызовов функций Solidity ABI-кодирует аргументы в calldata.\u003C\u002Fp>\n\u003Cp>Использование \u003Ccode>calldata\u003C\u002Fcode> вместо \u003Ccode>memory\u003C\u002Fcode> для параметров функций позволяет избежать копирования данных и значительно экономит газ:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F ПЛОХО: копирует весь массив в память\nfunction sum(uint256[] memory arr) external returns (uint256) { ... }\n\n\u002F\u002F ХОРОШО: читает напрямую из calldata\nfunction sum(uint256[] calldata arr) external returns (uint256) { ... }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"eip-1153\">Транзиентное хранилище (EIP-1153)\u003C\u002Fh2>\n\u003Cp>EIP-1153 (обновление Cancun, март 2024) ввёл два новых опкода: \u003Cstrong>TSTORE\u003C\u002Fstrong> и \u003Cstrong>TLOAD\u003C\u002Fstrong>. Транзиентное хранилище подобно обычному хранилищу, но автоматически очищается в конце транзакции.\u003C\u002Fp>\n\u003Cp>Ключевые свойства:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Та же модель ключ-значение, что и у хранилища (2^256 слотов по 32 байта)\u003C\u002Fli>\n\u003Cli>Стоимость 100 газа для TLOAD и TSTORE (аналогично тёплому SLOAD\u002FSSTORE)\u003C\u002Fli>\n\u003Cli>Очищается после завершения транзакции — нет постоянного изменения состояния\u003C\u002Fli>\n\u003Cli>Доступно между фреймами вызовов в рамках одной транзакции\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Идеально подходит для:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Блокировки реентрабельности\u003C\u002Fstrong> — установить флаг через TSTORE, проверить через TLOAD. Без стоимости 20000 газа SSTORE.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Межконтрактной коммуникации в рамках транзакции\u003C\u002Fstrong> — паттерны обратных вызовов, флеш-займы.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Временных разрешений\u003C\u002Fstrong> — временные ERC-20 allowance в рамках одной транзакции.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F До EIP-1153: блокировка реентрабельности стоит ~5000-20000 газа\nmapping(address =&gt; bool) private _locked;\n\n\u002F\u002F После EIP-1153: блокировка реентрабельности стоит ~200 газа\nassembly {\n    if tload(0) { revert(0, 0) }  \u002F\u002F проверка блокировки\n    tstore(0, 1)                   \u002F\u002F установка блокировки\n}\n\u002F\u002F ... тело функции ...\nassembly {\n    tstore(0, 0)                   \u002F\u002F снятие блокировки\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Практическая оптимизация: выбор правильной области\u003C\u002Fh2>\n\u003Cp>Вот система принятия решений для разработчиков MEV-ботов:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Может ли значение жить в стеке?\u003C\u002Fstrong> Используйте стек. Нулевая маржинальная стоимость.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Нужно больше 16 значений?\u003C\u002Fstrong> Используйте память. Аллоцируйте от свободного указателя.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Это входные данные только для чтения?\u003C\u002Fstrong> Используйте calldata. 3 газа за 32-байтное чтение.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Должно сохраняться между транзакциями?\u003C\u002Fstrong> Используйте хранилище. Бюджетируйте 20000+ газа для новых записей.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Должно сохраняться между фреймами вызовов, но не между транзакциями?\u003C\u002Fstrong> Используйте транзиентное хранилище. 100 газа.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"\">Заключение\u003C\u002Fh2>\n\u003Cp>Модель памяти EVM обманчиво проста — четыре области с ясными компромиссами. Но разница в стоимости огромна: ADD на стеке стоит 3 газа, а SSTORE — 20000. Понимание этих затрат на интуитивном уровне — вот что отличает газоэффективные контракты от газорасточительных. В следующей статье мы точно рассчитаем стоимость газа и изучим паттерны оптимизации, способные сэкономить тысячи газа за вызов.\u003C\u002Fp>\n","ru","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:23.385998Z","Глубокое погружение в модель памяти EVM: стек, память, хранилище, calldata, транзиентное хранилище и формула стоимости расширения памяти.","EVM модель памяти",null,"index, follow",[21,26,30],{"id":22,"name":23,"slug":24,"created_at":25},"c0000000-0000-0000-0000-000000000016","EVM","evm","2026-03-28T10:44:21.513630Z",{"id":27,"name":28,"slug":29,"created_at":25},"c0000000-0000-0000-0000-000000000020","Gas Optimization","gas-optimization",{"id":31,"name":32,"slug":33,"created_at":25},"c0000000-0000-0000-0000-000000000014","Solidity","solidity","Блокчейн",[36,42,48],{"id":37,"title":38,"slug":39,"excerpt":40,"locale":12,"category_name":34,"published_at":41},"de000000-0000-0000-0000-000000000013","Уровень интероперабельности Ethereum: как 55+ L2 становятся одной сетью","uroven-interoperabelnosti-ethereum-kak-55-l2-stanovyatsya-odnoj-setyu","У Ethereum 55+ роллапов Layer 2, фрагментирующих ликвидность и пользовательский опыт. Уровень интероперабельности Ethereum — объединяющий кросс-роллап-мессаджинг, общие секвенсоры и based-роллапы — призван объединить их в единую компонуемую сеть.","2026-03-28T10:44:36.068675Z",{"id":43,"title":44,"slug":45,"excerpt":46,"locale":12,"category_name":34,"published_at":47},"de000000-0000-0000-0000-000000000012","ZK-доказательства за пределами роллапов: верифицируемый AI-инференс на Ethereum","zk-dokazatelstva-za-predelami-rollapov-verificiruemyj-ai-inferens-ethereum","Доказательства с нулевым разглашением — это уже не только инструмент масштабирования. В 2026 году zkML обеспечивает верифицируемый AI-инференс в блокчейне, ZK-копроцессоры переносят тяжёлые вычисления оффчейн с ончейн-верификацией, а новые системы доказательств SP1 и Jolt делают это практичным.","2026-03-28T10:44:36.026310Z",{"id":49,"title":50,"slug":51,"excerpt":52,"locale":12,"category_name":34,"published_at":53},"dd000000-0000-0000-0000-000000000013","EIP-7702 на практике: создание потоков смарт-аккаунтов после Pectra","eip-7702-na-praktike-sozdanie-potokov-smart-akkauntov-posle-pectra","EIP-7702 позволяет любому EOA Ethereum временно действовать как смарт-контракт в рамках одной транзакции. Вот как реализовать пакетные транзакции, спонсирование газа и социальное восстановление с помощью нового примитива абстракции аккаунтов.","2026-03-28T10:44:35.357227Z",{"id":13,"name":55,"slug":56,"bio":57,"photo_url":18,"linkedin":18,"role":58,"created_at":59,"updated_at":59},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]