关注我,带你转型Web3开发,分享我从2018年入行区块链以来的开发经验(DeFi/GameFi/NFT/DAO/OTC/钱包/Trading Bot/MEME Launch)
内存管理在智能合约开发中可是个关键角色,直接决定着gas成本和代码效率。
这篇文章,我想带你走进EVM内存的世界,从基本特性聊到内存布局,再分享一些实用的优化小技巧,助你在开发路上更进一步。
不管你是备战技术面试,还是刚入门Solidity的新手,希望这些内容都能为你带来收获。
在深入内存布局之前,我们先来看看EVM内存的几个核心特性:
这些特性决定了内存在智能合约中的使用场景,也为我们后续的优化提供了基础。
当EVM执行一个智能合约方法时,会为它开辟一块全新的内存空间。这块空间从地址0x00开始,理论上可以扩展到2^256-1字节。为了高效管理,EVM将内存划分为几个功能区域:
| 内存地址范围 | 名称 | 用途 |
|---|---|---|
0x00 - 0x3F |
暂存空间(Scratch Space) | 用于临时存储数据,如哈希计算的中间结果。 |
0x40 - 0x5F |
空闲内存指针 | 指向下一个可用的内存位置。 |
0x60 - 0x7F |
零槽位(Zero Slot) | 通常用于初始化新内存,保持全0状态。 |
0x80 - ... |
用户区 | 存储函数参数、局部变量和动态数据。 |
0x00 - 0x3F)暂存空间就像EVM的“便签纸”,适合存放临时数据。比如,在计算存储槽位置时,我们可以用它来暂存数据:
contract MemoryExample {
mapping(address => uint256) public balances; // 槽0
function setBalance(address user, uint256 amount) public {
assembly {
mstore(0, user) // 存储用户地址
mstore(32, 0) // 存储槽位号
let balanceSlot := keccak256(0, 64) // 计算哈希
sstore(balanceSlot, amount) // 存储余额
}
}
}
这段代码展示了如何利用暂存空间计算mapping的存储位置,简单又高效。
0x40 - 0x5F)空闲内存指针是一个“导航标记”,记录着下一个可用的内存地址。分配内存时,我们会读取这个指针,使用它指向的位置存储数据,然后更新指针:
assembly {
let ptr := mload(0x40) // 获取当前指针
mstore(ptr, 42) // 存储数据
mstore(0x40, add(ptr, 0x20)) // 更新指针
}
这种机制确保了内存分配的连续性和有序性。
0x60 - 0x7F)零槽位通常保持全0状态,常用于初始化新分配的内存。例如,我们可以利用它来快速清零一段内存:
assembly {
let ptr := mload(0x40) // 获取当前指针
mstore(ptr, mload(0x60)) // 用零槽位的0初始化
mstore(0x40, add(ptr, 0x20)) // 更新指针
}
这在需要确保内存初始状态时非常实用。 零槽位并非强制全0,而是Solidity编译器常用它来提供零值初始化,增强可读性。
0x80 - ...)用户区是内存的“主战场”,用来存放函数参数、局部变量和动态数据。比如,创建一个动态数组:
uint256[] memory array = new uint256[](3);
array[0] = 1;
array[1] = 2;
array[2] = 3;
在内存中,array会从0x80开始,前32字节存储长度(3),后面连续存储元素。
EVM的内存分配是动态的,遵循“先预订后使用”的原则。过程如下:
来看一个例子,直观感受内存分配:
contract MemoryDemo {
function checkMemory() public pure returns (uint256, uint256) {
uint256 start;
uint256 end;
assembly {
start := mload(0x40) // 获取初始指针
mstore(start, 42) // 存储数据
mstore(0x40, add(start, 0x20)) // 更新指针
end := mload(0x40) // 获取新指针
}
return (start, end); // 返回分配前后的指针
}
}
运行结果通常是start = 0x80,end = 0xA0,说明分配了32字节。
接下来,我们看看不同数据类型在内存里是怎么摆放的。
不同数据类型在内存中的存储方式各有特点:
uint256或address,固定占32字节。例如,一个结构体的内存布局:
struct User {
uint256 id;
address addr;
}
User memory user = User(1, msg.sender);
在内存中,id占0x80-0x9F,addr占0xA0-0xBF。
EVM通过操作码管理内存,主要包括:
mload(p):从位置p读取32字节。mstore(p, v):在位置p写入32字节值v。mstore8(p, v):在位置p写入1字节值v。虽然EVM中没有mload8,但可以使用byte(0, mload(p))来代替。
3 * words + words^2 / 512计算(words为32字节的字数)。| 操作 | 内存使用(字节) | 字数 | 扩展gas |
|---|---|---|---|
mstore(0x80, 0) |
128 | 4 | 0(未扩展) |
mstore(0x100, 0) |
256 | 8 | ≈36 |
mstore(0x200, 0) |
512 | 16 | ≈96 |
EVM默认初始内存为0字节,首次扩展到128字节会有少量gas成本(约3 gas),后续在此范围内操作才无需额外扩展成本。
合理控制内存扩展是优化的关键。
掌握了内存机制后,怎么用它省gas呢?我总结了几个实用建议供你参考:
// 优化前:循环中分配
function inefficient(uint256 n) public pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < n; i++) {
uint256[] memory temp = new uint256[](1);
temp[0] = i;
sum += temp[0];
}
return sum;
}
// 优化后:预分配
function efficient(uint256 n) public pure returns (uint256) {
uint256[] memory temp = new uint256[](n);
uint256 sum = 0;
for (uint256 i = 0; i < n; i++) {
temp[i] = i;
sum += temp[i];
}
return sum;
}
function useCalldata(uint256[] calldata data) public pure returns (uint256) {
return data.length; // 直接读取,省gas
}
memory。calldata。EVM内存是智能合约开发中的核心环节,理解其布局和分配机制,能让我们在gas优化和安全性上更具优势。通过合理管理内存、选择正确的数据位置,我们可以显著提升合约的性能。
希望这篇干货对你有帮助,快去试试优化你的代码吧!有问题或心得,欢迎留言告诉我~
1111
我的产品体系: 《Web3开发转型陪跑计划》 《数字货币投资工具使用教学》 《AI编程教学》 本人技术栈: Solidity/Golang/Rust/NodeJS/Python/AI编程 项目合作,欢迎你来链接我! 都看到这里了,不关注一下嘛👇
基于全文检索与主题相似度
存储槽是什么? EVM(以太坊虚拟机)的存储本质上是一个巨大的键值映射:mapping(uint256 = uint256)。具体来说: - 键(key):一个256位(32字节)的存储槽编号,从0开始。 - 值(value):同样是256位的数据。 这种设计让每个合约理论上能访问高达 2^256
说在前面 在Solidity开发中,错误处理就像是合约的守护者,保护着你的代码免受意外和攻击,所以用好require、revert和assert这三个关键字特别重要。 这篇文章,我将和你分享它们的区别和用法,帮你在开发中少踩坑,写出更安全、更可靠的合约。 核心区别对比 先来个直观的对比(手机屏下可左
关注我,带你转型Web3开发,分享我从2018年入行区块链以来的开发经验(DeFi/GameFi/NFT/DAO/OTC/钱包/Trading Bot/MEME Launch),免费领取我私下整理的“智能合约学习”资料一份,帮你少走弯路! 说在前面 在Solidity开发中,搞懂数据存储位置就像掌握