No history yet

Архитектура EVM и хранение

Архитектура хранения данных в EVM

Виртуальная машина Ethereum (EVM) — это среда, в которой выполняются все смарт-контракты. Чтобы писать эффективный и экономный код, нужно понимать, как EVM работает с данными. В отличие от обычных компьютеров, где память относительно дешева, в блокчейне каждый байт и каждая операция имеют свою цену. EVM использует несколько областей для хранения данных, каждая со своим назначением, стоимостью и жизненным циклом.

Основными областями хранения являются Storage (хранилище), Memory (память), Calldata (данные вызова) и Stack (стек). Давайте разберемся, как они устроены и чем отличаются.

Storage: постоянная память контракта

Storage — это постоянное хранилище контракта, его «жесткий диск». Данные, записанные в Storage, сохраняются в блокчейне навсегда и изменяются только при выполнении транзакций, вызывающих функции контракта. Это самая дорогая область памяти.

Хранилище организовано как огромное виртуальное пространство, разделенное на слоты. Каждый слот имеет размер 32 байта (256 бит). Переменные состояния контракта (объявленные на уровне контракта, а не внутри функции) по умолчанию размещаются в этих слотах последовательно, начиная с нулевого.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SimpleStorage {
    uint256 public myNumber; // Занимает слот 0
    address public owner;    // Занимает слот 1
    bool public isEnabled;  // Занимает слот 2
}

В этом примере myNumber попадет в слот 0, owner — в слот 1, а isEnabled — в слот 2. Даже bool, который требует всего один бит, займет целый 32-байтный слот, если он объявлен таким образом. Это крайне неэффективно.

Операция записи в Storage (SSTORE) — одна из самых дорогих в EVM. Первая запись в пустой слот стоит 20 000 газа, а перезапись существующего значения — 5 000 газа. Именно поэтому оптимизация работы со Storage так важна.

Упаковка переменных для экономии

Чтобы сэкономить место и газ, EVM позволяет «упаковывать» несколько переменных, размер которых в сумме не превышает 32 байт, в один слот. Это называется упаковкой переменных (variable packing). Компилятор Solidity пытается делать это автоматически, если переменные меньшего размера объявлены одна за другой.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract PackedStorage {
    uint128 public valueA;  // 16 байт
    uint128 public valueB;  // 16 байт
    // valueA и valueB вместе займут один слот (слот 0)

    address public contractOwner; // 20 байт
    uint32 public creationTime;   // 4 байта
    bool public isActive;          // 1 байт
    // contractOwner, creationTime и isActive вместе займут один слот (слот 1)
}

В контракте PackedStorage переменные valueA и valueB (каждая по 16 байт) будут размещены в одном 32-байтном слоте. Аналогично, contractOwner (20 байт), creationTime (4 байта) и isActive (1 байт) вместе займут 25 байт и тоже уместятся в один слот. Таким образом, мы использовали два слота вместо пяти.

Временные области: Memory, Calldata и Stack

Помимо постоянного Storage, EVM использует несколько временных областей памяти, которые очищаются после завершения выполнения транзакции.

ОбластьНазначениеЖизненный циклИзменяемостьСтоимость
MemoryХранение временных данных внутри функцииВремя выполнения функцииИзменяемаяСредняя
CalldataАргументы внешних вызовов функцийВремя выполнения транзакцииНеизменяемаяНизкая
StackВыполнение операций, локальные переменныеВремя выполнения функцииИзменяемыйОчень низкая

Memory — это временное, изменяемое хранилище, похожее на оперативную память (RAM) в обычном компьютере. Оно используется для хранения сложных типов данных (строк, массивов, структур) внутри функций. Память очищается после каждого вызова функции. Стоимость использования Memory растет квадратично с увеличением ее размера, чтобы предотвратить злоупотребления.

Calldata — это специальная, неизменяемая область памяти, где хранятся аргументы, переданные в функцию извне (например, из кошелька пользователя или другого контракта). Это самая дешевая область для хранения данных, поскольку данные не копируются в память EVM, а считываются напрямую из данных транзакции. Ее следует использовать для аргументов функций с видимостью external.

Stack (стек) — это основная рабочая область EVM. Это LIFO-структура (Last-In, First-Out), где выполняются все вычисления. Опкоды EVM берут свои аргументы с вершины стека и помещают туда результат. Стек имеет ограниченный размер (1024 элемента), и работа с ним очень дешева. Локальные переменные простых типов (uint, bool, address) обычно хранятся прямо на стеке.

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

Quiz Questions 1/6

Какая область хранения данных в EVM является постоянной и самой дорогой для использования?

Quiz Questions 2/6

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