跳至内容
0%

Yul storage 是如何工作的?

发布于 2023年8月1日2 分钟阅读

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. 

注意 0x0000000000000000000000000000020x000000000000000000000000000001 完美地放入了同一个插槽。这是因为它们各占 16 字节(半个插槽)。

Instruction
Explanation

sload(p)

Loads the variable in slot p from storage.

sstore(p,v)

Assigns storage slot p value v.

v.slot

Returns the storage slot of variable v.

v.offset

Returns the index in bytes of where variable v begins in a storage slot. Variables are packed from right to left.

再看一个例子!

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。

以下是代码,让我们来看看!

具体过程如下:

  1. 我们首先获取第一个键(0)的哈希值。
  2. 然后将其与第二个键(1)一起再次做哈希运算。
  3. 最后,从存储中加载该插槽,得到我们要的值。

恭喜你,完成了关于 Yul 存储的这部分内容!

延伸阅读:Yul MemoryYul Contract CallsYul Packed Storage Variables

Background gradient

构建区块链应用

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