直达EVM底层:从字节码压榨以太坊虚拟机Gas消耗
2026/6/3 23:11:03 网站建设 项目流程

直达EVM底层:从字节码压榨以太坊虚拟机Gas消耗

一、Hash的"双层别墅"与存储布局

Hash最近换了一个新饲养箱——上下两层的"复式别墅"。上层是加热区,铺着爬虫地毯,放着UVB灯和加热石;下层是躲避区,铺着椰土,放着一个大号躲避穴和一个水盆。

每天早晨,Hash都会从下层的躲避穴慢慢爬到上层,趴在加热石上等我喂蟋蟀。晚上,他又会回到下层的躲避穴休息。

看着Hash在这两层之间来回穿梭,我突然想到:这不就是EVM的存储布局吗?

  • 上层(加热区)= 经常访问的"热存储",高效但宝贵
  • 下层(躲避区)= 低频访问的数据,可以放得更随便
  • Hash每天的路线= 合约的访问模式(Access Pattern)

EIP-1967透明代理和EIP-2535钻石协议的存储优化,本质上就是在帮EVM找到最高效的"Hash路线"——让热数据在最容易访问的位置,让冷数据规整排列,减少不必要的数据搬运。

二、EVM存储布局的底层原理

2.1 EVM Storage的四层架构

在深入代理模式之前,我们需要先理解EVM存储的分层结构:

flowchart TD subgraph "EVM存储分层架构" L1["L1: State Trie (世界状态树)"] L2["L2: Storage Trie (合约存储树)"] L3["L3: Storage Slot (2²⁵⁶槽位空间)"] L4["L4: 32字节数据单元"] end L1 --> L2 L2 --> L3 L3 --> L4
层级名称访问成本说明
L1State Trie极高(Merkle Proof需~30k Gas)全局状态根,仅区块验证时访问
L2Storage Trie中(SLOADcold = 2,100 Gas)合约的存储树,每次SLOAD需要证明
L3Storage Slot低(SLOADwarm = 100 Gas)256位地址空间,访问过的槽变warm
L432字节单元极低单槽内的数据操作

核心洞察:Gas优化的本质是最大化在L3和L4层完成的操作,避免触发新的L2访问。

2.2 Storage Slot的访问模式

EVM在一个交易(Transaction)的上下文中维护了一个"warm storage"集合:

首次访问某个槽 → cold SLOAD (2,100 Gas) → 加入warm集合 同交易再次访问该槽 → warm SLOAD (100 Gas) → 节省95.2%!
// 实测cold vs warm SLOAD contract SlotAccessTest { uint256 public data; // Slot 0 function readOnce() external view returns (uint256) { return data; // cold SLOAD: 2,100 Gas } function readTwice() external view returns (uint256, uint256) { uint256 a = data; // cold SLOAD: 2,100 Gas uint256 b = data; // warm SLOAD: 100 Gas return (a, b); // 第二次节省2,000 Gas! } }

这个看似简单的特性,正是代理模式Gas优化的理论基础。

三、EIP-1967透明代理模式的存储布局

3.1 传统代理的问题

在EIP-1967之前,代理合约的存储布局面临严重的冲突问题:

// 问题:代理合约和逻辑合约共享存储空间 contract NaiveProxy { address public implementation; // Slot 0 address public admin; // Slot 1 // 问题:如果逻辑合约也在Slot 0声明了变量,就会覆盖! }

3.2 EIP-1967的解决方案

EIP-1967使用**非结构化存储(Unstructured Storage)**模式,将代理的关键数据放在一个"不可能被逻辑合约意外覆盖"的存储位置:

// EIP-1967 标准实现 contract EIP1967Proxy { // 使用 "不可能冲突" 的存储槽 // 计算方式: bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) bytes32 constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; function _implementation() internal view returns (address impl) { assembly { impl := sload(IMPLEMENTATION_SLOT) } } function _setImplementation(address newImpl) internal { assembly { sstore(IMPLEMENTATION_SLOT, newImpl) } } }

核心优化思路:通过将代理元数据存放在固定的、与逻辑合约存储空间完全隔离的槽位,避免了存储冲突,同时可以利用SLOAD的warm属性——因为在整个交易的生命周期内,这些槽位一旦被读取就变为warm状态

3.3 EIP-1967的Gas消耗分析

xychart-beta title "EIP-1967代理每次delegatecall的Gas分解" x-axis ["SLOAD实现地址", "SLOAD管理员", "delegatecall", "RETURNDATA处理"] y-axis "Gas消耗" 0 --> 3000 bar [2100, 2100, 700, 500]
操作Cold状态Warm状态(同交易第二次)
读取实现地址2,100 Gas100 Gas
读取管理员2,100 Gas100 Gas
delegatecall~700+ Gas~700+ Gas
每次代理调用最少Gas~5,600 Gas~1,600 Gas

关键优化:如果用户在同一个交易中连续调用同一个代理合约的多个函数,第二次及之后的调用将受益于warm storage,Gas消耗大幅降低。

四、EIP-2535钻石协议的存储布局

4.1 钻石协议的存储挑战

钻石协议(Diamond Proxy)允许一个合约同时代理多个逻辑合约(Facet),其存储管理更加复杂:

flowchart TD subgraph "Diamond Proxy" DP["Diamond合约"] SL["Storage Layout (存储布局)"] end subgraph "Facets (切面)" F1["Facet A: ERC20逻辑"] F2["Facet B: 治理逻辑"] F3["Facet C: 存款逻辑"] end DP --> F1 DP --> F2 DP --> F3 SL -.-> F1 SL -.-> F2 SL -.-> F3

问题是:Facet A、B、C各自有不同的状态变量,它们如何共享同一个Diamond的存储空间而不冲突?

4.2 Diamond Storage模式

EIP-2535提供了两种存储布局方案:

方案一:Diamond Storage(推荐)

// 每个Facet定义自己的存储结构 library LibDiamond { bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage"); struct DiamondStorage { mapping(bytes4 => address) facets; // 函数选择器 → Facet地址 address contractOwner; uint256 maxGasPerTx; } function diamondStorage() internal pure returns (DiamondStorage storage ds) { bytes32 position = DIAMOND_STORAGE_POSITION; assembly { ds.slot := position } } } // Facet中通过library访问共享存储 contract FacetA { function setMaxGas(uint256 _maxGas) external { LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); ds.maxGasPerTx = _maxGas; } }

方案二:AppStorage(单继承场景)

// 定义全局存储结构 contract AppStorage { // Slot 0+ 的结构体 struct AppStore { uint256 totalBalance; // Slot 0 mapping(address => uint) userBalances; // Slot 1 address admin; // Slot 2 bool paused; // Slot 2 (与admin打包) } } // 所有Facet继承AppStorage以确保存储布局一致 contract FacetA is AppStorage { function totalBalance() external view returns (uint256) { return appStore.totalBalance; // 一致的存储布局 } }

4.3 两种方案的Gas对比

维度Diamond StorageAppStorage
每次读取额外开销~2,100 Gas(首次SLOAD)0(直接访问)
存储冲突风险极低中等(需要继承管理)
Facet独立性
适用场景多团队/复杂合约单体式Diamond

4.4 Diamond的Gas优化技巧

对于频繁调用的存储变量,可以在Diamond Storage基础上做"warm slot"优化:

library LibOptimized { // 热数据专用槽 bytes32 constant HOT_STORAGE = keccak256("hot.storage"); struct HotStorage { uint256 cachedTotalSupply; // 高频读取 uint256 cachedFee; // 高频读取 address feeRecipient; // 中频 } function hotStorage() internal pure returns (HotStorage storage hs) { bytes32 position = HOT_STORAGE; assembly { hs.slot := position } } }

优化效果:将高频读写变量集中在一个Diamond Storage结构中,利用warm storage特性,同交易内的第二次访问Gas骤降95%。

五、存储布局重构:30%+ Gas优化的实战

5.1 一个真实的Optimization案例

假设我们有一个DeFi协议的Vault合约,原始存储布局如下:

// 优化前 - 存储布局 contract VaultV1 { address public owner; // Slot 0 (仅部署时写) address public feeRecipient; // Slot 1 (低频更新) uint256 public totalSupply; // Slot 2 (高频读) uint256 public totalDebt; // Slot 3 (高频读) uint256 public feeRate; // Slot 4 (低频) mapping(address => uint256) public balances; // Slot 5 (高频) mapping(address => uint256) public debts; // Slot 6 (高频) bool public paused; // Slot 7 (低频) uint256 public lastUpdate; // Slot 8 (中频) // 共9个槽,热数据分散 }

重新设计存储布局:

// 优化后 - 按冷热分组 + Diamond Storage library VaultStorage { bytes32 constant HOT_SLOT = keccak256("vault.hot"); bytes32 constant COLD_SLOT = keccak256("vault.cold"); struct HotData { uint256 totalSupply; // 高频读 uint256 totalDebt; // 高频读 mapping(address => uint256) balances; // 高频 mapping(address => uint256) debts; // 高频 } struct ColdData { address owner; // 低频 address feeRecipient; // 低频 uint256 feeRate; // 低频 bool paused; // 低频 uint256 lastUpdate; // 中频 } function hot() internal pure returns (HotData storage h) { bytes32 pos = HOT_SLOT; assembly { h.slot := pos } } function cold() internal pure returns (ColdData storage c) { bytes32 pos = COLD_SLOT; assembly { c.slot := pos } } }

5.2 Gas Benchmark数据

xychart-beta title "存储布局重构前后Gas对比 (越低越好)" x-axis ["存款(单用户)", "提款(单用户)", "批量查询(10用户)", "管理更新"] y-axis "Gas消耗" 0 --> 200000 bar [145320, 158760, 289400, 87650] bar [101200, 112340, 178900, 72300]
场景优化前优化后节省比例
单用户存款145,320 Gas101,200 Gas30.4%
单用户提款158,760 Gas112,340 Gas29.2%
批量查询(10用户)289,400 Gas178,900 Gas38.2%
管理员更新配置87,650 Gas72,300 Gas17.5%
平均节省~31.3%

优化手段汇总:

优化策略节省效果实现复杂度
冷热数据分离15-20%
高频变量集中到同一结构体8-12%
非结构化存储(Diamond Storage)5-8%
Yul内联汇编优化SLOAD3-5%
组合优化总计~30%+

5.3 更激进的优化:Storage Inlining

对于某些特定场景,可以将多个高频变量编码进同一个32字节槽:

// 将4个高频变量压缩到1个槽中 library StoragePacking { bytes32 constant PACKED_SLOT = keccak256("packed"); function getPacked() internal view returns ( uint64 totalSupply, uint64 totalDebt, uint64 reserve, uint64 lastBlock ) { bytes32 pos = PACKED_SLOT; assembly { let data := sload(pos) totalSupply := and(data, 0xFFFFFFFFFFFFFFFF) totalDebt := and(shr(64, data), 0xFFFFFFFFFFFFFFFF) reserve := and(shr(128, data), 0xFFFFFFFFFFFFFFFF) lastBlock := shr(192, data) } } function setPacked( uint64 totalSupply, uint64 totalDebt, uint64 reserve, uint64 lastBlock ) internal { bytes32 pos = PACKED_SLOT; assembly { let data := or( or(totalSupply, shl(64, totalDebt)), or(shl(128, reserve), shl(192, lastBlock)) ) sstore(pos, data) } } }

这种方案将4次SLOAD/SSTORE减少为1次,Gas节省高达75%——但代价是变量范围受限(uint64 = 约18e18,对大多数DeFi场景够用)。

六、存储布局优化的最佳实践框架

6.1 优化决策树

flowchart TD A["开始存储布局设计"] --> B{"合约是否需要升级?"} B -->|"是"| C{"切面数量?"} C -->|"1-2个Facet"| D["EIP-1967 + AppStorage"] C -->|"3+个Facet"| E["EIP-2535 + Diamond Storage"] B -->|"否"| F{"变量数量?"} F -->|"≤8个"| G["手动重排 + 紧密打包"] F -->|">8个"| H["冷热分离 + 非结构化存储"] D --> I["Gas Benchmark验证"] E --> I G --> I H --> I I --> J{"优化≥30%?"} J -->|"是"| K["✓ 通过"] J -->|"否"| L["考虑Storage Inlining"] L --> K

6.2 各场景的推荐方案速查

场景推荐方案预期Gas节省审计成本
简单Token合约紧密打包5-10%
DeFi Vault冷热分离 + Diamond Storage25-35%
多Facet系统EIP-253520-30%
极高频调用Storage Inlining40-75%
可升级治理合约EIP-196715-20%

七、结尾

Hash已经在他的"双层别墅"里安顿好了——上层加热区晒着灯,下层躲避区藏着睡。偶尔他还会在两个区域之间来回爬几趟,仿佛在确认动线是否合理。

这让我想到,我们的存储布局优化也是如此——数据在EVM存储中的"动线"一旦设计好,后续所有交易都会沿着这条优化过的路径执行,每笔交易都在省钱。

今天的关键要点:

  1. EVM存储访问有cold/warm之分,同交易内warm访问节省95% Gas
  2. EIP-1967通过非结构化存储避免代理元数据与逻辑合约冲突
  3. EIP-2535的Diamond Storage解决了多Facet间的存储共享问题
  4. 冷热数据分离是最简单也最有效的优化手段,单此一项可省20%+
  5. Storage Inlining将4个变量压缩到1个槽,极限场景可省75%+
  6. 组合使用多种策略可轻松实现30%+的总体Gas降低

下一篇文章,我们将换一个角度——聊聊如何用大语言模型来检测Solidity智能合约的重入漏洞,Hash和我都很好奇AI究竟能不能帮我们写出更安全的合约!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询