在以太坊上,代碼即法律,交易即金錢。每一筆智能合約的運行,都要根據複雜度消耗一筆GAS費(ETH)。那麼,智能合約solidity語言的編寫,不只要考慮安全,也要考慮語言的優化,以便高效便宜了。html
本文將從如下一些方面分析如何節約GAS的編程總結:git
1)如何在REMIX編譯器上分析GAS/GAS LIMIT等信息 2) 如何優化節省GAS費用的方法github
建立合約優化編程
存儲優化數組
變量排序優化安全
交易輸入數據優化微信
轉帳優化網絡
部署合約優化app
調用合約函數的成本優化編輯器
若是你想了解以太坊的帳戶、交易、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; } }
若是你將此合約複製並粘貼到Remix(http://remix.ethereum.org/)中,則能夠運行此合約。經過MIST或來自網站的MetaMask與此合同進行交互的方式相似。
讓咱們運行saveB(5)並查看日誌窗口中發生的狀況:
這兒有3個咱們感興趣的值:
GAS總量( "gas limit"): 3,000,000
交易費用 ("transaction cost"): 41642 gas
執行費用( "execution cost"): 20178 gas.
2.2 發送的GAS總量(Gas limit)
這兒顯示的"Gas limit"是發送的GAS總量,Value是發給目標地址的ETH值。這2處的值能夠被髮送交易的用戶修改。
2.3 交易成本(Transaction Cost)
交易成本,在Remix中顯示,是實際交易成本加上執行成本的混合。我認爲,這兒看起來有點誤導。
若是您使用數據字段發送交易,那麼交易包含一個基本成本和每一個字節的附加成本(GAS計價)。看看以太坊黃皮書(https://github.com/riversyang/ethereum_yellowpaper)的附錄列出了每種的GAS費用:
一塊兒來看看41642的交易成本是如何結合在一塊兒的。這是Remix在交易中自動發送的數據字段:
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用於交易。 讓咱們看看是否會增長。
Remix稱「交易成本」爲41642 gas,「執行成本」爲 20178 gas。而在Remix中,「交易成本」其實是交易成本加執行成本的總和。所以,若是咱們從交易成本中減去執行成本,咱們應該獲得21464 gas。
41642 (交易成本」) - 20178 (執行成本) = 21464 gas
剩下的結果21464 gas爲數據交易成本,同上計算公式。
2.4 執行成本(Execution Cost)
執行成本有點難以計算,由於發生了不少事情,輝哥試着告訴你合同執行時到底發生了什麼。
讓咱們深刻了解實際的事務並打開調試器。這能夠經過單擊事務旁邊的「調試」按鈕來完成。
能夠打開指令摺疊菜單和單步調試菜單。你將看到每一條指令以及每一個指令在該特定步驟中花費的GAS費用。
這裏看到的是全部以太坊彙編指令。所以,咱們知道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。輝哥在第一步打開它:
它已經從咱們發送的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美分。一個巨大的差別。
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個氣體。
建議:
根據上述原則,咱們能夠很容易地知道如何處理它。
很差的代碼例子:
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的存儲可能如:
更多優化參考文章《[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 部署合約優化
大部分的優化在編譯時候已經完成了。
問題:部署合同中是否包含註釋,是否會增長部署氣體? 回答:不,在編譯期間刪除了執行時不須要的全部內容。其中包括註釋,變量名和類型名稱。
而且能夠在此處文章(https://solidity.readthedocs.io/en/latest/miscellaneous.html#internals-the-optimizer)找到優化程序的詳細信息。
另外一種經過刪除無用代碼來減少大小的方法。例如:
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。如下是一些減小昂貴操做的方法。
A)使用短路規則(https://solidity.readthedocs.io/en/develop/types.html#booleans)
操做符 || 和&&適用常見的短路規則。這意味着在表達式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之一,由於它們要便宜得多。
具備固定長度老是節省GAS。也請參考這個問題(https://ethereum.stackexchange.com/questions/11556/use-string-type-or-bytes32)描述。
3.8.4 刪除無用的代碼能夠在執行時節省GAS
如前面在合同部署中所解釋的那樣刪除無用的代碼即便在執行函數時也會節省GAS。
3.8.5 在實現功能時不使用庫對於簡單的使用來講更便宜。
調用庫以得到簡單的用法可能代價高昂。若是功能在合同中實現簡單且可行,由於它避免了調用庫的步驟。兩種功能的執行成本仍然相同。
參考
(1)區塊鏈系列十九:Gas優化:https://magicly.me/blockchain-19-solidity-gas-optimization
(2)How to write an optimized (gas-cost) smart contract?:https://ethereum.stackexchange.com/questions/28813/how-to-write-an-optimized-gas-cost-smart-contract
(3)[Solidity] Optimize Smart Contract Gas Usage:https://medium.com/joyso/solidity-save-gas-in-smart-contract-3d9f20626ea4
(4)What exactly is the Gas Limit and the Gas Price in Ethereum:https://vomtom.at/what-exactly-is-the-gas-limit-and-the-gas-price-in-ethereum/
本文做者:HiBlock區塊鏈技術佈道羣-輝哥
原文發佈於簡書
加微信baobaotalk_com,加入技術佈道羣