Solidity 数据存储如何工作?
欢迎阅读“跟我学习 Solidity ”系列中的另一篇文章。正如我在上一篇文章中保证过,我们将看到Solidity中数据存储的工作方式。
在讨论Solidity中的数据存储之前,我想介绍一些有关以太坊虚拟机的知识,以使事情更清楚。
EVM的内部工作原理:
EVM上下文(来源:fullstacks.org)
当我们安装以太坊客户端时,它附带了EVM,EVM是专门创建来运行智能合约的轻量级操作系统。 EVM是基于堆栈计算机的模型的体系结构,这意味着该指令集被设计为用于堆栈而不是寄存器。EVM 操作码的列表在黄皮书中进行了描述,可以在“ 以太坊虚拟机(EVM)操作码和指令参考”中找到。
代码执行步骤如下:当交易触发智能合约代码执行时,将实例化EVM,并在EVM的ROM中加载要调用的合约的代码。程序计数器设置为零,从合约帐户的存储器中加载存储器,存储器全部设置为零,并且所有块和环境变量都已设置。然后代码被执行。
现在让我们回到memory关键字,如Solidity 文档中所述。从版本0.5.0开始,所有复杂类型都必须给出一个明确的数据存储位置,并且有三个数据位置:memory(内存)
,storage(存储)
和calldata(调用数据)
。
注意:唯一可以省略数据位置的地方是状态变量,因为状态变量始终存储在帐户的存储空间中。
storage
(存储)storage
中的数据被永久存储。其以键值形式存储。storage
中的数据写在区块链中(因此它们会更改状态),这就是为什么使用存储非常昂贵的原因。storage
的值将花费5,000 gas 。memory
(内存)memory
是一个字节数组,其插槽大小为256位(32个字节)。数据仅在函数执行期间存储,执行完之后,将其删除。它们不会保存到区块链中。calldata
(调用数据)calldata
是一个不可修改的,非持久性的区域,用于存储函数参数,并且其行为基本上类似于memory
。calldata
,但也可用于其他变量。calldata
数据位置的数组和结构体也可以从函数中返回,但是不可以为这种类型赋值。如果你不想发生意外的行为,那么了解数据位置赋值的工作方式非常重要。
在赋值之间应用以下规则:
storage
和memory
之间(或来自calldata
)的赋值总是创建一个独立的副本。memory
到memory
的赋值仅创建引用。这意味着对一个内存变量的更改在引用相同数据的所有其他内存变量中同样有效。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
。你将在控制台中看到交易的详细信息,并在其旁边有一个调试按钮。点击它。
你现在应该看到如下所示的调试器区域:
要单步执行(Step over)代码,请单击我以红色框中的箭头。
storage
到memory
的赋值你应该首先注意到,正如我们在EVM部分中提到的, 状态(Solidity State)部分加载了storage
的stateVar的内容,当然没有局部变量。
当你单步执行时,应该看到变量y
出现在局部变量(Solidity locals)部分中。继续执行(step over),你会注意到,为了分配必要的内存空间,需要使用很多字节码
并从storage
中加载每个字,然后将其复制到memory
中。这意味着要支付更多的 gas ,因此从storage
到memory
的赋值非常昂贵。
memory
到storage
的赋值让我们研究第二种情况:从memory
到storage
的赋值。
当你修改完存储在memory
中的副本并且想要将更改保存回storage
时,可以使用它。它同样消耗大量的 gas 。如果我们用调试器步骤详细信息中的剩余 gas (remaining gas)来计算 gas 差值,则为17,083 gas 。该操作使用了四个SSTORE操作码:第一个用于存储数组大小(保持不变,消耗800个 gas ),另外三个用于更新数组的值(每个消耗了5,000个 gas )。
storage
到storage
的赋值现在,让我们看一下情况三:从storage
到storage
的赋值。这次将创建一个新的局部变量,并包含与stateVar相同的内容。如果我们查看代码的执行过程,会注意到Solidity所做的,将包含数组长度的存储的第一个插槽地址入栈。根据文档,对于动态数组,插槽位置包含数组长度,用于计算包含数组数据的插槽位置。
来比较两者的 gas 成本:
第一种是将数据复制到memory
,然后更新并复制回 storage
, 使用21,629 gas ,
第二种是直接创建引用并更新状态, 使用 5,085 gas 。
那么很明显第二种方法是便宜得多。
但是,如果我们像这样直接更新状态变量呢,像这样:
stateVar[0] = 12;
也有可能但是,如果你要处理映射和嵌套数据类型(我们将在后面看到),则使用storage
指针可以让代码更具可读性。
为了使本文简短,而不会给你太多信息,我决定在下一篇文章继续介绍复杂的变量。我希望本文对你有所帮助,并且像往常一样,如果你想了解更多信息,请继续关注即将发布的文章。
本翻译由 Cell Network 赞助支持。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!