如何使用值陣列(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   Description
uint128[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 values
uint7[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值陣列

不足為奇的是,記憶體氣體消耗可以忽略不計,而儲存氣體消耗是巨大的——尤其是第一次用非零值(大藍磚)寫入儲存位置時。隨後使用該儲存位置消耗的天然氣要少得多。

· uint8a32值陣列

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

令人驚訝的是,uint8a32值陣列消耗的氣體量只有uint8 [32] Solidity固定陣列的一半。在uint8 [16]和uint8 [4]的情況下,相關的氣體消耗相應降低。這是因為值陣列程式碼必須讀取和寫入值才能設定元素值,而uint8 []只需寫入值。

這是這些在EVM儲存空間中進行比較的方式:

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

· uint1a256值陣列

EVM記憶體空間中固定bool[]陣列與uint12256值陣列的比較:

顯然,分配bool陣列的氣體消耗占主導地位。

EVM儲存空間中的相同比較:

簡單的測試涉及bool [256]和bool [64]的2個儲存插槽,因此耗氣量相似。Bool [32]和uint1a256僅接觸一個儲存插槽。

分包智慧合約和庫的引數

毫不奇怪,最大的氣體消耗是為分包智慧合約或庫函式提供陣列引數。

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

其他可能性

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

 結論

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

我已經揭示了其他可能性,例如固定多值陣列,動態值陣列,值佇列,值堆疊等。

是的,我們可以使用Value Array減少儲存空間和氣體消耗。

如果您的Solidity智慧合約使用較小值的小陣列(用於使用者ID,角色等),則使用“價值陣列”可能會消耗更少的汽油。

複製陣列的位置,例如對於分包智慧合約或庫,值陣列將始終消耗少得多的氣體。

免責聲明:

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

推荐阅读

;