优化智能合约。 实体类型如何影响交易成本

程序员花费大量时间来担心程序的速度,而试图提高效率通常会严重影响调试和支持程序的能力。 在97%的情况下,有必要忘记小的优化。 过早的优化是万恶之源! 但是我们绝不能忽略这3%真正重要的部分!”
唐纳德·纳特(Donald Knut)。
以太坊气

在进行智能合约的审计时,我们有时会问自己,它们的开发是否与那些不需要考虑优化的97%有关,或者我们只处理那些重要的案例中的3%。 我们认为,第二种可能性更大。 与其他应用程序不同,智能合约不会更新,因此无法“随时随地”对其进行优化(前提是未规定其算法,但这是一个单独的主题)。 支持早期合同优化的第二个论点是,与大多数次优性仅在规模上体现的,与铁和环境的具体特征有关的大多数系统不同,它是由大量指标衡量的,而智能合约本质上是唯一的性能指标-燃气消耗。

因此,从技术上来说,评估合同的有效性比较容易,但是开发人员经常继续依靠自己的直觉,并进行与Knut教授所说的盲目“过早优化”。 我们将通过选择变量的位深度的示例来检查该解决方案与实际情况对应的直观程度。 在此示例中,与大多数实际情况一样,我们将无法实现节省,甚至反之亦然,就天然气消耗而言,我们的合同将变得更加昂贵。

哪种气体?


以太坊就像一台全球计算机,其“处理器”是EVM虚拟机,“程序代码”是智能合约中记录的一系列命令和数据,而呼叫是来自外部世界的交易。 事务打包在相关的结构中-每几秒钟发生一次的块。 并且由于定义上限制了块大小,并且处理协议是确定性的(需要网络的所有节点对块中的所有事务进行统一处理),因此为了用有限的节点资源来满足潜在的无限需求并防止DoS,系统必须提供公平的算法来选择服务于谁的请求,作为许多公共区块链中的一种机制,它忽略了一个简单的原理-发送方可以选择向矿工支付的报酬金额以执行其交易 ktsii和矿工选他们的需求包括块,并且其不,选择最有利可图的自己。

例如,在比特币中,该区块被限制为一兆字节,矿工基于其长度和建议的佣金(选择每字节比率最大satoshis的交易者)选择是否将交易包括在该区块中。

对于更复杂的以太坊协议,此方法不合适,因为单个字节既可以表示没有操作(例如STOP代码),又可以表示对存储设备(SSTORE)的昂贵且缓慢的写入操作。 因此,根据其资源消耗情况,为空中的每个操作码提供自己的价格。

协议规范中的费用表
以太坊收费表
不同类型操作的气体流量表。 以太坊黄纸协议规范。

与比特币不同,以太坊交易的发送者并没有设置加密货币的佣金,而是他愿意花费的最大天然气量-开始天然气和每单位gas- gasPrice的价格。 当虚拟机执行代码时,将从startGas中减去每个后续操作的气体量,直到达到代码的出口或气体用完为止。 显然,这就是为什么将这种奇怪的名称用于此工作单元的原因-交易就像汽车一样充满了汽油,是否到达目的地取决于罐中是否有足够的容积。 代码执行完成后,通过将实际消耗的气体乘以发件人设置的价格(每件wei )从所接收的空气量中扣除交易发件人的费用。 在全球网络中,这是在“挖掘”包括相应交易的区块时发生的,在Remix环境中,该交易是即时,免费且没有任何条件的“开采”。

我们的工具-Remix IDE


对于气体消耗的“分析”,我们将使用在线环境来开发Remix IDE的以太坊合约。 该IDE包含语法高亮代码编辑器,工件查看器,合同界面渲染,虚拟机可视调试器,所有可能版本的JS编译器以及许多其他重要工具。 我强烈建议和他一起开始学习以太。 另外一个优点是它不需要安装-只需在官方网站的浏览器中打开它即可。

变量类型选择


Solidity语言的规范为开发人员提供了多达32位整数类型uint-从8到256位。 想象一下,您正在开发一个智能合约,该合约旨在存储一个人的年龄(以年为单位)。 您选择什么深度单位?

为特定任务选择最小的足够类型是很自然的-uint8在数学上适合这里。 逻辑上假设,我们存储在区块链上的对象越小,我们在执行上花费的内存越少,我们拥有的开销越少,我们支付的费用就越少。 但是在大多数情况下,这种假设是不正确的。

对于实验,我们从Solidity官方文档提供的内容中获取最简单的合同,并以两个版本收集它-使用变量类型uint256和较小类型32倍-uint8。

simpleStorage_uint256.sol
 pragma solidity ^0.4.0; contract SimpleStorage { //uint is alias for uint256 uint storedData; function set(uint x) public { storedData = x; } function get() public view returns (uint) { return storedData; } } 
pragma solidity ^0.4.0; contract SimpleStorage { //uint is alias for uint256 uint storedData; function set(uint x) public { storedData = x; } function get() public view returns (uint) { return storedData; } }


simpleStorage_uint8.sol
 pragma solidity ^0.4.0; contract SimpleStorage { uint8 storedData; function set(uint8 x) public { storedData = x; } function get() public view returns (uint) { return storedData; } } 



衡量“储蓄”


因此,将创建合同,将其加载到Remix中,进行部署,并由事务执行对.set()方法的调用。 我们看到了什么? 录制长类型比录制短类型要贵 -20464与20205天然气单位! 怎么了 怎么了 让我们弄清楚!

Remix IDE中的以太坊气体消耗

储存uint8与uint256


出于显而易见的原因,写入持久性存储是该协议中最昂贵的操作之一:首先,记录状态会增加整个节点所需的磁盘空间。 此存储的大小不断增加,并且在节点上存储的状态越多,同步发生的速度就越慢,对基础结构的要求(分区大小,iops数量)就越高。 在高峰时间,决定整个网络性能的是缓慢的磁盘IO操作。

可以合理地预期uint8的存储成本应比uint256便宜几十倍。 但是,在调试器中,您可以看到两个值在存储插槽中的位置与256位值完全相同。



在这种特殊情况下,使用uint8不会对写入存储的成本产生任何好处。

处理uint8与uint256


如果不是在存储期间使用uint8,那么至少在处理内存中的数据时,也许我们会从中受益? 下面比较从不同类型的变量获得的相同功能的指令。



您可以看到,使用uint8进行操作的指令甚至超过了uint256。 这是因为机器将8位值转换为本地256位字,结果,代码被发送方付款的其他指令所包围。 在这种情况下,不仅编写而且使用uint8类型执行代码也更加昂贵。

在哪里可以证明使用短类型?


我们的团队从事智能合约的审核已经很长时间了,到目前为止,还没有一个实际案例可以证明,在为审核提供的代码中使用小字体会节省成本。 同时,在某些非常特定的情况下,从理论上讲可以节省费用。 例如,如果您的合同存储了大量的小状态变量或结构,则可以将它们打包到更少的存储插槽中。

在以下示例中,差异将最为明显:

1.带有32个变量uint256的协定

simpleStorage_32x_uint256.sol
 pragma solidity ^0.4.0; contract SimpleStorage { uint storedData1; uint storedData2; uint storedData3; uint storedData4; uint storedData5; uint storedData6; uint storedData7; uint storedData8; uint storedData9; uint storedData10; uint storedData11; uint storedData12; uint storedData13; uint storedData14; uint storedData15; uint storedData16; uint storedData17; uint storedData18; uint storedData19; uint storedData20; uint storedData21; uint storedData22; uint storedData23; uint storedData24; uint storedData25; uint storedData26; uint storedData27; uint storedData28; uint storedData29; uint storedData30; uint storedData31; uint storedData32; function set(uint x) public { storedData1 = x; storedData2 = x; storedData3 = x; storedData4 = x; storedData5 = x; storedData6 = x; storedData7 = x; storedData8 = x; storedData9 = x; storedData10 = x; storedData11 = x; storedData12 = x; storedData13 = x; storedData14 = x; storedData15 = x; storedData16 = x; storedData17 = x; storedData18 = x; storedData19 = x; storedData20 = x; storedData21 = x; storedData22 = x; storedData23 = x; storedData24 = x; storedData25 = x; storedData26 = x; storedData27 = x; storedData28 = x; storedData29 = x; storedData30 = x; storedData31 = x; storedData32 = x; } function get() public view returns (uint) { return storedData1; } } 



2.使用32个uint8变量进行收缩

simpleStorage_32x_uint8.sol
 pragma solidity ^0.4.0; contract SimpleStorage { uint8 storedData1; uint8 storedData2; uint8 storedData3; uint8 storedData4; uint8 storedData5; uint8 storedData6; uint8 storedData7; uint8 storedData8; uint8 storedData9; uint8 storedData10; uint8 storedData11; uint8 storedData12; uint8 storedData13; uint8 storedData14; uint8 storedData15; uint8 storedData16; uint8 storedData17; uint8 storedData18; uint8 storedData19; uint8 storedData20; uint8 storedData21; uint8 storedData22; uint8 storedData23; uint8 storedData24; uint8 storedData25; uint8 storedData26; uint8 storedData27; uint8 storedData28; uint8 storedData29; uint8 storedData30; uint8 storedData31; uint8 storedData32; function set(uint8 x) public { storedData1 = x; storedData2 = x; storedData3 = x; storedData4 = x; storedData5 = x; storedData6 = x; storedData7 = x; storedData8 = x; storedData9 = x; storedData10 = x; storedData11 = x; storedData12 = x; storedData13 = x; storedData14 = x; storedData15 = x; storedData16 = x; storedData17 = x; storedData18 = x; storedData19 = x; storedData20 = x; storedData21 = x; storedData22 = x; storedData23 = x; storedData24 = x; storedData25 = x; storedData26 = x; storedData27 = x; storedData28 = x; storedData29 = x; storedData30 = x; storedData31 = x; storedData32 = x; } function get() public view returns (uint) { return storedData1; } } 



部署第一个合同(32 uint256)所需的费用更少,只有89941瓦斯,但是.set()的价格要贵得多 它将占用256个存储空间,每次通话将消耗640,639汽油。 部署(221663 gas)时,第二个合同(32 uint8)的价格将是原来的两倍半,但是对.set()方法的每次调用都将便宜得多,因为 仅更改该阶段的一个单元(185291气体)。

是否应该应用这种优化?


争论的重点是类型优化的重要性。 如您所见,即使对于这样一个经过特殊选择的综合案例,我们也没有获得多重差异。 使用uint8或uint256的选择实际上说明了优化应该有意义地应用(了解工具,配置文件),或者根本不考虑它。 以下是一些一般准则:

  • 如果合同在存储库中包含许多较小的数字或紧凑的结构,则可以考虑进行优化;
  • 如果您使用“缩写”类型-请记住有关溢流/溢流漏洞的信息
  • 对于未写入存储库的内存变量和函数参数,最好使用本机类型uint256(或其别名uint)。 例如,将列表迭代器设置为uint8是没有意义的–只是失败;
  • 对于编译器而言,正确存储在存储插槽中的包装非常重要,它是合同中变量顺序

参考文献


我将得到没有任何禁忌的建议:尝试开发工具,了解语言,库和框架的规范。 我认为,以下是开始学习以太坊平台的最有用的链接:

  • Remix合同开发环境是一个功能非常强大的基于浏览器的IDE。
  • Solidity语言的规范 ,该链接将专门转到状态变量布局一节;
  • 来自著名的OpenZeppelin团队的非常有趣的合同 。 令牌,众包合同的实现示例,最重要的是-SafeMath库,该库有助于安全处理类型;
  • 以太坊黄纸 ,以太坊虚拟机的正式规范;
  • Ethereum白皮书 ,以太坊平台的规范,一个具有大量链接的更通用,更高级的文档;
  • 25分钟内的以太坊,平台创建者Vitalik Buterin对Ethereum进行了简短但宽敞的技术介绍;
  • Etherscan区块链资源管理器 ,进入真实以太世界的窗口,主网络上的区块,交易,代币,合约的浏览器。 在Etherscan上,您可以找到测试网络Rinkeby,Ropsten,Kovan(带有免费广播的网络,基于不同的共识协议)的资源管理器。

Source: https://habr.com/ru/post/zh-CN415791/


All Articles