分页内存计费是什么:EIP-3336想给EVM换一套算账方式 图 1
分页内存计费是什么:EIP-3336想给EVM换一套算账方式 · 图 1

经常有人说以太坊虚拟机的内存是”一条从0号地址开始的直线”,这不只是比喻,而是计费事实:EVM 收内存费时,看的是这次执行最高触碰到了哪个地址,从0到这个地址之间的每一段都要付钱,哪怕中间大部分根本没被写过。EIP-3336 在2021年3月提出把这套规则改成分页:内存按固定大小的页分配,程序实际用到哪几页,就只为那几页付账。提案状态是 Stagnant(因超过半年无活动而搁置),但它提出的问题至今还在影响编译器作者怎么写代码。

线性计费逼出来的内存挤压

按现行规则,只要程序在很高的地址写了一个字节,0到那里之间的全部空间都按内存扩张费收一遍。于是编译器生成字节码时被迫做一道很别扭的题:变量在内存里的位置必须尽量紧凑,任何”先留一段以后再说”的做法都是白烧 Gas。对象在运行时搬家、堆和栈想各占一块地盘、给未来变量预留位置——这些在普通程序里稀松平常的手法,在EVM里都会因为把最高水位抬高而直接变成账单。提案把这一点说得很直白:现有规则只适合简单的程序形态,它让内存复用产生的重排和拷贝全都由用户付钱。

页式计费怎么运转

EIP-3336 给出的参数很克制:常量 PAGE_BITS 取10,也就是每页2的10次方即1024字节;地址的高位部分当作页号,低10位当作页内偏移;每页首次被读或写时才真正分配,内容默认全零;另外定义了一个 PAGE_BASE_COST 为96的页数基础成本项。实现上鼓励用哈希表存页表,没用到的页根本不存在。这样一段合约代码完全可以把栈放在低地址区、把长期数据丢到高位页里,中间隔着十个没人碰的页也不用付费,就像操作系统里虚拟内存按需换页的极简版。配套提案EIP-3337负责给加载和存储指令补上帧指针寻址,两份文件是一起讨论的。

为什么停在半路

这个改动要求硬分叉,计费模型又牵动每条内存相关指令的语义,成本收益很难算清楚。EVM社区后来在内存问题上的主要精力转向了别的方向,3336和3337都进入了Stagnant状态,页表的地址切分、跨页读写要不要加价的细节也没能等到定稿。作为冷知识它仍有价值:它标记了EVM设计里一个反复出现的教训——gas计费模型不是中性的账房规则,它决定编译器敢生成什么样的代码。

快速问答

问:分页后地址空间有多大可用? 答:提案里地址仍是256位,高246位是页号、低10位是页内偏移,理论上可用的页数量极其庞大,但没被触碰的页不占账单也不占节点内存。 问:跨页的一次读写会多收钱吗? 答:提案在理由部分专门讨论过跨页读写的附加费如何定,但该定价细节没有随状态推进而落定,应以文件当时的文本为准。 问:今天写合约还需要关心这件事吗? 答:现行主网仍是线性计费,编译器仍按紧凑内存布局生成代码;3336只是提案史上的一条支线,不构成任何功能变化。

一笔直觉账

不妨给两种计费各算一笔最朴素的账。设想一个程序只在内存低位用到400字节,但在第65000字节的位置临时存了一个标志位。线性计费下,这条程序要为从0一直到65032的整段空间按扩张公式付费,哪怕其中九十八个字节从未被碰过;分页计费下,程序只为两个页付账——真正用到的那一页和标志位所在的那一页,合计约两千字节的账单。差距从几个数量级到几乎为零,完全取决于程序的内存散乱程度。这也解释了为什么提案对”编译器生成紧凑代码”的常规场景收益平平,而对带大查找表、多作用域对象的复杂合约格外有吸引力。当然代价也存在:跨页读写要多付一次记账、页表本身要让客户端多维护一个数据结构,天下没有免费的地址空间。

风险提示:本文介绍的是未上主网的历史提案机制,不构成投资建议,也不预示任何费用参数会按此方向调整。