Yul storage 是如何工作的?
Yul 是一种中间编程语言,可以用来在智能合约内编写一种形式的汇编语言。理解 Yul 可以提升你的智能合约水平,帮助用户节省 gas 成本。要开始用 Yul 编写智能合约,首先需要理解存储是如何工作的。
Yul 存储是如何工作的?
在深入了解 Yul 的工作原理之前,我们需要先充分理解智能合约中存储的工作方式。存储由一系列插槽(slot)组成,一个智能合约有 2²⁵⁶ 个插槽。
在声明变量时,我们从插槽 0 开始,依次递增。每个插槽长度为 256 位(32 字节),这也是 uint256 和 bytes32 命名的由来。所有变量都会被转换为十六进制。
如果使用的变量比如 uint128 更小,则不会占满整个插槽,而是在左侧用 0 填充。让我们通过一个例子来更好地理解这一点。
var1:由于 uint256 变量等于 32 字节,var1 占满了插槽 0 的全部空间。
以下是插槽 0 中存储的内容:
0x0000000000000000000000000000000000000000000000000000000000000100
var2:地址(address)稍微复杂一些。由于地址只占用 20 字节的存储空间,因此会在左侧用 0 填充。
以下是插槽 1 中存储的内容:
0x0000000000000000000000009acc1d6aa9b846083e8a497a661853aae07f0f00.
var3:这个看起来很简单,插槽 2 被这个 bytes32 变量完全占用。
var4 和 var5:还记得我提到过 uint128 会被 0 填充吗?如果我们对变量进行排序,使它们的存储总和小于 32 字节,就可以把它们打包进同一个插槽!这叫做变量打包(packing variables),可以帮你节省 gas。
让我们看看插槽 3 中存储了什么:
0x0000000000000000000000000000000200000000000000000000000000000001.
注意 0x000000000000000000000000000002 和 0x000000000000000000000000000001 完美地放入了同一个插槽。这是因为它们各占 16 字节(半个插槽)。
再看一个例子!
x = 3。这很合理,因为我们知道 var5 被打包进了插槽 3。
y = 16。这也说得通,因为我们知道 var4 占用了插槽 3 的一半。由于变量是从右到左打包的,所以 var5 的起始索引是字节 16。
z = 1。sstore() 将插槽 0 的值设为 1。然后,我们通过 sload() 将插槽 0 的值赋给 z。
在继续之前,你应该把这个函数添加到你的 remix 文件中。它可以帮助你查看每个存储插槽中存储的内容。
现在让我们来看一些更复杂的数据结构!
在处理静态数组时,EVM 知道需要为我们的数据分配多少个插槽。以这个数组为例,我们把 2 个元素打包进一个插槽。
所以如果你调用 getValInHex\(4\),它会返回 0x0000000000000000000000000000000100000000000000000000000000000000。
正如预期的那样,从右往左读,我们看到的是值 0 和值 1。
插槽 5 包含 0x0000000000000000000000000000000300000000000000000000000000000002。
接下来我们来看动态数组。
试着调用 getValInHex\(6\),你会看到它返回 0x00。由于 EVM 不知道需要分配多少个存储插槽,所以我们无法把数组存储在这里。
取而代之的是,当前存储插槽(插槽 6)的 keccak256 哈希值被用作数组的起始索引。从这里开始,我们只需加上所需元素的索引即可获取该值。
这是一个演示如何找到动态数组中某个元素的代码示例:
这里我们先获取数组的插槽,然后执行 add() 操作并配合 sload(),从而得到我们想要的数组元素的值。
你可能会问,是什么阻止了它与另一个变量的插槽发生冲突?
这种情况完全可能发生,但由于 2²⁵⁶ 是一个非常大的数字,发生的概率极低。
映射(mapping)的行为与动态数组类似,不同之处在于我们会把插槽和键(key)一起做哈希运算。
在这个演示中,我设置了映射值 var8[1] = 2。现在让我们看一个示例,了解如何获取映射中某个键对应的值。
正如你所看到的,这段代码与我们查找动态数组元素时的代码非常相似。主要区别在于,我们把键和插槽一起做哈希运算。
关于存储部分的最后一个内容是学习嵌套映射(nested mappings)。在继续阅读之前,我建议你根据目前学到的知识,自己尝试实现如何读取嵌套映射的值。
在这个例子中,我设置了映射值 var9[0][1] = 2。
以下是代码,让我们来看看!
具体过程如下:
- 我们首先获取第一个键(0)的哈希值。
- 然后将其与第二个键(1)一起再次做哈希运算。
- 最后,从存储中加载该插槽,得到我们要的值。
恭喜你,完成了关于 Yul 存储的这部分内容!
延伸阅读:Yul Memory、Yul Contract Calls 和 Yul Packed Storage Variables。
相关概览

构建区块链应用
Alchemy 将最强大的 Web3 开发者产品和工具与资源、社区及专业支持结合在一起。


