一个零也要占一个字节:PUSH0 想解决的字节税 图 1
一个零也要占一个字节:PUSH0 想解决的字节税 · 图 1

压一个零要交多少税

EVM 的栈机器里,往栈上放常数要用 PUSH 系列指令,最短的是 PUSH1 0——两个字节(操作码加立即数),运行时三元 gas,部署时每个字节二百 gas。看起来微不足道,但零是合约里最高频的常数:调用时不带返回数据偏移、循环初始化、边界哨兵,处处是零。提案列举了编译器年代久代的生存智慧——为了省钱,实现们用 PCMSIZECALLDATASIZERETURNDATASIZECODESIZECALLVALUESELFBALANCE 这些”恰好会把零压上栈”的指令来偷梁换柱。它们语义上并不承诺返回零(PC 取决于代码位置、MSIZE 取决于内存水位),靠这些指令取零等于把正确性押在指令的巧合行为上,可读性和审计性都很糟糕。

EIP-3855 的修法坦率:直接引入 PUSH0,码位 0x5f,不带立即数、不弹栈、压一个零,运行成本 2 gas,整条指令只占一个字节。对部署成本敏感的项目,省下的是每条零出现处的一个字节,按每字节部署 gas 折算成乘数就是二百 gas;更重要的是编译器不再需要在”用巧合指令”和”多花字节”之间做道德抉择。该提案随上海升级生效,如今现代工具链生成的字节码里已随处可见。

一个零也要占一个字节:PUSH0 想解决的字节税 图 2
一个零也要占一个字节:PUSH0 想解决的字节税 · 图 2

兼容性的暗礁

PUSH0 也留下一个经典教训:新指令是向后不兼容的隐性雷。在支持 PUSH0 的链上编译部署、再到尚未升级的同构链(分叉节奏不同的侧链、测试网、专用链)上执行,遇到 0x5f 会直接按无效操作码回滚。这段兼容性裂缝让”目标链集合的分叉状态”成为部署检查单上的固定条目。

一条指令的经济学

把 PUSH0 放进 EVM 的历史里看更有趣。EVM 的指令集从来不是免费自助餐厅:每次扩容都要权衡栈机一致性、解析器兼容与工具链迁移成本,所以一条”压零”的指令拖到二〇二三年才进主网,而它解决的问题在一九九八年就被汇编器们用各种花活绕了二十多年。EIP 文本里那份”替代指令清单”(PC、MSIZE、CALLDATASIZE 等)本质是一份民间智慧的化石层——每条被滥用的巧合背后,都是当年省字节的理性选择。指令集设计者的难处也在此:不补一条正门,民间就会自己搭违章的侧门,而侧门的语义是脆的——哪天编译器想让 MSIZE 前先分配一段内存,靠它取零的代码当场出错。

对合约工程师,这条小指令留下的长期教训有两条。其一是编译器版本纪律:同一份 Solidity 源码在不同目标版本下生成的字节码可能含或不含新指令,部署脚本里应显式固定目标 EVM 版本并纳入测试矩阵,别让”默认最新”替你决定兼容性。其二是对跨链部署的提醒:以太坊主网、各层二与独立链的分叉清单各有时差,字节码是便携的、分叉状态不是——把”同一条字节码在任何 EVM 链等价”当作默认假设的年代,在 PUSH0 之后正式结束了。

对工具链作者还有一层冷幽默式的长期影响:反编译器与审计器需要把 0x5f 识别为一条独立指令而非”未定义操作码”,字节码相似度工具需要把 PUSH1 0 与 PUSH0 归一化,否则同一份源码在不同版本编译产物间的差异会被误报成逻辑变化。指令集扩容的维护税,从来不只由链上节点缴纳。

快速问答

问:PUSH0 为什么定价 2 gas? 答:提案把它定为 base 档——与 ADDRESS 一类无访存的基础指令同价,比 PUSH1 便宜 1,反映”单字节、无立即数读取”的更轻工作量。

问:怎么在旧工具链里禁用 PUSH0? 答:主流编译器提供目标 EVM 版本开关,选定分叉代号后自动回退到 PUSH1 0 或巧合指令策略。

问:它对运行费的影响大吗? 答:单次可忽略,巨型合约里零的出现次数多时合计可观;主要红利仍在部署字节数与gas 押金。

风险提示:跨链或跨分叉部署字节码存在兼容性与资金损失风险,请先验证目标环境;本文不构成投资建议。