日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

【译】Diving Into The Ethereum VM Part 2 — How I Learned To Start Worrying And Count The Storage Cost

發布時間:2025/3/15 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【译】Diving Into The Ethereum VM Part 2 — How I Learned To Start Worrying And Count The Storage Cost 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
在本系列的第一篇文章中,我們窺見了一個簡單的Solidity合約的匯編代碼: 合同C { uint256 a; 函數C(){ a = 1; } }

該合約歸結為sstore指令的調用:

// a = 1 sstore(0x0,0x1)
  • EVM將值0x1存儲在存儲位置0x0 。
  • 每個存儲位置可以存儲32個字節(或256位)。
如果這看起來不熟悉,我推薦閱讀: 潛入以太坊虛擬機第1部分 - 裝配&字節碼

在本文中,我們將開始研究Solidity如何使用32個字節的塊來表示更復雜的數據類型,如結構和數組。 我們還會看到如何優化存儲,以及優化如何失敗。

在典型的編程語言中,理解數據類型如何在如此低層次上表現出來并不是非常有用。 在Solidity(或任何EVM語言)中,此知識至關重要,因為存儲訪問非常昂貴:

  • sstore成本為20000 sstore ,或比基本算術指令貴?5000倍。
  • sload需要200 sload天然氣,或比基本算術指令貴?100倍。

而通過“成本”,我們在這里談論真錢,而不僅僅是毫秒的表現。 運行和使用您的合同的成本很可能由sstore和sstore支配!

Parsecs在Parsecs磁帶上

圖靈機。 來源: http : //raganwald.com/

構建通用計算機需要兩個基本要素:

  • 循環的一種方式,無論是跳轉還是遞歸。
  • 無限量的記憶。
  • EVM匯編代碼跳轉,EVM存儲提供無限的內存。 這對一切都是足夠的,包括模擬一個運行以太坊版本的世界,它本身模擬一個運行以太坊的世界......

    潛入微電池

    合同的EVM存儲就像一個無限的自動收報機磁帶,磁帶的每個插槽都可容納32個字節。 喜歡這個:

    [32個字節] [32個字節] [32個字節] ...

    我們將看到數據如何存在于無限大的磁帶上。

    磁帶的長度為2÷5?,或每個合約約10??個存儲插槽。 可觀測宇宙的粒子數是10??。 大約1000個合約足以容納所有這些質子,中子和電子。 不要相信營銷炒作,因為它比無限更短。

    空白磁帶

    存儲最初是空白的,默認為零。 擁有無限磁帶并不需要花費任何東西。

    我們來看一個簡單的合約來說明零價值行為:

    雜注扎實0.4.11; 合同C { uint256 a; uint256 b; uint256 c; uint256 d; uint256 e; uint256 f; 函數C(){ f = 0xc0fefe; } }

    存儲中的布局很簡單。

    • 位置為0x0的變量a
    • 位置為0x1的變量b
    • 等等…

    關鍵問題:如果我們只使用f ,我們為a,b,c,d,e支付多少錢?

    讓我們編譯看看:

    $ solc --bin --asm --optimize c-many-variables.sol

    大會:

    // sstore(0x5,0xc0fefe) TAG_2: 0xc0fefe 0x5的 sstore

    因此,存儲變量聲明不需要任何費用,因為不需要初始化。 Solidity為該商店變量保留一個位置,并且只有當您存儲某些內容時才支付。

    在這種情況下,我們只支付存儲到0x5 。

    如果我們手工編寫程序集,我們可以選擇任何存儲位置而不必“擴展”存儲:

    //寫入任意位置 sstore(0xc0fefe,0x42)

    讀零

    您不僅可以在存儲的任何地方寫字,還可以立即從任何地方讀取。 從未初始化的位置讀取僅返回0x0 。

    讓我們看看一個從未初始化位置讀取的合約:

    雜注扎實0.4.11; 合同C { uint256 a; 函數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是構造函數,并且從未寫入過,所以我們可以用0x0替換sload序列以節省5000個氣體。

    代表結構

    我們來看看我們的第一個復雜數據類型,一個有6個字段的結構體:

    雜注扎實0.4.11; 合同C { 結構元組{ uint256 a; uint256 b; uint256 c; uint256 d; uint256 e; uint256 f; } 元組t; 函數C(){ tf = 0xC0FEFE; } }

    存儲中的布局與狀態變量相同:

    • 位置0x0的字段ta
    • 位置為0x1的字段tb
    • 等等…

    像以前一樣,我們可以直接寫入tf而無需支付初始化費用。

    讓我們編譯一下:

    $ solc --bin --asm --optimize c-struct-fields.sol

    我們看到完全相同的組件:

    TAG_2: 0xc0fefe 0x5的 sstore

    固定長度數組

    現在我們來聲明一個固定長度的數組:

    雜注扎實0.4.11; 合同C { uint256 [6]數字; 函數C(){ 數字[5] = 0xC0FEFE; } }

    由于編譯器確切地知道有多少個uint256(32個字節),因此它可以簡單地將數組的元素放在存儲器中,就像存儲變量和結構一樣。

    在這份合同中,我們再次存儲到位置0x5 。

    編譯:

    $ solc --bin --asm --optimize c-static-array.sol

    大會:

    TAG_2: 0xc0fefe 為0x0 0x5的 tag_4: 為0x0 tag_5: 流行的 sstore

    它稍微長一些,但如果你稍微瞇起一點,你會發現它實際上是一樣的。 我們手工進一步優化:

    TAG_2: 0xc0fefe // 0 + 5。 用0x5替換 為0x0 0x5的 //按下然后立即彈出。 沒用,只是刪除。 為0x0 流行的 sstore

    除去標簽和偽指令,我們再次得到相同的字節碼序列:

    TAG_2: 0xc0fefe 0x5的 sstore

    數組綁定檢查

    我們已經看到,固定長度的數組與存儲結構和狀態變量具有相同的存儲布局,但生成的匯編代碼是不同的。 原因是Solidity為數組訪問生成了邊界檢查。

    讓我們再次編譯數組合約,這次關閉優化:

    $ solc --bin --asm c-static-array.sol

    程序集在下面注釋,每條指令后打印機器狀態:

    TAG_2: 0xc0fefe [0xc0fefe] 0x5的 [0x5 0xc0fefe] DUP1 / *數組綁定檢查代碼* / // 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] 無效 //數組訪問是有效的。 做到這一點。 // stack:[0x5 0xc0fefe] tag_5: sstore [] 存儲:{0x5 => 0xc0fefe}

    我們現在看到綁定檢查代碼。 我們已經看到編譯器能夠優化這些東西,但并不完美。

    在本文的后面,我們將看到數組綁定檢查如何干擾編譯器優化,使得固定長度數組比存儲變量或結構的效率低得多。

    包裝行為

    存儲是昂貴的(yayaya我已經說了一百萬次)。 一個關鍵的優化是盡可能多地將數據打包到一個32字節的插槽中。

    考慮具有四個存儲變量(每個64位)的合同,總共可以累加256位(32個字節):

    雜注扎實0.4.11; 合同C { uint64 a; uint64 b; uint64 c; uint64 d; 函數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

    很多我無法破譯的小混混,我不在乎。 關鍵要注意的是,只有一個sstore 。

    優化成功!

    打破優化

    如果只有優化器可以一直很好地工作。 讓我們打破它。 我們唯一的改變是我們使用助手函數來設置存儲變量:

    雜注扎實0.4.11; 合同C { uint64 a; uint64 b; uint64 c; uint64 d; 函數C(){ SETAB(); setCD(); } 函數setAB()internal { a = 0xaaaa; b = 0xbbbb; } 函數setCD()internal { c = 0xcccc; d = 0xdddd; } }

    編譯:

    $ solc --bin --asm --optimize c-many-variables - packing-helpers.sol

    裝配輸出太多了。 我們將忽略大部分細節并關注結構:

    //構造函數 TAG_2: // ... //通過跳轉到tag_5來調用setAB() tag_4: // ... //通過跳轉到tag_7來調用setCD() //函數setAB() tag_5: // Bit-shuffle并設置a,b // ... sstore tag_9: 跳轉//返回setAB()的調用者 //函數setCD() tag_7: // Bit-shuffle并設置c,d // ... sstore tag_10: 跳轉//返回setCD()的調用者

    現在有兩個sstore而不是一個。 Solidity編譯器可以在標簽內進行優化,但不能在標簽內進行優化。

    調用函數會花費更多,而不是太多,因為函數調用很昂貴(它們只是跳轉指令),但是因為sstore優化可能會失敗。

    為了解決這個問題,Solidity編譯器需要學習如何內聯函數,本質上得到的代碼與不調用函數相同:

    a = 0xaaaa; b = 0xbbbb; c = 0xcccc; d = 0xdddd; 如果我們仔細閱讀完整的匯編輸出,我們會看到函數 setAB() 和 setCD() 的匯編代碼 被包含兩次,從而膨脹了代碼的大小,從而花費額外的氣體來部署合同。 我們稍后會在了解合同生命周期時再討論這一點。

    為什么優化器打破

    優化器不會跨標簽進行優化。 考慮“1 + 1”,如果在相同的標簽下,它可以優化為0x2 :

    //優化OK! TAG_0: 為0x1 為0x1 ...

    但是,如果指令由標簽分隔,則不適用:

    //優化失敗! TAG_0: 為0x1 為0x1 TAG_1: ...

    從版本0.4.13開始這種行為是正確的。 未來可能會改變。

    再次打破優化器

    讓我們看看優化器失敗的另一種方式。 包裝是否適用于固定長度的陣列? 考慮:

    雜注扎實0.4.11; 合同C { uint64 [4]數字; 函數C(){ 數字[0] = 0x0; 數字[1] = 0x1111; 數字[2] = 0x2222; 數字[3] = 0x3333; } }

    同樣,我們希望使用一個sstore指令將四個64位數字打包到一個32字節的存儲插槽中。

    編譯后的程序集太長。 讓我們來計算一下sstore和sstore指令的數量:

    $ solc --bin --asm --optimize c-static-array - packing.sol | grep -E'(sstore | sload)' SLOAD sstore SLOAD sstore SLOAD sstore SLOAD sstore

    哦,不。 即使這個固定長度數組的存儲布局與等效的結構或存儲變量完全相同,優化也會失敗。 它現在需要四對sstore和sstore 。

    快速查看匯編代碼可以發現,每個數組訪問都綁定了檢查代碼,并在不同的標記下進行組織。 但標簽邊界打破了優化。

    雖然有一點小小的安慰。 3個額外的sstore指令比第一個便宜:

    • sstore花費20000瓦斯首先寫入新的位置。
    • sstore花費5000瓦斯用于后續寫入現有位置。

    所以這個特定的優化失敗花費我們35k而不是20k,另外75%。

    結論

    如果Solidity編譯器能夠計算出存儲變量的大小,它只是將它們放在一個接一個的存儲空間中。 如果可能的話,編譯器將數據緊密地打包成32字節的塊。

    總結我們迄今為止看到的包裝行為:

    • 存儲變量:是的。
    • 結構字段:是。
    • 固定長度數組:不。 理論上,是的。

    由于存儲訪問成本非常高,因此您應該將存儲變量視為數據庫架構。 在編寫契約時,做小型實驗可能會很有用,并檢查程序集以確定編譯器是否正在優化。

    我們可以肯定,Solidity編譯器將來會有所改進。 不幸的是,現在我們不能盲目信任它的優化器。

    它從字面上支付了解您的商店變量。


    在這篇關于EVM的文章系列中,我寫到:

    • EVM匯編代碼簡介。
    • 如何表示固定長度的數據類型。
    • 如何表示動態數據類型。
    • ABI如何編碼外部方法調用。
    • 新合同創建時發生了什么。


    https://medium.com/@hayeah/diving-into-the-ethereum-vm-part-2-storage-layout-bc5349cb11b7

    總結

    以上是生活随笔為你收集整理的【译】Diving Into The Ethereum VM Part 2 — How I Learned To Start Worrying And Count The Storage Cost的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。