存储中状态变量的布局
合约的状态变量以紧凑的方式存储在存储中,以便多个值有时使用相同的存储槽。除了动态大小的数组和映射(见下文)之外,数据以连续的方式存储在以第一个状态变量开头的项接一项地存储,该状态变量存储在插槽中。对于每个变量,大小(以字节为单位)根据其类型确定。如果可能,需要少于 32 个字节的多个连续项目将打包到单个存储槽中,具体取决于以下规则:0
- 存储槽中的第一个项目以较低的顺序对齐存储。
- 值类型仅使用存储它们所需的字节数。
- 如果值类型不适合存储槽的剩余部分,则该值类型将存储在下一个存储槽中。
- 结构和阵列数据总是启动一个新的槽,并且根据这些规则,它们的物品被紧密地包装起来。
- 结构或数组数据后面的项始终启动新的存储槽。
对于使用继承的协定,状态变量的顺序由从最基本守合同开始的协定的 C3 线性化顺序确定。如果上述规则允许,来自不同合约的状态变量确实共享相同的存储槽。
结构和数组的元素彼此相邻存储,就像它们作为单个值给出一样。
警告
当使用小于 32 字节的元素时,合约的气体使用量可能会更高。这是因为 EVM 一次在 32 个字节上运行。因此,如果元素小于此值,则 EVM 必须使用更多操作,以便将元素的大小从 32 个字节减少到所需的大小。
如果要处理存储值,则使用减小大小的类型可能会有所帮助,因为编译器会将多个元素打包到一个存储槽中,从而将多个读取或写入合并到单个操作中。但是,如果不同时读取或写入槽中的所有值,则可能会产生相反的效果:当将一个值写入多值存储槽时,必须首先读取该存储槽,然后将其与新值组合,以便不会破坏同一槽中的其他数据。
在处理函数参数或内存值时,没有固有的好处,因为编译器不打包这些值。
最后,为了允许 EVM 对此进行优化,请确保尝试对存储变量和成员进行排序,以便它们可以紧密地打包。例如,按 的顺序声明存储变量,而不是 ,因为前者仅占用两个存储槽,而后者将占用三个。structuint128, uint128, uint256uint128, uint256, uint128
注意
存储中状态变量的布局被认为是 Solidity 外部接口的一部分,因为存储指针可以传递到库。这意味着对本节中概述的规则的任何更改都被视为语言的重大更改,并且由于其关键性,在执行之前应非常仔细地考虑。如果发生这样的重大更改,我们希望发布一种兼容模式,在该模式下,编译器将生成支持旧布局的字节码。
映射和动态数组
由于映射和动态大小的数组类型不可预测,因此不能存储在它们之前和之后的状态变量“之间”。相反,根据上述规则,它们被认为仅占用32个字节,并且它们包含的元素从使用Keccak-256哈希计算的不同存储槽开始存储。
假设映射或阵列的存储位置在应用存储布局规则后最终成为插槽。对于动态数组,此槽存储数组中的元素数(字节数组和字符串是例外,请参阅下文)。对于映射,该槽保持为空,但仍需要确保即使有两个映射彼此相邻,其内容最终也会位于不同的存储位置。p
数组数据从 开始,其布局方式与静态大小的数组数据相同:一个元素接一个,如果元素的长度不超过 16 个字节,则可能共享存储槽。动态数组的动态数组以递归方式应用此规则。元素的位置,其中类型为 ,计算如下(再次假设其本身存储在插槽中):插槽是,并且可以使用从插槽数据中获取元素。keccak256(p)
x[i][j]
x
uint24[][]
x
p
keccak256(keccak256(p) + i) + floor(j / floor(256 / 24))
v
(v >> ((j % floor(256 / 24)) * 24)) & type(uint24).max
与映射键对应的值位于串联位置,并且是根据其类型应用于键的函数:k
keccak256(h(k) . p)
.
h
-
对于值类型,将值填充到 32 个字节,其方式与将值存储在内存中时相同。
h
-
对于字符串和字节数组,只是未填充的数据。
h(k)
如果映射值是非值类型,则计算的槽将标记数据的开始。例如,如果值为结构类型,则必须添加与结构成员对应的偏移量才能到达该成员。
例如,请考虑以下合同:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract C { struct S { uint16 a; uint16 b; uint256 c; } uint x; mapping(uint => mapping(uint => S)) data; }
我们计算 的存储位置。映射本身的位置是(前面有 32 个字节的变量)。这意味着 存储在 。的类型再次是映射,并且 的数据从插槽 开始。结构内成员的槽偏移量是因为 并且被打包在单个槽中。这意味着 的插槽是 。该值的类型是 ,因此它使用单个插槽。data[4][9].c
1
x
data[4]
keccak256(uint256(4) . uint256(1))
data[4]
data[4][9]
keccak256(uint256(9) . keccak256(uint256(4) . uint256(1)))
c
S
1
a
b
data[4][9].c
keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1
uint256
bytes
和string
bytes
并且编码相同。通常,编码类似于 ,因为数组本身有一个插槽,而数据区域是使用该插槽位置的哈希值计算的。但是,对于短值(短于 32 个字节),数组元素将与长度一起存储在同一个插槽中。string
bytes1[]
keccak256
特别是:如果数据最多是字节长,则元素存储在高阶字节(左对齐)中,最低阶字节存储值。对于存储长度为或更多字节的数据的字节数组,主槽将存储,并且数据将照常存储在 中。这意味着您可以通过检查是否设置了最低位来区分短数组和长数组:短位(未设置)和长位(设置)。31
length * 2
32
p
length * 2 + 1
keccak256(p)
注意
目前不支持处理无效编码的插槽,但将来可能会添加。如果通过 IR 进行编译,则读取无效编码的插槽会导致错误。Panic(0x22)
JSON 输出
可以通过标准 JSON 接口请求合约的存储布局。输出是一个 JSON 对象,其中包含两个键和 。该对象是一个数组,其中每个元素具有以下形式:storage
types
storage
{ "astId": 2, "contract": "fileA:A", "label": "x", "offset": 0, "slot": "0", "type": "t_uint256" }
上面的示例是源单元的存储布局,并且contract A { uint x; }
fileA
-
astId
是状态变量声明的 AST 节点的 ID -
contract
是合约的名称,包括其路径作为前缀 -
label
是状态变量的名称 -
offset
是存储槽内根据编码的偏移量(以字节为单位) -
slot
是状态变量驻留或启动的存储槽。此数字可能非常大,因此其 JSON 值表示为字符串。 -
type
是用作变量类型信息键的标识符(如下所述)
给定的 ,在本例中表示 中的一个元素,其形式为:type
t_uint256
types
{ "encoding": "inplace", "label": "uint256", "numberOfBytes": "32", }
哪里
-
encoding
数据在存储中的编码方式,其中可能的值为: -
label
是规范类型名称。 -
numberOfBytes
是已用字节数(作为十进制字符串)。请注意,如果这意味着使用了多个插槽。numberOfBytes > 32
除了上述四种类型之外,某些类型还有额外的信息。映射包含其和类型(再次引用此类型映射中的条目),数组具有其类型,结构以与顶级相同的格式列出它们(见上文)。key
value
base
members
storage
注意
合约存储布局的 JSON 输出格式仍被视为实验性格式,并且可能会在 Solidity 的非中断版本中发生变化。
下面的示例演示协定及其存储布局,其中包含值和引用类型、编码打包的类型以及嵌套类型。
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract A { struct S { uint128 a; uint128 b; uint[2] staticArray; uint[] dynArray; } uint x; uint y; S s; address addr; mapping (uint => mapping (address => bool)) map; uint[] array; string s1; bytes b1; }
{ "storage": [ { "astId": 15, "contract": "fileA:A", "label": "x", "offset": 0, "slot": "0", "type": "t_uint256" }, { "astId": 17, "contract": "fileA:A", "label": "y", "offset": 0, "slot": "1", "type": "t_uint256" }, { "astId": 20, "contract": "fileA:A", "label": "s", "offset": 0, "slot": "2", "type": "t_struct(S)13_storage" }, { "astId": 22, "contract": "fileA:A", "label": "addr", "offset": 0, "slot": "6", "type": "t_address" }, { "astId": 28, "contract": "fileA:A", "label": "map", "offset": 0, "slot": "7", "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))" }, { "astId": 31, "contract": "fileA:A", "label": "array", "offset": 0, "slot": "8", "type": "t_array(t_uint256)dyn_storage" }, { "astId": 33, "contract": "fileA:A", "label": "s1", "offset": 0, "slot": "9", "type": "t_string_storage" }, { "astId": 35, "contract": "fileA:A", "label": "b1", "offset": 0, "slot": "10", "type": "t_bytes_storage" } ], "types": { "t_address": { "encoding": "inplace", "label": "address", "numberOfBytes": "20" }, "t_array(t_uint256)2_storage": { "base": "t_uint256", "encoding": "inplace", "label": "uint256[2]", "numberOfBytes": "64" }, "t_array(t_uint256)dyn_storage": { "base": "t_uint256", "encoding": "dynamic_array", "label": "uint256[]", "numberOfBytes": "32" }, "t_bool": { "encoding": "inplace", "label": "bool", "numberOfBytes": "1" }, "t_bytes_storage": { "encoding": "bytes", "label": "bytes", "numberOfBytes": "32" }, "t_mapping(t_address,t_bool)": { "encoding": "mapping", "key": "t_address", "label": "mapping(address => bool)", "numberOfBytes": "32", "value": "t_bool" }, "t_mapping(t_uint256,t_mapping(t_address,t_bool))": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => mapping(address => bool))", "numberOfBytes": "32", "value": "t_mapping(t_address,t_bool)" }, "t_string_storage": { "encoding": "bytes", "label": "string", "numberOfBytes": "32" }, "t_struct(S)13_storage": { "encoding": "inplace", "label": "struct A.S", "members": [ { "astId": 3, "contract": "fileA:A", "label": "a", "offset": 0, "slot": "0", "type": "t_uint128" }, { "astId": 5, "contract": "fileA:A", "label": "b", "offset": 16, "slot": "0", "type": "t_uint128" }, { "astId": 9, "contract": "fileA:A", "label": "staticArray", "offset": 0, "slot": "1", "type": "t_array(t_uint256)2_storage" }, { "astId": 12, "contract": "fileA:A", "label": "dynArray", "offset": 0, "slot": "3", "type": "t_array(t_uint256)dyn_storage" } ], "numberOfBytes": "128" }, "t_uint128": { "encoding": "inplace", "label": "uint128", "numberOfBytes": "16" }, "t_uint256": { "encoding": "inplace", "label": "uint256", "numberOfBytes": "32" } } }