跟我学 Solidity : 变量的存储

Solidity 数据存储如何工作?

欢迎阅读“跟我学习 Solidity ”系列中的另一篇文章。正如我在上一篇文章中保证过,我们将看到Solidity中数据存储的工作方式。

以太坊虚拟机(EVM)

在讨论Solidity中的数据存储之前,我想介绍一些有关以太坊虚拟机的知识,以使事情更清楚。

EVM的内部工作原理:

flow chart showing the Ethereum EVM environment

EVM上下文(来源:fullstacks.org)

当我们安装以太坊客户端时,它附带了EVM,EVM是专门创建来运行智能合约的轻量级操作系统。 EVM是基于堆栈计算机的模型的体系结构,这意味着该指令集被设计为用于堆栈而不是寄存器。EVM 操作码的列表在黄皮书中进行了描述,可以在“ 以太坊虚拟机(EVM)操作码和指令参考”中找到。

代码执行步骤如下:当交易触发智能合约代码执行时,将实例化EVM,并在EVM的ROM中加载要调用的合约的代码。程序计数器设置为零,从合约帐户的存储器中加载存储器,存储器全部设置为零,并且所有块和环境变量都已设置。然后代码被执行。

数据存储位置

现在让我们回到memory关键字,如Solidity 文档中所述。从版本0.5.0开始,所有复杂类型都必须给出一个明确的数据存储位置,并且有三个数据位置:memory(内存)storage(存储)calldata(调用数据)

注意:唯一可以省略数据位置的地方是状态变量,因为状态变量始终存储在帐户的存储空间中。

  1. storage(存储)
  • storage中的数据被永久存储。其以键值形式存储。
  • storage中的数据写在区块链中(因此它们会更改状态),这就是为什么使用存储非常昂贵的原因。
  • 占用256位插槽的 gas成本为20,000。
  • 修改storage的值将花费5,000 gas 。
  • 清理存储插槽时(即将非零字节设置为零),将退还一定量的 gas 。
  • 将数据保存在256位大小(32字节为一个字)的存储字段中。即使未完全占用每个插槽,也会产生成本。
  1. memory(内存)
  • memory是一个字节数组,其插槽大小为256位(32个字节)。数据仅在函数执行期间存储,执行完之后,将其删除。它们不会保存到区块链中。
  • 读或写一个字(256位)需要3 gas 。
  • 为了避免给矿工带来太多工作,在进行22次读写操作后,之后的读写成本开始上升。
  1. calldata调用数据
  • calldata是一个不可修改的,非持久性的区域,用于存储函数参数,并且其行为基本上类似于memory
  • 外部函数的参数需要calldata,但也可用于其他变量。
  • 它避免了复制,并确保了数据不能被修改。
  • 带有calldata数据位置的数组和结构体也可以从函数中返回,但是不可以为这种类型赋值。

数据位置和赋值行为

如果你不想发生意外的行为,那么了解数据位置赋值的工作方式非常重要。

在赋值之间应用以下规则:

  • storagememory之间(或来自calldata)的赋值总是创建一个独立的副本。
  • memorymemory的赋值仅创建引用。这意味着对一个内存变量的更改在引用相同数据的所有其他内存变量中同样有效。
  • storage到本地storage变量的赋值也仅赋值一个引用。
  • 所有其他到storage的赋值总是被复制。这种情况的示例是赋值给状态变量或storage结构体类型的局部变量成员,即使局部变量本身只是一个引用。

让我们使用Remix debugger进行更详细的研究

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.7.0;

contract DataLocationTest {

    uint[] stateVar = [1,4,5];

    function foo() public{
        // case 1 : 从存储中加载到内存
        uint[] memory y = stateVar; // 复制 stateVar 到 y

        // case 2 : from memory to storage
        y[0] = 12;
        y[1] = 20;
        y[2] = 24;

        stateVar = y; // copy the content of y to stateVar

        // case 3 : from storage to storage
        uint[] storage z = stateVar; // z is a pointer to stateVar

        z[0] = 38;
        z[1] = 89;
        z[2] = 72;
    }

}

创建一个新文件,复制上面的代码,然后部署合约。 现在尝试调用函数foo。你将在控制台中看到交易的详细信息,并在其旁边有一个调试按钮。点击它。

Solidity Remix 调试

你现在应该看到如下所示的调试器区域:

Solidity debugger area screen with the arrow that allows you to step over the code highlighted in red

要单步执行(Step over)代码,请单击我以红色框中的箭头。

storagememory的赋值

你应该首先注意到,正如我们在EVM部分中提到的, 状态(Solidity State)部分加载了storage 的stateVar的内容,当然没有局部变量。

当你单步执行时,应该看到变量y出现在局部变量(Solidity locals)部分中。继续执行(step over),你会注意到,为了分配必要的内存空间,需要使用很多字节码

并从storage中加载每个字,然后将其复制到memory中。这意味着要支付更多的 gas ,因此从storagememory的赋值非常昂贵。

memorystorage的赋值

让我们研究第二种情况:从memorystorage的赋值。

当你修改完存储在memory中的副本并且想要将更改保存回storage时,可以使用它。它同样消耗大量的 gas 。如果我们用调试器步骤详细信息中的剩余 gas (remaining gas)来计算 gas 差值,则为17,083 gas 。该操作使用了四个SSTORE操作码:第一个用于存储数组大小(保持不变,消耗800个 gas ),另外三个用于更新数组的值(每个消耗了5,000个 gas )。

storagestorage的赋值

现在,让我们看一下情况三:从storagestorage的赋值。这次将创建一个新的局部变量,并包含与stateVar相同的内容。如果我们查看代码的执行过程,会注意到Solidity所做的,将包含数组长度的存储的第一个插槽地址入栈。根据文档,对于动态数组,插槽位置包含数组长度,用于计算包含数组数据的插槽位置。

来比较两者的 gas 成本:

第一种是将数据复制到memory,然后更新并复制回 storage, 使用21,629 gas ,

第二种是直接创建引用并更新状态, 使用 5,085 gas 。

那么很明显第二种方法是便宜得多。

但是,如果我们像这样直接更新状态变量呢,像这样:

stateVar[0] = 12;

也有可能但是,如果你要处理映射和嵌套数据类型(我们将在后面看到),则使用storage指针可以让代码更具可读性。

为了使本文简短,而不会给你太多信息,我决定在下一篇文章继续介绍复杂的变量。我希望本文对你有所帮助,并且像往常一样,如果你想了解更多信息,请继续关注即将发布的文章。

参考文献

  1. Storage vs. Memory vs. Stack in Solidity & Ethereum: https://dlt-repo.net/storage-vs-memory-vs-stack-in-solidity-ethereum/
  2. The Ethereum Virtual Machine: https://fullstacks.org/materials/ethereumbook/14_evm.html
  3. ETHEREUM VIRTUAL MACHINE: https://ethereum.org/en/developers/docs/evm/
  4. gas: https://ethereum.org/en/developers/docs/gas/

    本翻译由 Cell Network 赞助支持。

点赞 9
收藏 7
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

5 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO