title | tags | ||
---|---|---|---|
04. 内存布局 |
|
《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。
所有代码和教程开源在github: github.com/AmazingAng/WTF-Solidity-Internals
这一讲,我们将介绍Solidity中的变量是如何在内存中保存的。
EVM使用内存来支持交易执行期间的数据存储和读取。EVM的内存是一个线性寻址存储器,你可以把它理解为一个动态字节数组,可以根据需要动态扩展。它支持以8或256 bit写入(MSTORE8
/MSTORE
),但只支持以256 bit读取(MLOAD
)。
需要注意的是,EVM的内存是“易失性”的:交易开始时,所有内存位置的值均为0;交易执行期间,值被更新;交易结束时,内存中的所有数据都会被清除,不会被持久化。如果需要永久保存数据,就需要使用EVM的存储。
相比于存储,在内存上写入和读取数据要便宜的多,因此我们不需要像使用存储那样节省,可以豪横一点。这主要体现在我们不会将多个变量压缩保存在同一个内存槽中。
在内存中读写数据便宜的原因有两个:一是数据分别放在各自的内存槽中,而不是尽量压缩在一个槽中。如果是后者,那EVM每次使用数据时就会多一步操作,从槽中截取出真正自己要使用的数据,也就有了更多的gas消耗。二是内存空间是临时的,使用完就会释放,并不像Storage存储那样永久占用链上空间。因此,数据各自占用一个槽位,不仅带来了计算时的便利,又不会真正占用存储空间,gas费自然就低。
Solidity保留了前4个内存插槽,每个插槽32字节,用于特殊目的:
0x00
-0x3f
(64字节):用于哈希方法的临时空间,比如读取mapping里的数据时,要用到key的hash值,key的hash结果就暂存在这里。0x40
-0x5f
(32字节):当前分配的内存大小,又称空闲内存指针(Free Memory Pointer),指向当前空闲的内存位置。Solidity 总会把新对象保存在空闲内存指针的位置,并更新它的值到下一个空闲位置。0x60
-0x7f
(32字节): 32字节的0
值插槽,用于需要零值的地方,比如动态长度数据的初始长度值。
每个值变量会占用一个内存插槽。
function testUint() public pure returns (uint){
uint a = 3;
return a;
}
可以看到,上面testUint()
函数中的a
变量的值被存在内存槽0x80
中:
对于内存布局,字符串/字节数组不论长短规则都是一样的。字符串/字节数组长度保存在单独的一个内存槽中,接着是内容,一个内存槽不够的话会顺序保存到后面的内存槽中。
function testShortString() public pure returns (string memory){
string memory x = "WTF";
return x;
}
上面的字符串变量x
的长度为3
,保存在内存槽0x80
;内容为WTF
,保存在内存槽0xa0
function testLongBytes() public pure returns (bytes memory){
bytes memory x = hex"365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3";
return x;
}
上面的字节数组变量x
的长度为44
(0x2c
),保存在内存槽0x80
;内容保存在内存槽0xa0
-0xc0
中。
静态数组的每一个元素会占用一个单独的内存槽。
function testStaticArray() public pure returns (uint8[3] memory){
uint8[3] memory b = [1,2,3];
return b;
}
可以看到,上面testStaticArray()
函数中的b
变量的元素被顺序的存在内存槽0xe0
-0x120
中,虽然每个元素为uint8
类型,但仍然占用一个单独的内存槽:
动态数组的长度以及每一个元素会占用一个单独的内存槽。
function testDynamicArray() public pure returns (uint[] memory){
uint[] memory x = new uint[](3);
x[0] = 1;
x[2] = 4;
return x;
}
可以看到,上面testDynamicArray()
函数中的x
变量的长度和元素被顺序的存在内存槽0x80
-0xe0
中:
当数组里存的是可变长度的数据或其他数组时,对应元素的内存槽存的是变长数据在内存中的指针也就是起始地址。
function testMultiDimensionalArray(string memory info, uint16 length) public pure returns (string[] memory){
string[] memory x = new string[](length);
x[0] = info;
x[1] = "HELLO";
x[length - 1] = "WTF";
return x;
}
可以看到, testMultiDimensionalArray("123456789", 5)
函数中,参数info
的长度和数据存在内存槽0x80
- 0xa0
中,参数length
因为被编译器优化没有被存在接下来的内存槽中,内存槽0xc0
存的是x
变量的长度,内存槽0xe0
- 0x160
存的是对应元素的string数据在内存中的起始地址, x[0]
中存的是0x80
,x[1]
中存的是0x180
也就是string("HELLO")
在内存中的起始位置,x[2]
、x[3]
都存的是0x60
,内存槽0x60
中是恒定的32字节0,在这里就表示长度为0的string,x[4]
中存的是0x1c0
也就是string("WTF")
在内存中的起始位置,0x200
后的数据都是abi.encode
过的返回数据。
这一讲,我们介绍了Solidity合约的内存布局。内存布局与存储布局大致类似,但是由于内存操作消耗的gas很低,我们不需要像存储布局那样将多个变量保存在同一个内存槽中。