說明

你也許不知道,Solidity 宣告變數的順序也會影響 gas 的消耗。由於 EVM 的操作都是以 32 bytes 為單位進行,所以編譯器會嘗試在讀寫變數時,將小於 32 bytes 的變數打包成 32 bytes 一組來進行存取,以達到節省存取次數的目的。不過編譯器並沒有足夠聰明,能自動將合約的變數做最佳化的分組。他會將固定大小的變數,依序每 32 bytes 為一組。例如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 bytes ,可以做一次性的寫入,所以實際上執行一個 SSTORE ,消耗 20000 gas 。接著看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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, ec 會被分為一組, d 獨立為一組。同樣的 test() 造成兩次寫入,消耗 40000 gas 。最後再看一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
}

這和第一個例子不同的地方是,在 test() 中的 ab 寫入之後,做了一些其他的事情,最後才寫入 cd 。結果這次會造成兩次的寫入,因為在執行 do something 的時候,編譯器判斷為打包的動作已經結束,就送出寫入。不過由於只有一組資料,所以這個程式結果會產生一新增和一次修改,總共會消耗 25000 gas 。

優化策略

根據上面的特性,我們可以很容易的知道如何進行應對。

正確的排序與分組

將資料大小能剛好 32 bytes 的湊成一組,並且將時常一起更新的放一起。

不好

1
2
3
4
5
6
7
contract MyContract {
uint128 public hp;
uint128 public maxHp;
uint32 level;
uint128 public mp;
uint128 public maxMp;
}

1
2
3
4
5
6
7
contract MyContract {
uint128 public hp;
uint128 public mp;
uint128 public maxHp;
uint128 public maxMp;
uint32 level;
}

我們假設 hpmp 較常一起更新,而 maxHpmaxMp 較常一起更新,所以避免交錯排列。

一次性讀寫

如上面的例子,盡量一次性讀寫。

不好

1
2
3
4
5
function test() {
hp = 1;
// ... do something
mp = 2;
}

1
2
3
4
5
function test() {
// ... do something
hp = 1;
mp = 2;
}

這個特性在 struct 上也是有效。

延伸閱讀

上一篇 Solidity 智能合約 Gas 優化技巧 - 資料壓縮