1. 程式人生 > >區塊鏈100講:以太坊智慧合約solidity如何節省GAS費?

區塊鏈100講:以太坊智慧合約solidity如何節省GAS費?

image

1

摘要

在以太坊上,程式碼即法律,交易即金錢。每一筆智慧合約的執行,都要根據複雜度消耗一筆GAS費(ETH)。那麼,智慧合約solidity語言的編寫,不僅要考慮安全,也要考慮語言的優化,以便高效便宜了。

本文將從以下一些方面分析如何節約GAS的程式設計總結:

1)如何在REMIX編譯器上分析GAS/GAS LIMIT等資訊
2) 如何優化節省GAS費用的方法

  • 建立合約優化

  • 儲存優化

  • 變數排序優化

  • 交易輸入資料優化

  • 轉賬優化

  • 部署合約優化

  • 呼叫合約函式的成本優化

2

如何在REMIX編譯器上分析GAS/GAS LIMIT等資訊

如果你想了解以太坊的賬戶、交易、Gas和Gas Limit等基本概念資訊,可以閱讀文章《

以太坊的賬戶、交易、Gas和Gas Limit》。

如果你不瞭解以太坊智慧合約語言solidity編譯IDE環境REMIX,可以閱讀文章《Solidity語言編輯器REMIX指導大全》。

本章節聚焦在如何通過REMIX編譯器檢視GAS/GAS LIMIT等資訊。

2.1 簡單智慧合約樣例

以太坊指令執行主要依靠GAS。當你執行智慧合約時,它會消耗GAS。所以,如果你正在執行一個智慧合約,那麼每一條指令都要花費一定數量的GAS費。這有兩個因素,即您傳送的GAS數量和總區塊GAS上限(a total block gas limit)。

舉例來說,一個簡單的智慧合約,有一個儲存無符號整數256值的函式。
合約程式碼如下:

pragma solidity ^0.4.19;
contract A {
   uint b;    function saveB(uint _b) public {
       b = _b;
   }
}

讓我們執行saveB(5)並檢視日誌視窗中發生的情況:

image

這兒有3個我們感興趣的值:

  • GAS總量( “gas limit”): 3,000,000

  • 交易費用 (“transaction cost”): 41642 gas

  • 執行費用( “execution cost”): 20178 gas.

2.2 傳送的GAS總量(Gas limit)

image

這兒顯示的"Gas limit"是傳送的GAS總量,Value是發給目標地址的ETH值。這2處的值可以被髮送交易的使用者修改。

2.3 交易成本(Transaction Cost)

交易成本,在Remix中顯示,是實際交易成本加上執行成本的混合。我認為,這兒看起來有點誤導。

image

一起來看看41642的交易成本是如何結合在一起的。這是Remix在交易中自動傳送的資料欄位:

image

input_remix

這兒是 Data-Field:

> 0x348218ec0000000000000000000000000000000000000000000000000000000000000005

資料欄位是雜湊函式簽名的前4個位元組和32位元組填充引數的組合。我們快速手動計算。

函式簽名是saveB(uint256),如果我們用SHA3-256(或Keccak-256)雜湊函式,那麼我們得到:348218ec5e13d72ab0b6b9db1556cba7b0b97f5626b126d748db81c97e97e43d

如果我們取前4個位元組(提醒:1個位元組= 8位= 2個十六進位制字元.1個十六進位制字元= 4 bit = 0-15 = 0000到1111 = 0x0到0xF),然後我們得到348218ec。讓我們0x在前面新增,我們得到0x348218ec。引數是一個256位的無符號整數,即32個位元組。這意味著它將整數“5”填充到32個位元組,換句話說,它將在數字前面新增63個零:
0000000000000000000000000000000000000000000000000000000000000005。

從以太坊黃皮書上可以獲得參考:

  • 每筆交易都有21000 GAS支付

  • 為交易的每個非零位元組資料或程式碼支付68 GAS

  • 為交易的每個零位元組資料或程式碼支付4 GAS

計算一下:
348218ec 是4個位元組的資料,顯然是非零的。
0000000000000000000000000000000000000000000000000000000000000005是31個位元組的零資料和1個位元組的非零資料的混合。

這使得總共5個位元組的非零資料和31個位元組的零資料。

(5 non-zero-bytes * 68 gas) + (31 zero-bytes * 4 gas) = 340 + 124 = 464   gas

對於我們的輸入資料,我們必須支付464 GAS。除此之外,我們還要支付 21000 GAS,這是每筆交易支付的。因此總共需要21464用於交易。
讓我們看看是否會增加。

image

Remix稱“交易成本”為41642 gas,“執行成本”為 20178 gas。而在Remix中,“交易成本”實際上是交易成本加執行成本的總和。因此,如果我們從交易成本中減去執行成本,我們應該得到21464 gas。

41642 (交易成本”) - 20178 (執行成本) = 21464 gas

剩下的結果21464 gas為資料交易成本,同上計算公式。

2.4 執行成本(Execution Cost)

執行成本有點難以計算,因為發生了很多事情,輝哥試著告訴你合同執行時到底發生了什麼。

讓我們深入瞭解實際的事務並開啟偵錯程式。這可以通過單擊事務旁邊的“除錯”按鈕來完成。

image

可以開啟指令摺疊選單和單步除錯選單。你將看到每一條指令以及每個指令在該特定步驟中花費的GAS費用。

image

這裡看到的是所有以太坊彙編指令。因此,我們知道Solidity可以歸結為EVM Assembly。這是礦工實際執行的智慧合約執行看起來的實際情況。來看看前兩個指令:

PUSH1 60
PUSH1 40

這意味著除了將值60和40推入堆疊之外別無其他。顯然還有很多事情要做,你可以通過在單步偵錯程式中移動藍色滑塊來完成它們的工作。

根據以太坊黃皮書將每個指令所需的確切氣體量彙總在一起,以便將值5寫入儲存:

GAS Instruction

3   000 PUSH1 60

3   002 PUSH1 40

12  004 MSTORE

3   005 PUSH1 04

2   007 CALLDATASIZE

3   008 LT

3   009 PUSH1 3f

10  011 JUMPI

3   012 PUSH1 00

3   014 CALLDATALOAD

3   015 PUSH29 0100000000000000000000000000000000000000000000000000000000

3   045 SWAP1

5   046 DIV

3   047 PUSH4 ffffffff

3   052 AND

3   053 DUP1

3   054 PUSH4 348218ec

3   059 EQ

3   060 PUSH1 44

10  062 JUMPI

1   068 JUMPDEST

2   069 CALLVALUE

3   070 ISZERO

3   071 PUSH1 4e

10  073 JUMPI

3   074 PUSH1 00

3   076 DUP1

1   078 JUMPDEST

3   079 PUSH1 62

3   081 PUSH1 04

3   083 DUP1

3   084 DUP1

3   085 CALLDATALOAD

3   086 SWAP1

3   087 PUSH1 20

3   089 ADD

3   090 SWAP1

3   091 SWAP2

3   092 SWAP1

2   093 POP

2   094 POP

3   095 PUSH1 64

8   097 JUMP

1   100 JUMPDEST

3   101 DUP1

3   102 PUSH1 00

3   104 DUP2

3   105 SWAP1

20000   106 SSTORE

2   107 POP

2   108 POP

8   109 JUMP

1   098 JUMPDEST

0   099 STOP

合計為20178 GAS費。

2.5 GAS上限(Gas Limit)

所以,以太坊區塊鏈上的每一條指令都會消耗一些GAS。如果你要將值寫入儲存,則需要花費很多。如果你只是使用堆疊,它的成本會低一些。但基本上所有關於EVM的指令都需要GAS。這意味著智慧合約只能做有限的事情,直到傳送的GAS用完為止。在樣例這種情況下,我們傳送了300萬 GAS費。

當您返回REMIX的單步偵錯程式,點選第一步時,您會看到每個步驟剩餘多少GAS。輝哥在第一步開啟它:

image

它已經從我們傳送的300萬(從3,000,000 - 21464 = 2,978,536)中扣除的交易成本開始。(說明:21464是之前2.3章節執行的資料執行成本。)

一旦此計數器達到零,那麼合約執行將立即停止,所有儲存的值將被回滾,你將獲得“Out of Gas”異常告警。

2.6 區塊GAS上限(Block Gas Limit)

除了通過交易設定的氣Gas Limit外,還有一個所謂的“區塊上限”。這是你可以傳送的最大GAS量。目前,在Main-Net,該值大概為8M左右。

2.7 GAS退款(Gas Refund)

Gas Limit有一個好處:你不必自己計算它。如果你向合約傳送8M的GAS,它耗盡41642 GAS,可以退還其餘部分。因此,傳送遠遠超過必要的GAS總會節省下來的,其餘的將自動退還到你的賬號地址。

2.8 GAS價格(Gas Price)

GAS價格決定了交易在能否被包含在下一個被挖出的區塊中。

當你傳送交易時,你可以激勵礦工接下來處理您的交易。這種激勵就是GAS PRICE。礦工一旦挖出新區塊,也會將交易納入該區塊。哪些交易被納入下一個區塊是由礦工確定的 - 但他很可能將GAS PRICE從高到低排序。

假設有15筆未完成的交易,但只有12筆交易可以進入下一個區塊。5個20 Gwei,5個15 Gwei和5個 5Gwei的GAS PRICE。礦工很可能按此順序選擇交易:5 * 20 + 5 * 15 + 2 * 5 Gwei並將它們合併到下一個挖掘區塊中。

因此,GAS Limit基本上決定了以太坊虛擬機器可以執行的指令數量,而GAS Price決定了礦工選擇此交易的可能性。

大多數錢包將標準GAS Price設定為20Gwei左右(0.00000002 ETH)。如果您正在執行上述合約,那麼您將支付約60-70美分(美元分),當前匯率為1 ETH = 800美元。所以它根本不便宜。

幸運的是,在網路擁塞期間,您只需要更高的GAS PRICE,那是因為許多人嘗試同時傳送交易。如果網路沒有擁擠,那麼您不需要支付這麼多GAS。EthGasStation網站(https://ethgasstation.info)評估目前的交易價格為4 Gwei足夠 。所以,憑藉這個小功能,只需要4 Gwei的GAS,它將是16美分左右,而不是65美分。一個巨大的差異。

3

如何優化節省GAS費用的方法

GAS消耗可參考以下兩個表:

  • 表格1

  • 表2

下面提供一下優化GAS消耗的方法。

3.1 建立合約

建立合約對應CREATE和CODECOPY這兩條指令。在合約中建立另一個空合約消耗42,901個GAS(總共64,173個GAS)。如果直接部署空白合約,共有68,653個GAS。

如果包含實施,可能會有數十萬甚至數百萬的GAS。它應該是所有指令中最昂貴的。如果建立多個合約例項,則GAS消耗可能很大。

建議: 避免將合約用作資料儲存。

不好的程式碼實現:

contract User {

   uint256 public amount; 

 bool public isAdmin;

  function User(uint256 _amount, bool _isAdmin) {

    amount = _amount;

    isAdmin = _isAdmin;

  }

 }

好的程式碼實現:

contract MyContract {
 mapping(address => uint256) amount;
 mapping(address => bool) isAdmin;
}

另一種OK的程式碼實現:

contract MyContract {

  struct {
   uint256 amount;

    bool isAdmin;
 }
mapping(address => User) users;
}

3.2 儲存

對應於SSTORE指令。儲存新資料需要20,000 GAS。修改資料需要5000 GAS。一個例外是將非零變數更改為零。我們稍後會討論這個問題。

建議: 避免重複寫入,最好一次在最後儘可能多地寫入到儲存變數。

不好的程式碼樣例:

uint256 public count;

// ...

for (uint256 i = 0; i < 10; ++i) {

  // ...  
 ++count;
}

好的程式碼樣例:

for (uint256 i = 0; i < 10; ++i) {

  // ...

}
count += 10;

3.3 變數排序對GAS的影響

你可能不知道變數宣告的順序也會影響Gas的消耗。

由於EVM操作都是以32位元組為單位執行的,因此編譯器將嘗試將變數打包成32位元組集進行訪問,以減少訪問時間。

但是,編譯器不夠智慧,無法自動優化變數分組。它將靜態大小的變數分組為32個位元組的組。例如:

contract MyContract {

  uint64 public a;

  uint64 public b;

  uint64 public c;

  uint64 public d;

function test() {

    a = 1;

    b = 2;

    c = 3;

    d = 4;

  }

 }

執行test()時,看起來已經儲存了四個變數。由於這四個變數之和恰好是32個位元組,因此實際執行了一個SSTORE。這隻需要20,000 GAS。

再看下一個例子:

contract MyContract { 

  uint64 public a;

  uint64 public b;

  byte e;

  uint64 public c;

  uint64 public d;

function test() {

    a = 1;

    b = 2;

    c = 3;

    d = 4;

  }

 }

中間插入了另一個變數,結果造成a,b,e和c會被分為一組,d獨立為一組。同樣的test()造成兩次寫入,消耗40000 Gas。

最後再看一個例子:

contract MyContract {

  uint64 public a;

  uint64 public b;

  uint64 public c;

  uint64 public d;

function test() {

    a = 1;

    b = 2;

    // ... do something

    c = 3;

    d = 4;

  }

 }

**這與第一個例子的區別在於:**在儲存a和b之後,完成了其他事情,最後儲存了c和d。結果這次將導致兩次寫入。因為當執行“執行某事”時,編譯器確定打包操作已結束,然後傳送寫入。但是,由於第二次寫入是同一組資料,因此認為它是被修改的。將消耗總共25,000個氣體。

建議:

根據上述原則,我們可以很容易地知道如何處理它。

  • 正確的排序和分組
    將資料大小分組為32個位元組,並將通常同時更新的變數放在一起。

不好的程式碼例子:

contract MyContract {
 uint128 public hp;
 uint128 public maxHp;
 uint32 level;
 uint128 public mp;
 uint128 public maxMp;
}

好的例子:

contract MyContract {
 uint128 public hp;
 uint128 public mp;
 uint128 public maxHp;
 uint128 public maxMp;
 uint32 level;
}

這裡我們假設hp和mp更頻繁地更新,並且maxHp和maxMp更頻繁地一起更新。

  • 儘量一次訪問

不好的程式碼例子:

function test() {
   hp = 1;

    // ... do something
   mp = 2;
 }

好的例子:

function test() {

    // ... do something
   hp = 1;
   mp = 2;
 }

這個規則在struct上是一樣的。

3.4 交易輸入資料

合約交易的基本氣體是21,000。輸入資料為每位元組68個GAS,如果位元組為0x00則為4個GAS。

例如,如果資料為0x0dbe671f,則氣體為68 * 4 = 272; 如果是0x0000001f,它是68 * 1 + 4 * 3 = 80。

由於所有引數都是32位元組,因此當引數為零時,氣體消耗最小。它將是32 * 4 = 128。最大值如下:

n * 68 +(32-n)* 4 的位元組數 (n:引數)

例如,32位元組輸入引數的最大GAS為2,176 (3268 = 2176)。輸入引數為地址,地址是20個位元組,因此它是1,408 (2068+(32-20)*4 = 1408)。

建議: 可以通過更改排序來節省GAS消耗。

例如EtherScan有下一段交易記錄:

Function: trade(address tokenGet, uint256 amountGet, address tokenGive, uint256 amountGive, uint256 expires, uint256 nonce, address user, uint8 v, bytes32 r, bytes32 s, uint256 amount) ***

MethodID: 0x0a19b14a

[0]:0000000000000000000000000000000000000000000000000000000000000000

[1]:000000000000000000000000000000000000000000000000006a94d74f430000

[2]:000000000000000000000000a92f038e486768447291ec7277fff094421cbe1c

[3]:0000000000000000000000000000000000000000000000000000000005f5e100

[4]:000000000000000000000000000000000000000000000000000000000024cd39

[5]:00000000000000000000000000000000000000000000000000000000e053cefa

[6]:000000000000000000000000a11654ff00ed063c77ae35be6c1a95b91ad9586e

[7]:000000000000000000000000000000000000000000000000000000000000001c

[8]:caa3a70dd8ab2ea89736d7c12c6a8508f59b68590016ed99b40af0bcc2de8dee

[9]:26e2347abfba108444811ae5e6ead79c7bd0434cf680aa3102596f1ab855c571

[10]:000000000000000000000000000000000000000000000000000221b262dd8000

所有引數都是256位,無論型別是byte32,address還是uint8。所以左邊的大多數引數都有大量的“0”是未使用的位。很容易想到使用這些“空間”。

例如可以把tokenGive的高位位元組用於存放下面嗎一些變數,把命名改為uint256 tokenSellWithData。

nonce  - > 40位
takerFee  - > 16位
makerFee  - > 16位
uint256 joyPrice  - > 28位
isBuy  - > 4位(實際上,1位就足夠了。只是為了方便呈現文件)

假如上面變數的值分別為:

nonce: 0181bfeb
takerFee: 0014
makerFee: 000a
joyPrice: 0000000
isBuy: 1

那麼tokenSellWithData的儲存可能如:

image

更多優化參考文章《[Solidity] Compress input in smart contract》。

3.5 轉賬

Call, send 和transfer 函式對應於CALL指令。基本消耗是7,400 GAS。事實上,消費將近7,600 GAS。值得注意的是,如果轉賬到一個從未見過的地址,將額外增加25,000個GAS。

沒有額外的消耗樣例:

function withdraw(uint256 amount){ 
 msg.sender.transfer(amount); 
}

可能會有額外的消耗樣例(receiver引數未被使用,多餘引數):

function withdrawTo(uint256 amount, address receiver) {
 receiver.transfer(amount);
}

3.6 其他命令

3.6.1 ecrecover

對應CALL指令。此功能將消耗3700 GAS。

3.6.2呼叫外部合約

呼叫外部合約執行EXTCODESIZE和CALL指令。基本消耗1400 GAS。除非必要,否則不建議拆分多個合同。可以使用多個繼承來管理程式碼。

3.6.3事件

對應於LOG1指令。沒有引數的事件是750 GAS。理論上每個附加引數將增加256個GAS,但事實上,它會更多。

3.6.4雜湊

你可以使用智慧合約中的幾個內建雜湊函式:keccak256,sha256和ripemd160。引數越多,消耗的氣體越多。耗氣量:ripemd160> sha256> keccak256。因此,如果沒有其他目的,建議使用keccak256函式。

3.7 部署合約優化

大部分的優化在編譯時候已經完成了。

問題:部署合同中是否包含註釋,是否會增加部署氣體?
回答:不,在編譯期間刪除了執行時不需要的所有內容。其中包括註釋,變數名和型別名稱。

另一種通過刪除無用程式碼來減小大小的方法。例如:

1 function p1 ( uint x ){ 
2    if ( x > 5)

3     if ( x*x < 20)

4        XXX }

在上面的程式碼中,第3行和第4行永遠不會執行,並且可以避免這些型別的無用程式碼仔細通過合同邏輯,這將減少智慧合約的大小。

3.8 呼叫合約函式的成本優化

當呼叫合約額的功能時,為了執行功能,它需要GAS。因此,優化使用較少GAS的功能非常重要。在考慮每個合約時時,可以採用多種不同的方式。這裡有一些可能在執行過程中節省GAS的方法。

3.8.1 減少昂貴的操作

昂貴的操作是指一些需要更多GAS值的操作碼,例如SSTORE。以下是一些減少昂貴操作的方法。

操作符 || 和&&適用常見的短路規則。這意味著在表示式f(x)|| g(y)中,如果f(x)的計算結果為真,即使它有副作用,也不會評估g(y)。

因此,如果邏輯操作包括昂貴的操作和低成本操作,那麼以昂貴的操作可以短路的方式安排將在一些執行中減少GAS。

如果f(x)是便宜的並且g(y)是昂貴的,邏輯運算程式碼(便宜的放在前面):

  • OR : f(x) || g(y)

  • AND: f(x) && g(y)

如果短路,將節省更多的氣體。

  • f(x)與g(y)安排AND操作相比,如果返回錯誤的概率要高得多,f(x) && g(y)可能會導致通過短路節省更多的氣體。

  • f(x)與g(y)安排OR運算相比,如果返回真值的概率要高得多,f(x) || g(y)可能會導致通過短路節省更多氣體。

B)迴圈中昂貴的操作

不好的程式碼,例如:

uint sum = 0;

 function p3 ( uint x ){

     for ( uint i = 0 ; i < x ; i++)
        sum += i; }

在上面的程式碼中,由於sum每次在迴圈內讀取和寫入儲存變數,所以在每次迭代時都會發生昂貴的儲存操作。這可以通過引入如下的區域性變數來節省GAS來避免。

好的程式碼,例如:

uint sum = 0; 

function p3 ( uint x ){
    uint temp = 0;

     for ( uint i = 0 ; i < x ; i++)
        temp += i; }
    sum += temp;

3.8.2 其他迴圈相關模式

迴圈組合,不好的程式碼樣例:

function p5 ( uint x ){

    uint m = 0;

    uint v = 0;

    for ( uint i = 0 ; i < x ; i++) //loop-1

        m += i;

    for ( uint j = 0 ; j < x ; j++) /loop-2

        v -= j; }

loop-1和loop-2可以組合,可以節省燃氣。

好的程式碼樣例:

function p5 ( uint x ){
   uint m = 0;
   uint v = 0;

    for ( uint i = 0 ; i < x ; i++) //loop-1
      m += i;
      v -= j; }

3.8.3 使用固定大小的位元組陣列

可以使用一個位元組陣列作為byte [],但它在傳入呼叫時浪費了大量空間,每個元素31個位元組。最好使用bytes。

根據經驗,對任意長度的原始位元組資料使用 bytes識別符號,對任意長度的字串(UTF-8)資料使用 string識別符號。如果您可以將長度限制為特定的位元組數,請始終使用bytes1到bytes32之一,因為它們要便宜得多。

3.8.4 刪除無用的程式碼可以在執行時節省GAS

如前面在合同部署中所解釋的那樣刪除無用的程式碼即使在執行函式時也會節省GAS。

3.8.5 在實現功能時不使用庫對於簡單的使用來說更便宜。

呼叫庫以獲得簡單的用法可能代價高昂。如果功能在合同中實現簡單且可行,因為它避免了呼叫庫的步驟。兩種功能的執行成本仍然相同。

4

參考

參考

本文作者:HiBlock區塊鏈技術佈道群-輝哥

原文釋出於簡書

加微信baobaotalk_com,加入技術佈道群

以下是我們的社群介紹,歡迎各種合作、交流、學習:)

image