如何使用值数组(Value Array)模式减少Solidity的高gas损耗问题
本文讨论如何使用值数组(Value Array)模式减少Solidity的高gas损耗问题。
背景
在Datona Labs的Solidity智能数据访问合约(S-DAC)模板的开发和测试过程中,我们经常需要使用较小值的小数组。在本文的示例中,我研究了使用值数组(Value Array)是否比引用数组更有效地做到这一点。
讨论
Solidity支持内存中的数组,这些数组可能会浪费空间阵列,而存储中的数组则会消耗大量的气体来分配和访问阵列。但是Solidity也运行在以太坊虚拟机(EVM)上,它有一个256bits(32字节)的非常大的机器字。正是后一个特性使我们能够考虑使用值数组(Value Array)。在字型较小的语言中,例如32位(4字节),值数组(Value Array)不太可能实用。
值数组与引用数组的比较
引用数组
实际上,数组通常是引用类型。这意味着每当在程序文本中遇到变量符号时,都会使用指向数组的指针,不过也有一些例外情况会生成一个副本。在以下代码中,将10位8位uint用户的数组传递给函数setUser,该函数设置users数组中的一个元素:
contract TestReferenceArray { function test() public pure { uint8[10] memory users; setUser(users, 5, 123); require(users[5] == 123); } function setUser(uint8[10] memory users, uint index, uint8 ev) public pure { users[index] = ev; }}
函数返回后,用户中的数组元素将被更改。
值数组
值数组是以值类型保存的数组。这意味着只要在程序文本中遇到变量符号,就会使用该值。
contract TestValueArray { function test() public pure { uint users; users = setUser(users, 5, 12345); require(users == ...); } function setUser(uint users, uint index, uint ev) public pure returns (uint) { return ...; }}
请注意,在函数返回之后,函数的users参数将保持不变,因为它是通过值传递的-为了获得更改后的值,有必要将函数返回值分配给users变量。
Solidity bytes32值数组
Solidity在bytesX(X=1..32)类型中提供了一个部分值数组。这些存储字节可以使用数组式访问单独读取,例如:
... bytes32 bs = "hello"; byte b = bs[0]; require(bs[0] == 'h'); ...
但不幸的是,在Solidity v0.7.1中,我们无法使用数组式访问写入单个字节:
... bytes32 bs = "hello"; bs[0] = 'c'; // unfortunately, this is NOT possible! ...
首先,让我们使用Solidity在导入库文件中使用库的类型将函数添加到bytes32类型:
library bytes32lib { uint constant bits = 8; uint constant elements = 32; function set(bytes32 va, uint index, byte ev) internal pure returns (bytes32) { require(index < elements); index = (elements - 1 - index) * bits; return bytes32((uint(va) & ~(0x0FF << index)) | (uint(uint8(ev)) << index)); }}
这个库提供了函数set(),它允许调用者将bytes32变量中的任何字节设置为任何所需的字节值。根据您的需求,您可能希望为您使用的其他bytesX类型生成类似的库。
让我们导入该库并测试它:
import "bytes32lib.sol";contract TestBytes32 { using bytes32lib for bytes32; function test1() public pure { bytes32 va = "hello"; require(va[0] == 'h'); // the replacement for this: va[0] = 'c'; va = va.set(0, 'c'); require(va[0] == 'c'); }}
在这里,您可以清楚地看到set()函数的返回值被分配回参数变量。如果缺少赋值,则变量将保持不变,如require()所测试的那样。
可能的固定值数组
在Solidity机器字类型256位(32字节)中,我们可以考虑以下可能的值数组。
固定值数组
这些是与某些Solidity可用类型匹配的固定值数组:
Fixed Value ArraysType Type Name Descriptionuint128[2] uint128a2 two 128bit element valuesuint64[4] uint64a4 four 64bit element valuesuint32[8] uint32a8 eight 32bit element valuesuint16[16] uint16a16 sixteen 16bit element valuesuint8[32] uint8a32 thirty-two 8bit element values
我建议使用如上所示的类型名,这在本文中都会用到,但是您可能会找到一个更好的命名约定。
更多固定值数组
实际上,还有更多可能的值数组。我们还可以考虑与Solidity可用类型不匹配的类型,但对于特定解决方案可能有用。X值中的位数乘以Y元素数必须小于或等于256:
More Fixed Value ArraysType Type Name DescriptionuintX[Y] uintXaY X * Y <= 256uint10[25] uint10a25 twenty-five 10bit element valuesuint7[36] uint7a36 thirty-six 7bit element valuesuint6[42] uint6a42 forty-two 6bit element valuesuint5[51] uint5a51 fifty-one 5bit element valuesuint4[64] uint4a64 sixty-four 4bit element valuesuint1[256] uint1a256 two-hundred & fifty-six 1bit element valuesetcetera
特别有趣的是uint12256值数组。这使得我们能够高效地将256个表示布尔值的1位元素值高效地编码到1个EVM字中。相比之下,Solidity的bool[256]消耗了256倍的内存空间,甚至是8倍的存储空间。
甚至更多的固定值数组
还有更多可能的值数组。以上是最有效的值数组类型,因为它们有效地映射到EVM字中的位。在上面的值数组类型中,X始终是许多位数。此处使用的按位移位技术的另一种方法是在算术编码中使用乘法和除法,但这超出了本文的范围。
让我们看看一个可能的实现.
固定值数组实现
下面是一个有用的导入文件,为值数组类型uint8a32提供get和set函数:
// uint8a32.sollibrary uint8a32 { // provides the equivalent of uint8[32] uint constant bits = 8; uint constant elements = 32; // must ensure that bits * elements <= 256 uint constant range = 1 << bits; uint constant max = range - 1; // get function function get(uint va, uint index) internal pure returns (uint) { require(index < elements); return (va >> (bits * index)) & max; } // set function function set(uint va, uint index, uint ev) internal pure returns (uint) { require(index < elements); require(value < range); index *= bits; return (va & ~(max << index)) | (ev << index); }}
get()函数只是根据index参数从值数组中返回适当的值。set()函数将删除现有值,然后根据index参数将给定值设置为返回值。
可以推断出,只需复制上面给出的uint8a32库代码,然后更改位和元素常量,即可使用其他uintXaY值数组类型。
Solidity库合约中不允许存储空间变量。
让我们看看上面的示例库代码的几个简单测试:
import "uint8a32.sol";contract TestUint8a32 { using uint8a32 for uint; function test1() public { uint va; va = va.set(0, 0x12); require(va.get(0) == 0x12, "va[0] not 0x12"); va = va.set(1, 0x34); require(va.get(1) == 0x34, "va[1] not 0x34"); va = va.set(31, 0xF7); require(va.get(31) == 0xF7, "va[31] not 0xF7"); }}
由于将编译器的using库用于type指令,因此使用set()函数的语法可以使用可变点符号。但是在您的智能合约需要多种不同的值数组类型的情况下,由于名称空间冲突,这是不可能的(每种类型只能使用一种特定名称的函数),因此必须使用显式库名点表示法来访问 函数代替:
import "uint8a32.sol";import "uint16a16.sol";contract MyContract { uint users; // uint8a32 uint roles; // uint16a16 ... function setUser(uint n, uint user) private { // wanted to do this: users = users.set(n, user); users = uint8a32.set(users, n, user); } function setRole(uint n, uint role) private { // wanted to do this: roles = roles.set(n, role); roles = uint16a16.set(roles, n, role); } ...}
还必须警惕在正确的变量上使用正确的值数组类型。
这是相同的代码,但数据类型已合并到变量名称中,以解决该问题:
import "uint8a32.sol";import "uint16a16.sol";contract MyContract { uint users_u8a32; uint roles_u16a16; ... function setUser(uint n, uint user) private { users_u8a32 = uint8a32.set(users_u8a32, n, user); } function setRole(uint n, uint role) private { roles_u16a16 = uint16a16.set(roles_u16a16, n, role); } ...}
如果我们提供一个采用1元素数组的函数,则实际上有可能避免使用set()函数的返回值赋值。但是由于此技术使用更多的内存,代码和复杂性,因此否定了使用值数组的可能优点。
气体消耗量
编写了库和合约后,我们使用作者在本文中介绍的技术测量了气体消耗。结果如下:
bytes32值数组
内存和存储字节上的get和set的Gas消耗32个变量
不足为奇的是,内存气体消耗可以忽略不计,而存储气体消耗是巨大的——尤其是第一次用非零值(大蓝砖)写入存储位置时。随后使用该存储位置消耗的天然气要少得多。
uint8a32值数组
在这里,我们比较了在EVM内存空间中使用固定uint8 []数组与uint8a32值数组的情况:
在uint8/byte内存变量上获取和设置的耗气量
令人惊讶的是,uint8a32值数组消耗的气体量只有uint8 [32] Solidity固定数组的一半。在uint8 [16]和uint8 [4]的情况下,相关的气体消耗相应降低。这是因为值数组代码必须读取和写入值才能设置元素值,而uint8 []只需写入值。
这是这些在EVM存储空间中进行比较的方式:
get和set on uint8/byte存储变量的耗气体量
在这里,与使用uint8 [Y]相比,每个uint8a32 set()函数消耗的气体循环少几百个。uint8 [32],uint8 [16]和uint8 [4]的耗气体量相同,因为它们使用相同数量的EVM存储空间(一个32字节的插槽)。
uint1a256值数组
EVM内存空间中固定bool[]数组与uint12256值数组的比较:
通过bool/1bit内存变量获取和设置的耗气体量
显然,分配bool数组的气体消耗占主导地位。
EVM存储空间中的相同比较:
bool/1bit存储变量上get和set的耗气量
简单的测试涉及bool [256]和bool [64]的2个存储插槽,因此耗气量相似。Bool [32]和uint1a256仅接触一个存储插槽。
分包智能合约和库的参数
将bool/1bit参数传递给分包智能合约或库的气体消耗
毫不奇怪,最大的气体消耗是为分包智能合约或库函数提供数组参数。
使用单个值而不是复制数组显然会消耗更少的气体。
其他可能性
如果您发现固定值数组很有用,那么您还可以考虑固定多值数组、动态值数组、值队列、值堆栈等。
结论
我已经提供并测量了用于写入Solidity bytes32变量的代码,以及用于uintX [Y]值数组的通用库代码。
我已经揭示了其他可能性,例如固定多值数组,动态值数组,值队列,值堆栈等。
是的,我们可以使用Value Array减少存储空间和气体消耗。
如果您的Solidity智能合约使用较小值的小数组(用于用户ID,角色等),则使用“价值数组”可能会消耗更少的汽油。
复制数组的位置,例如对于分包智能合约或库,值数组将始终消耗少得多的气体。
微信扫描关注公众号,及时掌握新动向
2.本文版权归属原作所有,仅代表作者本人观点,不代表比特范的观点或立场
2.本文版权归属原作所有,仅代表作者本人观点,不代表比特范的观点或立场