【译】Diving Into The Ethereum VM Part 2 — How I Learned To Start Worrying And Count The Storage Cost
該合約歸結(jié)為sstore指令的調(diào)用:
// a = 1 sstore(0x0,0x1)- EVM將值0x1存儲在存儲位置0x0 。
- 每個存儲位置可以存儲32個字節(jié)(或256位)。
在本文中,我們將開始研究Solidity如何使用32個字節(jié)的塊來表示更復(fù)雜的數(shù)據(jù)類型,如結(jié)構(gòu)和數(shù)組。 我們還會看到如何優(yōu)化存儲,以及優(yōu)化如何失敗。
在典型的編程語言中,理解數(shù)據(jù)類型如何在如此低層次上表現(xiàn)出來并不是非常有用。 在Solidity(或任何EVM語言)中,此知識至關(guān)重要,因為存儲訪問非常昂貴:
- sstore成本為20000 sstore ,或比基本算術(shù)指令貴?5000倍。
- sload需要200 sload天然氣,或比基本算術(shù)指令貴?100倍。
而通過“成本”,我們在這里談?wù)撜驽X,而不僅僅是毫秒的表現(xiàn)。 運(yùn)行和使用您的合同的成本很可能由sstore和sstore支配!
Parsecs在Parsecs磁帶上
圖靈機(jī)。 來源: http : //raganwald.com/構(gòu)建通用計算機(jī)需要兩個基本要素:
EVM匯編代碼跳轉(zhuǎn),EVM存儲提供無限的內(nèi)存。 這對一切都是足夠的,包括模擬一個運(yùn)行以太坊版本的世界,它本身模擬一個運(yùn)行以太坊的世界......
潛入微電池合同的EVM存儲就像一個無限的自動收報機(jī)磁帶,磁帶的每個插槽都可容納32個字節(jié)。 喜歡這個:
[32個字節(jié)] [32個字節(jié)] [32個字節(jié)] ...我們將看到數(shù)據(jù)如何存在于無限大的磁帶上。
磁帶的長度為2÷5?,或每個合約約10??個存儲插槽。 可觀測宇宙的粒子數(shù)是10??。 大約1000個合約足以容納所有這些質(zhì)子,中子和電子。 不要相信營銷炒作,因為它比無限更短。空白磁帶
存儲最初是空白的,默認(rèn)為零。 擁有無限磁帶并不需要花費(fèi)任何東西。
我們來看一個簡單的合約來說明零價值行為:
雜注扎實0.4.11; 合同C { uint256 a; uint256 b; uint256 c; uint256 d; uint256 e; uint256 f; 函數(shù)C(){ f = 0xc0fefe; } }存儲中的布局很簡單。
- 位置為0x0的變量a
- 位置為0x1的變量b
- 等等…
關(guān)鍵問題:如果我們只使用f ,我們?yōu)閍,b,c,d,e支付多少錢?
讓我們編譯看看:
$ solc --bin --asm --optimize c-many-variables.sol大會:
// sstore(0x5,0xc0fefe) TAG_2: 0xc0fefe 0x5的 sstore因此,存儲變量聲明不需要任何費(fèi)用,因為不需要初始化。 Solidity為該商店變量保留一個位置,并且只有當(dāng)您存儲某些內(nèi)容時才支付。
在這種情況下,我們只支付存儲到0x5 。
如果我們手工編寫程序集,我們可以選擇任何存儲位置而不必“擴(kuò)展”存儲:
//寫入任意位置 sstore(0xc0fefe,0x42)讀零
您不僅可以在存儲的任何地方寫字,還可以立即從任何地方讀取。 從未初始化的位置讀取僅返回0x0 。
讓我們看看一個從未初始化位置讀取的合約:
雜注扎實0.4.11; 合同C { uint256 a; 函數(shù)C(){ a = a + 1; } }編譯:
$ solc --bin --asm --optimize c-zero-value.sol大會:
TAG_2: // sload(0x0)返回0x0 為0x0 DUP1 SLOAD // a + 1; 其中一個== 0 為0x1 加 // sstore(0x0,a + 1) swap1 sstore請注意,生成從未初始化位置sload代碼是有效的。
然而,我們可以比Solidity編譯器更聰明。 由于我們知道tag_2是構(gòu)造函數(shù),并且從未寫入過,所以我們可以用0x0替換sload序列以節(jié)省5000個氣體。
代表結(jié)構(gòu)
我們來看看我們的第一個復(fù)雜數(shù)據(jù)類型,一個有6個字段的結(jié)構(gòu)體:
雜注扎實0.4.11; 合同C { 結(jié)構(gòu)元組{ uint256 a; uint256 b; uint256 c; uint256 d; uint256 e; uint256 f; } 元組t; 函數(shù)C(){ tf = 0xC0FEFE; } }存儲中的布局與狀態(tài)變量相同:
- 位置0x0的字段ta
- 位置為0x1的字段tb
- 等等…
像以前一樣,我們可以直接寫入tf而無需支付初始化費(fèi)用。
讓我們編譯一下:
$ solc --bin --asm --optimize c-struct-fields.sol我們看到完全相同的組件:
TAG_2: 0xc0fefe 0x5的 sstore固定長度數(shù)組
現(xiàn)在我們來聲明一個固定長度的數(shù)組:
雜注扎實0.4.11; 合同C { uint256 [6]數(shù)字; 函數(shù)C(){ 數(shù)字[5] = 0xC0FEFE; } }由于編譯器確切地知道有多少個uint256(32個字節(jié)),因此它可以簡單地將數(shù)組的元素放在存儲器中,就像存儲變量和結(jié)構(gòu)一樣。
在這份合同中,我們再次存儲到位置0x5 。
編譯:
$ solc --bin --asm --optimize c-static-array.sol大會:
TAG_2: 0xc0fefe 為0x0 0x5的 tag_4: 加 為0x0 tag_5: 流行的 sstore它稍微長一些,但如果你稍微瞇起一點,你會發(fā)現(xiàn)它實際上是一樣的。 我們手工進(jìn)一步優(yōu)化:
TAG_2: 0xc0fefe // 0 + 5。 用0x5替換 為0x0 0x5的 加 //按下然后立即彈出。 沒用,只是刪除。 為0x0 流行的 sstore除去標(biāo)簽和偽指令,我們再次得到相同的字節(jié)碼序列:
TAG_2: 0xc0fefe 0x5的 sstore數(shù)組綁定檢查
我們已經(jīng)看到,固定長度的數(shù)組與存儲結(jié)構(gòu)和狀態(tài)變量具有相同的存儲布局,但生成的匯編代碼是不同的。 原因是Solidity為數(shù)組訪問生成了邊界檢查。
讓我們再次編譯數(shù)組合約,這次關(guān)閉優(yōu)化:
$ solc --bin --asm c-static-array.sol程序集在下面注釋,每條指令后打印機(jī)器狀態(tài):
TAG_2: 0xc0fefe [0xc0fefe] 0x5的 [0x5 0xc0fefe] DUP1 / *數(shù)組綁定檢查代碼* / // 5 <6 為0x6 [0x6 0x5 0xc0fefe] DUP2 [0x5 0x6 0x5 0xc0fefe] LT [0x1 0x5 0xc0fefe] // bound_check_ok = 1(TRUE) // if(bound_check_ok){goto tag5} else {invalid} tag_5 [tag_5 0x1 0x5 0xc0fefe] jumpi //測試條件為真。 會得到tag_5。 //并且`jumpi`消耗堆棧中的兩項。 [0x5 0xc0fefe] 無效 //數(shù)組訪問是有效的。 做到這一點。 // stack:[0x5 0xc0fefe] tag_5: sstore [] 存儲:{0x5 => 0xc0fefe}我們現(xiàn)在看到綁定檢查代碼。 我們已經(jīng)看到編譯器能夠優(yōu)化這些東西,但并不完美。
在本文的后面,我們將看到數(shù)組綁定檢查如何干擾編譯器優(yōu)化,使得固定長度數(shù)組比存儲變量或結(jié)構(gòu)的效率低得多。
包裝行為
存儲是昂貴的(yayaya我已經(jīng)說了一百萬次)。 一個關(guān)鍵的優(yōu)化是盡可能多地將數(shù)據(jù)打包到一個32字節(jié)的插槽中。
考慮具有四個存儲變量(每個64位)的合同,總共可以累加256位(32個字節(jié)):
雜注扎實0.4.11; 合同C { uint64 a; uint64 b; uint64 c; uint64 d; 函數(shù)C(){ a = 0xaaaa; b = 0xbbbb; c = 0xcccc; d = 0xdddd; } }我們希望(希望)編譯器使用一個sstore將它們放在同一個存儲槽中。
編譯:
$ solc --bin --asm --optimize c-many-variables - packing.sol大會:
TAG_2: / *“c-many-variables - packing.sol”:121:122 a * / 為0x0 / *“c-many-variables - packing.sol”:121:131 a = 0xaaaa * / DUP1 SLOAD / *“c-many-variables - packing.sol”:125:131 0xaaaa * / 加上0xAAAA 不是(0xffffffffffffffff) / *“c-many-variables - packing.sol”:121:131 a = 0xaaaa * / swap1 swap2 和 要么 not(sub(exp(0x2,0x80),exp(0x2,0x40))) / *“c-many-variables - packing.sol”:139:149 b = 0xbbbb * / 和 0xbbbb0000000000000000 要么 not(sub(exp(0x2,0xc0),exp(0x2,0x80))) / *“c-many-variables - packing.sol”:157:167 c = 0xcccc * / 和 0xcccc00000000000000000000000000000000 要么 sub(exp(0x2,0xc0),0x1) / *“c-many-variables - packing.sol”:175:185 d = 0xdddd * / 和 0xdddd000000000000000000000000000000000000000000000000 要么 swap1 sstore很多我無法破譯的小混混,我不在乎。 關(guān)鍵要注意的是,只有一個sstore 。
優(yōu)化成功!
打破優(yōu)化
如果只有優(yōu)化器可以一直很好地工作。 讓我們打破它。 我們唯一的改變是我們使用助手函數(shù)來設(shè)置存儲變量:
雜注扎實0.4.11; 合同C { uint64 a; uint64 b; uint64 c; uint64 d; 函數(shù)C(){ SETAB(); setCD(); } 函數(shù)setAB()internal { a = 0xaaaa; b = 0xbbbb; } 函數(shù)setCD()internal { c = 0xcccc; d = 0xdddd; } }編譯:
$ solc --bin --asm --optimize c-many-variables - packing-helpers.sol裝配輸出太多了。 我們將忽略大部分細(xì)節(jié)并關(guān)注結(jié)構(gòu):
//構(gòu)造函數(shù) TAG_2: // ... //通過跳轉(zhuǎn)到tag_5來調(diào)用setAB() 跳 tag_4: // ... //通過跳轉(zhuǎn)到tag_7來調(diào)用setCD() 跳 //函數(shù)setAB() tag_5: // Bit-shuffle并設(shè)置a,b // ... sstore tag_9: 跳轉(zhuǎn)//返回setAB()的調(diào)用者 //函數(shù)setCD() tag_7: // Bit-shuffle并設(shè)置c,d // ... sstore tag_10: 跳轉(zhuǎn)//返回setCD()的調(diào)用者現(xiàn)在有兩個sstore而不是一個。 Solidity編譯器可以在標(biāo)簽內(nèi)進(jìn)行優(yōu)化,但不能在標(biāo)簽內(nèi)進(jìn)行優(yōu)化。
調(diào)用函數(shù)會花費(fèi)更多,而不是太多,因為函數(shù)調(diào)用很昂貴(它們只是跳轉(zhuǎn)指令),但是因為sstore優(yōu)化可能會失敗。
為了解決這個問題,Solidity編譯器需要學(xué)習(xí)如何內(nèi)聯(lián)函數(shù),本質(zhì)上得到的代碼與不調(diào)用函數(shù)相同:
a = 0xaaaa; b = 0xbbbb; c = 0xcccc; d = 0xdddd; 如果我們仔細(xì)閱讀完整的匯編輸出,我們會看到函數(shù) setAB() 和 setCD() 的匯編代碼 被包含兩次,從而膨脹了代碼的大小,從而花費(fèi)額外的氣體來部署合同。 我們稍后會在了解合同生命周期時再討論這一點。為什么優(yōu)化器打破
優(yōu)化器不會跨標(biāo)簽進(jìn)行優(yōu)化。 考慮“1 + 1”,如果在相同的標(biāo)簽下,它可以優(yōu)化為0x2 :
//優(yōu)化OK! TAG_0: 為0x1 為0x1 加 ...但是,如果指令由標(biāo)簽分隔,則不適用:
//優(yōu)化失敗! TAG_0: 為0x1 為0x1 TAG_1: 加 ...從版本0.4.13開始這種行為是正確的。 未來可能會改變。
再次打破優(yōu)化器
讓我們看看優(yōu)化器失敗的另一種方式。 包裝是否適用于固定長度的陣列? 考慮:
雜注扎實0.4.11; 合同C { uint64 [4]數(shù)字; 函數(shù)C(){ 數(shù)字[0] = 0x0; 數(shù)字[1] = 0x1111; 數(shù)字[2] = 0x2222; 數(shù)字[3] = 0x3333; } }同樣,我們希望使用一個sstore指令將四個64位數(shù)字打包到一個32字節(jié)的存儲插槽中。
編譯后的程序集太長。 讓我們來計算一下sstore和sstore指令的數(shù)量:
$ solc --bin --asm --optimize c-static-array - packing.sol | grep -E'(sstore | sload)' SLOAD sstore SLOAD sstore SLOAD sstore SLOAD sstore哦,不。 即使這個固定長度數(shù)組的存儲布局與等效的結(jié)構(gòu)或存儲變量完全相同,優(yōu)化也會失敗。 它現(xiàn)在需要四對sstore和sstore 。
快速查看匯編代碼可以發(fā)現(xiàn),每個數(shù)組訪問都綁定了檢查代碼,并在不同的標(biāo)記下進(jìn)行組織。 但標(biāo)簽邊界打破了優(yōu)化。
雖然有一點小小的安慰。 3個額外的sstore指令比第一個便宜:
- sstore花費(fèi)20000瓦斯首先寫入新的位置。
- sstore花費(fèi)5000瓦斯用于后續(xù)寫入現(xiàn)有位置。
所以這個特定的優(yōu)化失敗花費(fèi)我們35k而不是20k,另外75%。
結(jié)論
如果Solidity編譯器能夠計算出存儲變量的大小,它只是將它們放在一個接一個的存儲空間中。 如果可能的話,編譯器將數(shù)據(jù)緊密地打包成32字節(jié)的塊。
總結(jié)我們迄今為止看到的包裝行為:
- 存儲變量:是的。
- 結(jié)構(gòu)字段:是。
- 固定長度數(shù)組:不。 理論上,是的。
由于存儲訪問成本非常高,因此您應(yīng)該將存儲變量視為數(shù)據(jù)庫架構(gòu)。 在編寫契約時,做小型實驗可能會很有用,并檢查程序集以確定編譯器是否正在優(yōu)化。
我們可以肯定,Solidity編譯器將來會有所改進(jìn)。 不幸的是,現(xiàn)在我們不能盲目信任它的優(yōu)化器。
它從字面上支付了解您的商店變量。
在這篇關(guān)于EVM的文章系列中,我寫到:
- EVM匯編代碼簡介。
- 如何表示固定長度的數(shù)據(jù)類型。
- 如何表示動態(tài)數(shù)據(jù)類型。
- ABI如何編碼外部方法調(diào)用。
- 新合同創(chuàng)建時發(fā)生了什么。
https://medium.com/@hayeah/diving-into-the-ethereum-vm-part-2-storage-layout-bc5349cb11b7
總結(jié)
以上是生活随笔為你收集整理的【译】Diving Into The Ethereum VM Part 2 — How I Learned To Start Worrying And Count The Storage Cost的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [译】Diving Into The E
- 下一篇: 【译】Diving Into The E