Solidity 智能合約 Gas 優化技巧 - 函式名稱
說明
你也許不會想到,函式名稱也會對 gas 消耗造成影響,事實上,在最糟的情況下,甚至會有上千 gas 的差距。我們來看看下面程式:
1 | contract Test { |
上面程式執行 b()
會消耗 125 gas,接著改成下面:
1 | contract Test { |
這次執行 b()
變成消耗 147 gas,一樣的空函式怎麼消耗增加了呢?試著執行 a()
會發現只消耗 125 gas。原來在智能合約中,函式存在前後順序,排序越後面的會消耗越多,每差一個順位就會多 22 gas。此時你可能會想把它改成下面:
1 | contract Test { |
但結果卻沒有變化,b()
一樣消耗 147 gas,因為函式的排序是依據 Method ID。在這個例子中,他們的 Method ID 如下:
1 | a: 0x0dbe671f |
依據這個規則我們再多加了一個函式:
1 | contract Test { |
他們的 Method ID 如下:
1 | a: 0x0dbe671f |
排序之後我們可以得到 a < f < b 的順序,猜猜這次執行 b()
會花多少 gas?沒錯,答案是會消耗 169 gas。
參與排序的成員
事實上,所有公用成員都會被列入計算。所以除了函式之外,屬性也會被列入計算,也包含 constant。例如:
1 | contract Test { |
b()
會消耗147 gas。
當然繼承的公用屬性和函式也都會列入計算,所以當合約功能較多後,公用成員可能會有幾十個。假設有 50 個的話,最後一個函式就多了一千的消耗。
函式簽章
在說明 Method ID 產生規則之前,要先知道函式簽章的規則,函式簽章包含函式名稱和參數型別。例如:
1 | function func() public { |
上面兩個函式的簽章分別為:
1 | func() |
為了參與排序,屬性也會有簽章存在,再以同樣方式產生 Method ID。一般屬性視為沒有參數的函式,且只看名稱不看型別,無論是 uint256
還是 address
。mapping
則將 Key 視為第一個參數,例如:
1 | uint256 public a; |
簽章
1 | a() |
Method ID
Method ID 的產生規則如下:
1 | keccack256(函式簽章) 取前四 bytes |
所以上面的 a()
, b()
和 f()
函式簽章拿去做 keccak256
( 可利用我寫的線上 Keccak-256 工具 ) 可以得到下面結果:
1 | 0dbe671f81a573cff601b9c227db0ed2e5339d3e0a07edc45c42f315a9cb8f0f |
可以看出前四個 Bytes 正是 Method ID。
結論
在暸解了這些運作原理之後,我們可以做出以下兩種應對方式:
- 減少公用成員
- 將常用函式順位提高
第一點很容易理解與執行,而第二點我們可以透過命名的方式來達成。但是要人工的方式,命名到雜湊值剛好較低的方式並不容易。於是我寫了一個函式名稱優化小工具來處理這個問題。以上面的例子來說,假設 b()
是常用的函式,若想將它提升順位,可以輸入函式簽章 b()
,程式會自動找出一個 Method ID 前兩個 bytes 為零,結尾亂碼的新名稱:
1 | b_A6Q(): 0x0000e3fa |
另外由於 Method ID 會出現在 input data 中,所以除了排序提升的 gas 減少之外,還額外省了 128 gas ( 請參考上一篇 Solidity 智能合約 Gas 優化技巧的交易資料章節 )。