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 知识体系总览
下面这张图,把本章的核心逻辑串起来了:
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