EIP-3855: PUSH0 — опкод для числа, которое нужно всем
Положить константу ноль на стек — одна из самых частых операций в коде EVM, и до 2023 года для неё не было отдельной инструкции. Компиляторы либо тратили лишний байт на PUSH1 0, либо прибегали к хаку. PUSH0 — однобайтовый опкод, который делает ровно одно дело, а поскольку ноль встречается повсюду, эта крошечная экономия накапливается в каждом контракте.
- стек и опкоды EVM
- газ и размер кода
Shanghai, апрель 2023 года. Не каждый значимый апгрейд грандиозен; некоторые просто устраняют мелкий порез, который годами чувствовал каждый контракт. Вот этот порез: положить число ноль на стек — одна из самых частых операций во всём коде EVM: инициализация переменных, сравнения, обнуление памяти, шаблонный код вокруг каждой функции, — а у EVM не было для этого опкода. Есть опкоды, чтобы протолкнуть любую константу от 1 до 32 байт, но не аккуратный опкод для самой используемой константы из всех. EIP-3855 добавляет PUSH0. Это едва ли не самый маленький EIP из возможных, и хороший урок того, почему «маленькое × повсюду» стоит хардфорка.
Проблема: два плохих способа протолкнуть ноль
Чтобы положить ноль на стек, у кода EVM было два варианта, и ни один не был хорош. Очевидный — PUSH1 0x00, но это два байта (опкод PUSH1 плюс его непосредственный операнд 0x00) и стоит 3 газа — тратится байт драгоценного кода контракта лишь на то, чтобы закодировать константу, которую машина могла бы произвести бесплатно. Поэтому компиляторы схитрили и злоупотребляли трюком с RETURNDATASIZE (RETURNDATASIZE trick) Использование опкода RETURNDATASIZE (1 байт, 2 газа), чтобы протолкнуть ноль, эксплуатируя тот факт, что returndata пусто (размер 0) в тех местах, где компиляторы это знали заранее. Меньше, чем PUSH1 0, но это хак, зависящий от контекста, а не от намерения. : однобайтовым опкодом, который в нужном контексте возвращает 0. Он экономит байт, но это хак — вы читаете значение, которое означает «размер данных возврата последнего вызова», полагаясь на то, что оно равно нулю, вместо того чтобы сказать, что вы имеете в виду.
→ Шаг 2: дать нулю собственную инструкцию.
PUSH0: один байт, одна задача
PUSH0 Опкод 0x5F, добавленный в Shanghai. Он проталкивает константу 0 на стек. Один байт кода, 2 газа (базовый тариф), и он говорит ровно то, что делает. (опкод 0x5F) делает ровно одно: проталкивает значение 0. Это один байт кода стоимостью 2 газа — меньше и дешевле, чем PUSH1 0, и, в отличие от трюка с RETURNDATASIZE, он честен насчёт своего намерения, так что его нельзя сломать изменением контекста. Он аккуратно занимает своё место в пространстве опкодов прямо перед PUSH1 (0x60), завершая семейство «протолкнуть константу» случаем нулевой длины.
→ Шаг 3: умножаем на «повсюду».
Итог: байт, сэкономленный миллионы раз
Один байт кода — это ничто; байт, сэкономленный на каждом проталкивании нуля в каждом контракте, — это много. Поскольку ноль проталкивается постоянно, замена PUSH1 0 на PUSH0 заметно уменьшает байткод контрактов — а размер кода является главной статьёй стоимости развёртывания контракта, к тому же ограниченной 24 КБ, так что меньший код означает более дешёвое развёртывание и больше запаса для сложных контрактов. Исполнение тоже становится чуть дешевле, а компиляторы могут отказаться от хака с RETURNDATASIZE в пользу того, что действительно говорит «протолкни ноль».