在 Solidity 中 ++i 為什么比 i++ 更省 Gas?
前言
作為一個初學者,“在 Solidity 中 ++i 為什么比 i++ 更省 Gas?” 這個問題始終在每個寂靜的深夜困擾著我。也曾在網上搜索過相關問題,但沒有得到根本性的解答。最終決定扒拉一下它們的字節碼,從較為底層的層面看一下它們的差別究竟在哪里。
Solidity 代碼選擇
Solidity 版本選用了 0.8.4 (隨手選的沒啥說法),代碼選用了兩個簡單的合約,分別是 Test(i++) 和 Test2(++i) ,兩個合約都有一個全局變量 i ,修改值的時候從 storage 中取值然后進行修改。選擇全局變量的這個形式是想要通過定位 SLOAD 和 SSTORE 兩個比較有特征的操作碼來進行比較。當然,這個只是我知識淺薄的腦瓜子想出來的一個代碼形式,如果有更好的更直接明了的代碼形式也十分歡迎各位師傅提出來交流交流。
Solidity Code:
pragma solidity 0.8.4;
contract Test{
uint256 i = 0;
// 0xfb5343f3
function t1() public {
i++;
}
}
contract Test2{
uint256 i = 0;
// 0xbaf2f868
function t2() public {
++i;
}
}
RuntimeCode 分析
Solidity 代碼經過編譯以后,截取兩個合約的 RuntimeCode,注意是 RuntimeCode 而不是包括 CreationCode 的所有代碼。否則在后面看地址轉跳的時候會對不上號。
OK,拿到了字節碼。我們簡單地從長度比較上面就可以看出兩個合約的字節碼是不一樣的,但是具體怎么不一樣,不一樣發生在什么地方,就需要進行進一步的分析。
Test Contract RuntimeCode:
6080604052348015600f57600080fd5b506004361060285760003560e01c8063fb5343f314602d575b600080fd5b60336035565b005b6000808154809291906045906056565b9190505550565b6000819050919050565b6000605f82604c565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff821415608f57608e609a565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fdfea264697066735822122036565a2f31dfc56ec3a1576d52790574b00eea2721561ecdc6581a7c865a382564736f6c63430008040033
Test2 Contract RuntimeCode:
6080604052348015600f57600080fd5b506004361060285760003560e01c8063baf2f86814602d575b600080fd5b60336035565b005b60008081546041906054565b91905081905550565b6000819050919050565b6000605d82604a565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff821415608d57608c6098565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fdfea2646970667358221220a395400661088056760f04d1c0d531d36c787fe81f654b35987819f5b3a4e36564736f6c63430008040033
Operation Code 分析
當然也不至于手撕字節碼,所以下一步就是把字節碼翻譯成操作碼(Operation Code)來分析。推薦去 dedaub(https://library.dedaub.com/decompile)上面反編譯一下。由于OP Code太長,考慮到文章篇幅就不貼上來了,朋友們可以自己去操作一下。
但是!我根據 OP Code 做了兩個圖,去掉了一些不重要的結束分支,保留了主干。其中標有紅藍兩種顏色的代碼塊表示此處出現不同的操作。其余沒有標記顏色的代碼塊操作基本相同。(說基本相同是因為紅藍顏色代碼塊的長度不同,導致整體的地址發生了一些偏移。操作是一樣的,只是跳轉的地址相應地存在一點偏移。)而且剛剛好, SLOAD 和 SSTORE 兩個操作碼正好處于這兩個不一樣的代碼塊中,那說明 i++ 和 ++i 這兩個操作在取值后和賦值前這兩個地方會出現差異。


現在兩個合約的不同點已經找出來了。接下來我們把標有顏色的代碼塊取出來,結合運行到此處時堆棧的變化,進行進一步的對比分析。
堆棧的分析工具可以用 evm.codes (https://www.evm.codes/playground),把字節碼貼上去,配置好函數選擇器就可以單步調試了。但是這里還有一個問題,就是用 remix 的 debug 調試的時候操作碼的地址與反編譯出來的地址對不上號,用 evm.codes 倒是完美對上。希望有頭緒的師傅可以指點一下這到底是怎么回事。
接下來看對比圖。為了更好地分析堆棧的變化,選擇了當 i = 1 時的狀態來進行 +1 操作對比。這是為了避免當 i = 0 時讀取進來的 0 值不夠顯眼,容易與堆棧中的其他 0 值混淆。0x3a Stack 代表當代碼運行完 0x3a 這個位置的操作碼后,堆棧 Stack 中的情況。

先看左邊,當 i = 1 時,進行 i++ 操作。從左上角的代碼塊可以看出,0x3a 處的 SLOAD 指令從 solt 中取出 i 的值存放在堆棧頂。然后 0x3b 處的 DUP1 將棧頂 i 的值進行復制。隨后的幾個 SWAP 操作把復制出來的值交換到堆棧的第 4 位處。隨后程序運行到左下的代碼塊中。當程序運行到 0x48 處時,此時棧頂的 0 為 i 的 slot 位置,堆棧第 2 位為 i++ 后的值,堆棧第 3 位是在 0x3b 處 i 進行 +1 操作前復制出來的 i 值。隨后 0x49 處的 SSTORE 操作將 2 存放到 solt 0 中。
然后右邊,當 i = 1 時,進行 ++i 操作。從右上角的代碼塊可以看出,0x3a 處的 SLOAD 指令從 solt 中取出 i 的值存放在堆棧頂。隨后程序運行到左下的代碼塊中。當程序運行到 0x44 處,此時棧頂的 0 為 i 的 slot 位置,堆棧第 2 位為 i++ 后的值。隨后 0x45 處的 DUP2 操作將堆棧第 2 位的 2 值復制并存放的棧頂。隨后 0x46 的 SWAP1 操作將其堆棧 1, 2 位的值調換。此時堆棧的第 3 位是 i 進行 +1 后的值。0x47 處的 SSTORE 指令將 2 值存放到 solt 0 中。
上面的解釋可能稍微有點繞,有簡單版的。
簡單的理解可以把 i++ 的操作類似于:
uint256 j = i;
i = i + 1;
// store 'i', keeep 'j'
因為我們可以通過堆棧中的情況看到,在執行完 0x3a: SLOAD 這個操作后,馬上執行 0x3b: DUP1 對取出來的 i 值進行一個復制,就相當于 uint256 j = i;,而隨后對 i 的值進行 +1 操作,并不影響復制出來 j 的值。當執行完 0x49: SSTORE 后,堆棧頂的 1 值就是 0x3b: DUP1 復制出來的 j。
而 ++i 的操作則類似于:
i = i + 1;
// store 'i', keep 'i' copy value
當代碼運行到 0x44 處時,棧頂的 0 為 i 的 slot 位置,堆棧第 2 位為 i++ 后的值。然后 0x45: DUP2 對 i 值進行了復制,利用 0x46: SWAP1 調整完順序以后執行 0x47: SSTORE 保存。此時,棧頂的 2 值就是 0x45: DUP2 復制出來的進行過 +1 操作后的 i 值。
好!話說回來!
那么到底為什么在 Solidity 中 ++i 為什么比 i++ 更省 Gas 呢?我們看代碼對比(比較圖中的黃色代碼)可以看出,當執行 i++ 的時候,要比 ++i 多執行一個 SWAP2 和一個 SWAP3,而每個 SWAP* 固定的消耗為 3 gas。

所以可以得出,以本文的案例 Test 合約與 Test2 合約為例,執行一遍 i++ 要比 ++i 多消耗 6 gas,如下圖所示:


就是這樣。
后記
誒終于把這篇文章寫出來了,疑問是一直的疑問,但是搜出來的答案也流于表面沒有具體講明白。然后就自己分析著玩吧,分析之前也不知道能不能搞得懂。但在分析的過程中還是挺興奮的,一直慢慢摸索,也踩了很多坑(有些坑現在還沒搞明白)。最終還是把想知道的東西弄明白了,也希望能夠把它分享給你~能看到最后的你很棒哦:D!然后,也是希望自己能夠繼續抱有熱情繼續學習下去吧,在搗弄這些玩意的時候確實能夠將自己從現實的失落中暫時的抽離出來。最后,下一篇文章也不知道是什么時候了,想寫,但是也不知道寫什么,總覺得自己沒啥東西,還是要多學點東西吧寫個博客都把自己寫得江郎才盡了。

浙公網安備 33010602011771號