EVM 里的内存不是 RAM:临时工作区如何按字数收“扩张费” 图 1
EVM 里的内存不是 RAM:临时工作区如何按字数收“扩张费” · 图 1

看合约 Gas 分析工具时,常出现一类叫“内存扩张”的费用项,初学者容易把它理解成“程序占了多少内存条”。实际上 EVM 的内存和操作系统里的 RAM 是两回事。本文讲清这块工作区是什么、怎么计费、对费用和调用上限有什么影响。已有文章讲过 Gas Limit 与 Gas Price 的区别,本文补充 Gas 被消耗的一个内部来源。

一块每次执行都从零开始的草稿纸

EVM 执行合约代码时,虚拟机维护一个按字节寻址的线性数组,这就是内存。三个特性决定它的全部性格:第一,初始为空,每次交易执行开始时长度为零;第二,按 32 字节为一字增长,访问到第 n 字节会把数组扩张到覆盖它的整字;第三,执行结束即释放,不写进链、不需要任何人清理,下一笔交易看到的是全新一张草稿纸。它与存储的关系最容易被新手混淆:访问存储是查一本登记在册的账外台账,读写各自收固定的高额 Gas 且内容永久上链;内存只是演算纸,便宜但不留痕。MLOAD、MSTORE 这类指令各收很少的固定 Gas,真正的变量是扩张成本。

二次方扩张费在算什么

内存计费的核心思想是“用得越多,单位越便宜但总量加速增长”:扩张到若干字的内存,费用包含与字数平方成正比的一项,加上与字数成正比的线性一项,常数取自协议费用表(平方项的分母是 512,每字线性成本为 3 Gas)。直觉解释:虚拟机必须把整块内存保持在内存中供任意读写,规模越大对节点资源压力越大,平方项让超大内存用量变得不经济,从而在协议层压住“用一笔交易逼迫节点分配巨大缓冲区”的拒绝服务攻击。实际效果是,前几百字几乎白送,超过约百字级别后每字成本线性上升,合约作者因此有强烈动机复用低位地址、避免稀疏开辟。字节码里常见的先向第 0 字写入再向第 64 起区域操作的模式,就是编译器在精打细算这张草稿纸。

用户视角的两个后果

其一,内存扩张费计入交易的 Gas 消耗,一个中间值哈希、一段超长 calldata 的处理都会体现在 Gas 用量里,这也是哈希类指令费用随输入长度增长的底层原因之一——SHA3 指令按要读取的字数同时收读取费和扩张费。其二,Gas Limit 间接封住了内存上限:扩张费平方增长,任何交易能负担的内存总量远小于理论地址空间,2021 年曾有提案(EIP-3337)试图给内存设显式硬上限,长期停留在搁置状态而未进入主网,现行约束仍然是这条隐形曲线。

常见误区

一是把“内存用量”当持久资源:关掉交易一切归零,不存在“清理内存”的操作。二是以为降低内存用量能显著省 Gas:常规函数的内存费在总账单里占比通常很小,除非处理超大数组或字节串。三是用桌面程序的经验推断“Gas 用完内存就满了”:两者是不同资源池,Gas 耗尽只是中止执行并收下已烧费用,内存从未成为独立瓶颈。

一段循环的内存轨迹

设想合约循环处理 1000 个元素的数组:每轮若只在低位地址反复读写,内存停在几十个字不再增长,扩张费一次付清;若每轮都往高位新开辟区域,平方项逐轮加码,总费用按圈数的平方爬升。同一段算法,两种内存用法,费用曲线是线性对平方——审计 Gas 报告时,看内存高水位线比看指令条数更早发现问题。

快速问答

问:合约能在内存里“存”用户余额吗?答:不能,执行结束即蒸发,所有需要留存的状态必须写入存储。问:内存扩张费现在是多少?答:随用量动态计算,平方与线性两项按协议费用表常数套用,静态值无法一概而论。问:和 calldata 谁贵?答:calldata 每字节按交易数据规则收费且发生在执行前,内存扩张费只在实际读写触及时产生,量级与场景各自独立。

风险提示:本文为虚拟机机制科普,不构成投资建议。Gas 结构随协议升级可能调整,费用细节以当期协议规范为准。