3、基础Gas模型:EVM Opcode Gas表、静态Gas与动态Gas、内存扩展成本

Gas模型,说白了就是以太坊虚拟机(EVM)的「计价器」。你写的每一行代码,每一条指令,都得花钱。我刚开始接触Solidity时,总觉得Gas是个很遥远的概念——直到有一次部署合约,发现Gas费比预估高了整整三倍,项目方差点跟我翻脸。嗯,从那以后,我养成了先看Opcode Gas表的习惯。

3.1 EVM Opcode Gas表:你的代码成本清单

EVM的每条指令都有固定的Gas消耗。我把常用的Opcode Gas整理成了一张表,你写合约时可以对照着看:

Opcode 名称 Gas消耗 说明
STOP 停止执行 0 免费,但很少单独用
ADD / SUB 加减法 3 基础算术,很便宜
MUL / DIV 乘除法 5 比加减贵一点
MOD 取模 5 和乘除同价
ADDMOD / MULMOD 模加/模乘 8 带模运算,稍贵
EXP 指数运算 10 + 50 * byte(指数) 指数越大越贵,小心使用
SHA3 Keccak256哈希 30 + 6 * 数据长度(字) 哈希不便宜,数据越长越贵
SLOAD 读取存储 100(热访问)/ 2100(冷访问) 存储读取是主要成本之一
SSTORE 写入存储 20000(新写入)/ 5000(修改)/ 2900(清零返还) 存储写入最贵,能省则省
BALANCE 查询余额 700 查别人余额也不便宜
CALL 调用合约 700 + 转账费用 外部调用成本高
RETURN 返回数据 0 返回本身免费,但数据大小影响内存成本

核心要点:SLOAD和SSTORE是Gas消耗的大头。我见过一个合约,循环里反复读写存储,结果一次交易花了0.5 ETH的Gas费。你想想看,这谁受得了?

3.2 静态Gas vs 动态Gas:固定成本与浮动成本

Gas分为两种:静态Gas和动态Gas。理解它们的区别,是优化Gas的第一步。

静态Gas,就是指令本身固定的消耗。比如ADD永远是3 Gas,MUL永远是5 Gas。这部分你没法省,写多少指令就花多少钱。

动态Gas,则取决于操作的数据大小、状态或上下文。比如EXP指令,指数越大越贵;SHA3指令,数据越长越贵。最典型的动态Gas是SSTORE——新写入一个slot要20000 Gas,修改一个已有的slot只要5000 Gas,清零还能返还2900 Gas。

我的经验:我曾经优化过一个NFT合约,把存储从「每次写入新值」改成「先检查再写入」,结果Gas费降了40%。说白了,就是利用动态Gas的规则——能修改就别新建,能清零就别留着。

为什么会这样设计?EVM的开发者希望鼓励「复用」和「清理」。你想想看,链上存储空间是稀缺资源,你占着茅坑不拉屎,当然要多收费。反过来,你主动释放存储,EVM就给你打折。

3.3 内存扩展成本:被忽视的隐形杀手

很多人只盯着存储(Storage)的Gas,却忽略了内存(Memory)的成本。内存扩展成本,是EVM根据你使用的内存大小动态计算的。

EVM的内存是按字(32字节)扩展的。你第一次访问内存时,它只有0字节。当你写入第1个字节时,EVM会扩展到32字节(1个字)。当你写入第33个字节时,扩展到64字节(2个字)。以此类推。

内存扩展的Gas计算公式是:

// 内存大小以字节为单位
memory_size_bytes = 当前使用的最大内存偏移量 + 32(向上取整到32的倍数)
words = memory_size_bytes / 32

// 扩展成本
cost = (words * words) / 512 + (3 * words)

嗯,这个公式看着有点绕。我举个例子你就明白了:

使用内存(字节) 需要的字数 扩展成本(Gas)
0 - 32 1 3
33 - 64 2 6
65 - 96 3 9
97 - 128 4 12
129 - 160 5 15
1000 32 98
10000 313 约 1,080
100000 3125 约 28,500

注意:内存扩展成本是平方级增长的!当内存超过几千字节后,成本会急剧上升。我曾经见过一个合约,在循环里不断拼接字符串,结果内存扩展到几万字节,Gas费直接爆了。后来改成固定大小缓冲区,问题就解决了。

所以我的建议是:尽量预分配内存,避免动态扩展。如果你知道最大需要多少内存,一次性分配好,比边用边扩展要便宜得多。

3.4 知识体系总览

下面这张图,把本章的核心逻辑串起来了:

基础Gas模型知识体系 EVM Gas模型 Opcode Gas表 静态Gas vs 动态Gas 内存扩展成本 Opcode Gas表 • 每条指令固定Gas消耗 • SLOAD: 100/2100 Gas • SSTORE: 20000/5000/2900 Gas • EXP: 10 + 50 * byte(指数) 静态Gas vs 动态Gas • 静态:固定消耗,无法优化 • 动态:取决于数据/状态 • 利用规则:修改优于新建 • 清零可获Gas返还 内存扩展成本 • 按字(32字节)扩展 • 成本 = words²/512 + 3*words • 平方级增长,小心大内存 • 建议:预分配固定缓冲区 核心原则:能省则省,预分配优于动态扩展

3.5 实战避坑指南

说了这么多理论,最后分享几个我踩过的坑:

  • 别在循环里读写存储。 我曾经写过一个批量转账合约,循环里每次调用SSTORE,结果Gas费高得离谱。后来改成先计算再一次性写入,Gas降了60%。
  • 小心字符串拼接。 字符串拼接会不断扩展内存,成本是平方级增长的。如果你需要拼接大量字符串,用abi.encodePacked或者固定大小缓冲区。
  • 利用Gas返还机制。 如果你确定某个存储变量不再需要,主动清零它。EVM会返还2900 Gas,相当于打了个大折扣。
  • 冷热存储访问。 同一个slot第一次读取是2100 Gas(冷访问),后续在同一笔交易中再读只要100 Gas(热访问)。所以尽量把相关数据放在同一个slot里。

我的习惯:每次写完合约,我都会用Remix的Debugger逐条指令看Gas消耗。你会发现很多意想不到的「隐形消费」——比如一次看似无害的require,背后可能藏着SLOAD。嗯,细节决定成败。

好了,Gas模型的基础就讲到这里。记住:理解Opcode Gas表、区分静态与动态Gas、警惕内存扩展成本,这三件事做好了,你的合约Gas优化就成功了一半。


公众号:蓝海资料掘金营,微信deep3321