在 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 值陣列

在記憶體和儲存上,bytes32的get和set的Gas消耗32個變數

不用奇怪,在記憶體中gas消耗可以忽略不計,而儲存中,gas消耗是巨大的,尤其是第一次用非零值(大藍色塊)寫入儲存位置時。隨後使用該儲存位置消耗的gas要少得多。

uint8a32 值陣列

在這裡,我們比較了在EVM記憶體中使用固定長度的uint8 []陣列與uint8a32值陣列的情況:

在uint8/byte記憶體上,gas 消耗對比

令人驚訝的是,uint8a32 值陣列消耗的gas只有固定長度陣列uint8[32] 的一半左右。而uint8[16]和uint8[4]相應的gas消耗更低。這是因為值陣列程式碼必須讀取和寫入值才能設定元素值,而uint8[]只需寫入值。

以下是在EVM儲存中比較gas 消耗:

在存款上,gas 消耗的對比

在這裡,與使用uint8[Y]相比,每個uint8a32 set() 函式消耗的gas迴圈少幾百個。uint8 [32],uint8 [16]和uint8 [4]的gas 消耗量相同,因為它們使用相同數量的EVM儲存空間(一個32位元組的插槽)。

uint1a256 值陣列

在EVM記憶體中,固定長度的bool[]陣列與uint1a256值陣列的gas對比:

bool與1bit 在記憶體的 gas消耗 對比

顯然,bool陣列的gas消耗很顯著

相同的比較在EVM儲存中:

bool與1bit 在儲存中的 gas消耗 對比

bool [256]和bool [64] 使用2個儲存插槽,因此gas 消耗相似。bool [32]和uint1a256僅使用一個儲存插槽。

作為合約和庫的引數

將bool/1bit引數傳遞給合約或庫的gas消耗

不用奇怪,最大的gas消耗是為合約或庫函式提供陣列引數。

使用單個值而不是複製陣列顯然會消耗更少的gas。

其他可能性

如果你發現固定長度的值陣列很有用,那麼你還可以考慮固定長度的多值陣列、動態值陣列、值佇列、值堆疊等。

結論

我已經提供用於寫入Solidity bytes32變數的程式碼,以及用於uintX [Y]值陣列的通用庫程式碼。

也提出瞭如固定長度的多值陣列,動態值陣列,值佇列,值堆疊等其他可能性。

是的,我們可以使用值陣列減少儲存空間和gas消耗。

如果你的Solidity智慧合約使用較小值的小陣列(例如使用者ID,角色等),則使用值陣列可能會消耗更少的gas。

當陣列被複制時,例如智慧合約或庫引數,值陣列將始終消耗少得多的gas。

參考資料

[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----------------------

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;