c++ 数组引用_在 Solidity中使用值数组以降低 gas 消耗
背景
我們Datona Labs在開發和測試Solidity數據訪問合約(S-DAC:Smart-Data-Access-Contract)模板過程中,經常需要使用只有很小數值的小數組(數組元素個數少)。在本示例中,研究了使用值數組(Value Array)是否比引用數組(Reference Array)更高效。
討論
Solidity支持內存(memory)中的分配數組,這些數組會很浪費空間(參考 文檔[1]),而存儲(storage)中的數組則會消耗大量的gas來分配和訪問存儲。但是Solidity所運行的以太坊虛擬機(EVM)[2]有一個256位(32字節)機器字長。正是后一個特性使我們能夠考慮使用值數組(Value Array)。在機器字長的語言中,例如32位(4字節),值數組(Value Array)不太可能實用。
我們可以使用值數組(Value Array)減少存儲空間和gas消耗嗎?
譯者注:機器字長 是指每一個指令處理的數據長度。
比較值數組與引用數組
引用數組(Reference Array)
在 Solidity 中,數組通常是引用類型。這意味著每當在程序中遇到變量符號時,都會使用指向數組的指針,不過也有一些例外情況會生成一個拷貝(參考文檔-引用類型[3])。在以下代碼中,將10個元素的 8位uint ?users 的數組傳遞給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; }}函數返回后,users數組元素將被更改。
值數組(Value Arrays)
值數組是以值類型[4]保存的數組。這意味著在程序中遇到變量符號,就會使用其值。
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 目前的版本[5]中,我們無法使用數組訪問方式寫入某個字節:
... bytes32 bs = "hello"; bs[0] = 'c'; // 不可以實現 ...讓我們使用Solidity的 using for[6] 導入庫的方式為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'); // 類似 va[0] = 'c'; 的功能 va = va.set(0, 'c'); require(va[0] == 'c'); }}在這里,你可以清楚地看到set()函數的返回值被分配回參數變量。如果缺少賦值,則變量將保持不變,require()就是來驗證它。
可能的固定長度值數組
在Solidity機器字長為256位(32字節),我們可以考慮以下可能的值數組。
固定長度值數組
這些是以些Solidity可用整型[7]匹配的固定長度的值數組:
固定長度值數組類型 類型名 描述uint128[2] uint128a2 2個128位元素的值數組uint64[4] uint64a4 4個64位元素的值數組uint32[8] uint32a8 8個32位元素的值數組uint16[16] uint16a16 16個16位元素的值數組uint8[32] uint8a32 32個8位元素的值數組128位元素: 意思是一個元素占用128位空間
我建議使用如上所示的類型名,這在本文中都會用到,但是你可能會找到一個更好的命名約定。
更多固定長度值數組
實際上,還有更多可能的值數組。我們還可以考慮與Solidity可用類型不匹配的類型,對于特定解決方案可能有用。X(值的位數)乘以Y(元素個數)必須小于等于256:
更多固定長度值數組類型 類型名 描述uintX[Y] uintXaY X * Y <= 256uint10[25] uint10a25 25個10位元素的值數組uint7[36] uint7a36 36個7位元素的值數組uint6[42] uint6a42 42個6位元素的值數組uint5[51] uint5a51 51個5位元素的值數組uint4[64] uint4a64 64個4位元素的值數組uint1[256] uint1a256 256個1位元素的值數組...特別感興趣的是uint1a256值數組。這使我們可以將最多256個1位元素值(代表布爾值)有效地編碼為1個EVM字長。相比之下,Solidity的bool [256]會消耗256倍的內存空間,甚至是8倍的存儲空間。
還有更多固定長度值數組
還有更多可能的值數組。以上是最有效的值數組類型,因為它們有效地映射到EVM字長中的位。在上面的值數組類型中,X表示元素所占用的位數。
還有按位移位技術的在算術編碼中使用乘法和除法,但這超出了本文的范圍,可以參考這里[8]
固定長度值數組實現
下面是一個有用的可導入庫文件,為值數組類型uint8a32提供get和set函數:
// uint8a32.sollibrary uint8a32 { // 等效于 uint8[32] uint constant bits = 8; uint constant elements = 32; // 確保 bits * elements <= 256 uint constant range = 1 << bits; uint constant max = range - 1; // get 函數 function get(uint va, uint index) internal pure returns (uint) { require(index < elements); return (va >> (bits * index)) & max; } // set 函數 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庫代碼,然后更改bits和elements常量,即可用于其他uintXaY值數組類型。
Solidity庫合約中無法存儲變量[9]。
測試一把
讓我們測試一下上面的示例庫代碼:
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 for 指令,因此可以在變量上直接使用. 語法來調用set()函數。但是在你的智能合約需要多種不同的值數組類型的情況下,由于名稱空間沖突(或者需要每種類型使用各自特定名稱的函數),這需要使用顯式庫名點表示法來訪問函數:
import "uint8a32.sol";import "uint16a16.sol";contract MyContract { uint users; // uint8a32 uint roles; // uint16a16 ... function setUser(uint n, uint user) private { // 想實現的是: users = users.set(n, user); users = uint8a32.set(users, n, user); } function setRole(uint n, uint role) private { // 想實現的是: 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()函數的返回值賦值。但是,由于此技術使用更多的內存,代碼和復雜性,因此抵消了使用值數組的可能優勢。
Gas 消耗對比
編寫了庫和合約后,我們使用在此文[10]中介紹的技術測量了gas消耗。結果如下:
bytes32 值數組
1_1rFIufB3Y9e6txiTnDpoKQ在內存和存儲上,bytes32的get和set的Gas消耗32個變量
不用奇怪,在內存中gas消耗可以忽略不計,而存儲中,gas消耗是巨大的,尤其是第一次用非零值(大藍色塊)寫入存儲位置時。隨后使用該存儲位置消耗的gas要少得多。
uint8a32 值數組
在這里,我們比較了在EVM內存中使用固定長度的uint8 []數組與uint8a32值數組的情況:
uint8與byte內存上gas 消耗對比在uint8/byte內存上,gas 消耗對比
令人驚訝的是,uint8a32 值數組消耗的gas只有固定長度數組uint8[32] 的一半左右。而uint8[16]和uint8[4]相應的gas消耗更低。這是因為值數組代碼必須讀取和寫入值才能設置元素值,而uint8[]只需寫入值。
以下是在EVM存儲中比較gas 消耗:
gas 消耗對比在存款上,gas 消耗的對比
在這里,與使用uint8[Y]相比,每個uint8a32 set() 函數消耗的gas循環少幾百個。uint8 [32],uint8 [16]和uint8 [4]的gas 消耗量相同,因為它們使用相同數量的EVM存儲空間(一個32字節的插槽)。
uint1a256 值數組
在EVM內存中,固定長度的bool[]數組與uint1a256值數組的gas對比:
gas 對比bool與1bit 在內存的 gas消耗 對比
顯然,bool數組的gas消耗很顯著
相同的比較在EVM存儲中:
1_pqdUNkuGjqJd7UyejQxoIgbool與1bit 在存儲中的 gas消耗 對比
bool [256]和bool [64] 使用2個存儲插槽,因此gas 消耗相似。bool [32]和uint1a256僅使用一個存儲插槽。
作為合約和庫的參數
參數的gas消耗將bool/1bit參數傳遞給合約或庫的gas消耗
不用奇怪,最大的gas消耗是為合約或庫函數提供數組參數。
使用單個值而不是復制數組顯然會消耗更少的gas。
其他可能性
如果你發現固定長度的值數組很有用,那么你還可以考慮固定長度的多值數組、動態值數組、值隊列、值堆棧等。
結論
我已經提供用于寫入Solidity bytes32變量的代碼,以及用于uintX [Y]值數組的通用庫代碼。
也提出了如固定長度的多值數組,動態值數組,值隊列,值堆棧等其他可能性。
是的,我們可以使用值數組減少存儲空間和gas消耗。
如果你的Solidity智能合約使用較小值的小數組(例如用戶ID,角色等),則使用值數組可能會消耗更少的gas。
當數組被復制時,例如智能合約或庫參數,值數組將始終消耗少得多的gas。
作者:Julian Goddard[11]
https://medium.com/coinmonks/value-arrays-in-solidity-32ca65135d5b
參考資料
[1]文檔: https://learnblockchain.cn/docs/solidity/types.html#arrays
[2]以太坊虛擬機(EVM): https://learnblockchain.cn/2019/04/09/easy-evm
[3]文檔-引用類型: https://learnblockchain.cn/docs/solidity/types.html#reference-types
[4]值類型: https://learnblockchain.cn/docs/solidity/types.html#value-types
[5]Solidity 目前的版本: https://learnblockchain.cn/docs/solidity/types.html#index-7
[6]using for: https://learnblockchain.cn/docs/solidity/contracts.html#using-for
[7]可用整型: https://learnblockchain.cn/docs/solidity/types.html#integers
[8]這里: https://en.wikipedia.org/wiki/Arithmetic_coding
[9]無法存儲變量: https://solidity.readthedocs.io/en/latest/contracts.html#libraries
[10]此文: https://medium.com/coinmonks/gas-cost-of-solidity-library-functions-dbe0cedd4678
[11]Julian Goddard: https://medium.com/@plaxion?source=post_page-----32ca65135d5b----------------------
譯者:Tiny熊
作者主頁:
https://learnblockchain.cn/people/15
總結
以上是生活随笔為你收集整理的c++ 数组引用_在 Solidity中使用值数组以降低 gas 消耗的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: db2分页查询sql语句_MySQL学习
- 下一篇: python自定义变量名_Python