作為一個初學者,「在 Solidity 中 ++i 為什麼比 i++ 更省 Gas?」 這個問題始終在每個寂靜的深夜困擾著我。也曾在網上搜尋過相關問題,但沒有得到根本性的解答。最終決定扒拉一下它們的位元組碼,從較為底層的層面看一下它們的差別究竟在哪裡。
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;
}
}
Solidity 程式碼經過編譯以後,擷取兩個合約的 RuntimeCode,注意是 RuntimeCode 而不是包括 CreationCode 的所有程式碼。否則在後面看地址轉跳的時候會對不上號。
OK,拿到了位元組碼。我們簡單地從長度比較上面就可以看出兩個合約的位元組碼是不一樣的,但是具體怎麼不一樣,不一樣發生在什麼地方,就需要進行進一步的分析。
Test Contract RuntimeCode:
6080604052348015600f57600080fd5b506004361060285760003560e01c8063fb5343f314602d575b600080fd5b60336035565b005b6000808154809291906045906056565b9190505550565b6000819050919050565b6000605f82604c565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff821415608f57608e609a565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fdfea264697066735822122036565a2f31dfc56ec3a1576d52790574b00eea2721561ecdc6581a7c865a382564736f6c63430008040033
Test2 Contract RuntimeCode:
6080604052348015600f57600080fd5b506004361060285760003560e01c8063baf2f86814602d575b600080fd5b60336035565b005b60008081546041906054565b91905081905550565b6000819050919050565b6000605d82604a565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff821415608d57608c6098565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fdfea2646970667358221220a395400661088056760f04d1c0d531d36c787fe81f654b35987819f5b3a4e36564736f6c63430008040033
當然也不至於手撕位元組碼,所以下一步就是把位元組碼翻譯成操作碼(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!然後,也是希望自己能夠繼續抱有熱情繼續學習下去吧,在搗弄這些玩意的時候確實能夠將自己從現實的失落中暫時的抽離出來。最後,下一篇文章也不知道是什麼時候了,想寫,但是也不知道寫什麼,總覺得自己沒啥東西,還是要多學點東西吧寫個部落格都把自己寫得江郎才盡了。