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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

深入了解以太坊虚拟机第3部分——动态数据类型的表示方法

發布時間:2025/3/15 编程问答 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入了解以太坊虚拟机第3部分——动态数据类型的表示方法 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文由幣乎社區(bihu.com)內容支持計劃贊助。

Solidity提供了在其他編程語言常見的數據類型。除了簡單的值類型比如數字和結構體,還有一些其他數據類型,隨著數據的增加可以進行動態擴展的動態類型。動態類型的3大類:

  • 映射(Mappings):mapping(bytes32 => uint256), mapping(address => string)等等
  • 數組(Arrays):[]uint256,[]byte等等
  • 字節數組(Byte arrays):只有兩種類型:string,bytes

在本系列的第二篇文章中我們看見了固定大小的簡單類型在內存中的表示方式。

  • 基本數值:uint256,byte等等
  • 定長數組:[10]uint8,[32]byte,bytes32
  • 組合了上面類型的結構體

固定大小的存儲變量都是盡可能的打包成32字節的塊然后依次存放在存儲器中的。(如果這看起來很陌生,請閱讀本系列的第二篇文章: 固定長度數據類型的表示方法

在本文中我們將會研究Solidity是如何支持更加復雜的數據結構的。在表面上看可能Solidity中的數組和映射比較熟悉,但是從它們的實現方式來看在本質上卻有著不同的性能特征。

我們會從映射開始,這是三者當中最簡單的。數組和字節數組其實就是擁有更加高級特征的映射。

映射

讓我們存儲一個數值在uint256 => uint256映射中:

pragma solidity ^0.4.11; contract C {mapping(uint256 => uint256) items;function C() {items[0xC0FEFE] = 0x42;} }

編譯:

solc --bin --asm --optimize c-mapping.sol

匯編代碼:

tag_2:// 不做任何事情,應該會被優化掉0xc0fefe0x0swap1dup2mstore0x20mstore// 將0x42 存儲在地址0x798...187c上0x420x79826054ee948a209ff4a6c9064d7398508d2c1909a392f899d301c6d232187csstore

我們可以將EVM想成一個鍵-值( key-value)數據庫,不過每個key都限制為32字節。與其直接使用key0xC0FEFE,不如使用key的哈希值0x798...187c,并且0x42存儲在這里。哈希函數使用的是keccak256(SHA256)函數。

在這個例子中我們沒有看見keccak256指令本身,因為優化器已經提前計算了結果并內聯到了字節碼中。在沒什么作用的mstore指令中,我們依然可以看到計算的痕跡。

計算地址

使用一些Python代碼來把0xC0FEFE哈希成0x798...187c。如果你想要跟著做下去,你需要安裝Python 3.6,或者安裝pysha3 來獲得keccak_256哈希函數。

定義兩個協助函數:

import binascii import sha3 #將數值轉換成32字節數組 def bytes32(i):return binascii.unhexlify('%064x' % i) # 計算32字節數組的 keccak256 哈希值 def keccak256(x):return sha3.keccak_256(x).hexdigest()

將數值轉換成32個字節:

>>> bytes32(1) b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' >>> bytes32(0xC0FEFE) b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\xfe\xfe'

使用+操作符,將兩個字節數組連接起來:

>>> bytes32(1) + bytes32(2) b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02'

計算一些字節的 keccak256 哈希值:

>>> keccak256(bytes(1)) 'bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a'

現在我們可以計算0x798...187c了。

存儲變量items的位置是0x0(因為它是第一個存儲變量)。連接key0xc0fefe和items的位置來獲取地址:

# key = 0xC0FEFE, position = 0 >>> keccak256(bytes32(0xC0FEFE) + bytes32(0)) '79826054ee948a209ff4a6c9064d7398508d2c1909a392f899d301c6d232187c'

為key計算存儲地址的公式是:

keccak256(bytes32(key) + bytes32(position))

兩個映射

我們先把公式放在這里,后面數值存儲時需要計算會用到該公式。

假設我們的合約有兩個映射:

pragma solidity ^0.4.11; contract C {mapping(uint256 => uint256) itemsA;mapping(uint256 => uint256) itemsB;function C() {itemsA[0xAAAA] = 0xAAAA;itemsB[0xBBBB] = 0xBBBB;} }
  • itemsA的位置是0,key為0xAAAA:
# key = 0xAAAA, position = 0 >>> keccak256(bytes32(0xAAAA) + bytes32(0)) '839613f731613c3a2f728362760f939c8004b5d9066154aab51d6dadf74733f3'
  • itemsB的位置為1,key為0xBBBB:
# key = 0xBBBB, position = 1 >>> keccak256(bytes32(0xBBBB) + bytes32(1)) '34cb23340a4263c995af18b23d9f53b67ff379ccaa3a91b75007b010c489d395'

用編譯器來驗證一下這些計算:

$ solc --bin --asm --optimize c-mapping-2.sol

匯編代碼:

tag_2:// ... 忽略可能會被優化掉的內存操作0xaaaa0x839613f731613c3a2f728362760f939c8004b5d9066154aab51d6dadf74733f3sstore0xbbbb0x34cb23340a4263c995af18b23d9f53b67ff379ccaa3a91b75007b010c489d395sstore

跟期望的結果一樣。

匯編代碼中的KECCAK256

編譯器可以提前計算key的地址是因為相關的值是常量。如果key使用的是變量,那么哈希就必須要在匯編代碼中完成。現在我們無效化優化器,來看看在匯編代碼中哈希是如何完成的。

事實證明很容易就能讓優化器無效,只要引入一個間接的虛變量i:

pragma solidity ^0.4.11; contract C {mapping(uint256 => uint256) items;//這個變量會造成常量的優化失敗uint256 i = 0xC0FEFE;function C() {items[i] = 0x42;} }

變量items的位置依然是0x0,所以我們應該期待地址與之前是一樣的。

加上優化選項進行編譯,但是這次不會提前計算哈希值:

$ solc --bin --asm --optimize c-mapping--no-constant-folding.sol

注釋的匯編代碼:

tag_2:// 加載`i` 到棧中sload(0x1)[0xC0FEFE]// 將key`0xC0FEFE`存放在內存中的0x0位置上,為哈希做準備0x0[0x0 0xC0FEFE]swap1[0xC0FEFE 0x0]dup2[0x0 0xC0FEFE 0x0]mstore[0x0]memory: {0x00 => 0xC0FEFE}// 將位置 `0x0` 存儲在內存中的 0x20 (32)位置上,為哈希做準備0x20 // 32[0x20 0x0]dup2[0x0 0x20 0x0]swap1[0x20 0x0 0x0]mstore[0x0]memory: {0x00 => 0xC0FEFE0x20 => 0x0}// 從第0個字節開始,哈希在內存中接下來的0x40(64)個字節0x40 // 64[0x40 0x0]swap1[0x0 0x40]keccak256[0x798...187c]// 將0x42 存儲在計算的地址上0x42[0x42 0x798...187c]swap1[0x798...187c 0x42]sstorestore: {0x798...187c => 0x42}

mstore指令寫入32個字節到內存中。內存操作便宜很多,只需要3 gas就可以讀取和寫入。前半部分的匯編代碼就是通過將key和位置加載到相鄰的內存塊中來進行“連接”的:

0 31 32 63 [ key (32 bytes) ][ position (32 bytes) ]

然后keccak256指令哈希內存中的數據。成本取決于被哈希的數據有多少:

  • 每個SHA3操作需要支付 30 gas
  • 每個32字節的字需要支付 6 gas

對于一個uint256類型key,gas的成本是42:30 + 6 * 2。

映射大數值

每個存儲槽只能存儲32字節。如果我們嘗試存儲一個更大一點的結構體會怎么樣?

pragma solidity ^0.4.11; contract C {mapping(uint256 => Tuple) tuples;struct Tuple {uint256 a;uint256 b;uint256 c;}function C() {tuples[0x1].a = 0x1A;tuples[0x1].b = 0x1B;tuples[0x1].c = 0x1C;} }

編譯,你會看見3個sstore指令:

tag_2://忽略未優化的代碼0x1a0xada5013122d395ba3c54772283fb069b10426056ef8ca54750cb9bb552a59e7dsstore0x1b0xada5013122d395ba3c54772283fb069b10426056ef8ca54750cb9bb552a59e7esstore0x1c0xada5013122d395ba3c54772283fb069b10426056ef8ca54750cb9bb552a59e7fsstore

注意計算的地址除了最后一個數字其他都是一樣的。Tulp結構體成員是依次排列的(..7d, ..7e, ..7f)。

映射不會打包

考慮到映射的設計方式,每項需要的最小存儲空間是32字節,即使你實際只需要存儲1個字節:

pragma solidity ^0.4.11; contract C {mapping(uint256 => uint8) items;function C() {items[0xA] = 0xAA;items[0xB] = 0xBB;} }

如果一個數值大于32字節,那么你需要的存儲空間會以32字節依次增加。

動態數組是映射的升級

在典型語言中,數組只是連續存儲在內存中一系列相同類型的元素。假設你有一個包含100個uint8類型的元素數組,那么這就會占用100個字節的內存。這種模式的話,將整個數組加載到CPU的緩存中然后循環遍歷每個元素會便宜一點。

對于大多數語言而言,數組比映射都會便宜一些。不過在Solidity中,數組是更加昂貴的映射。數組里面的元素會按照順序排列在存儲器中:

0x290d...e563 0x290d...e564 0x290d...e565 0x290d...e566

但是請記住,對于這些存儲槽的每次訪問實際上就像數據庫中的key-value的查找一樣。訪問一個數組的元素跟訪問一個映射的元素是沒什么區別的。

思考一下[]uint256類型,它本質上與mapping(uint256 => uint256)是一樣的,只不過后者多了一點特征,讓它看起去就像數組一樣。

  • length表示一共有多少個元素
  • 邊界檢查。當讀取或寫入時索引值大于length就會報錯
  • 比映射更加復雜的存儲打包行為
  • 當數組變小時,自動清除未使用的存儲槽
  • bytes和string的特殊優化讓短數組(小于32字節)存儲更加高效

簡單數組

看一下保存3個元素的數組:

// c-darray.sol pragma solidity ^0.4.11; contract C {uint256[] chunks;function C() {chunks.push(0xAA);chunks.push(0xBB);chunks.push(0xCC);} }

數組訪問的匯編代碼難以追蹤,使用Remix調試器來運行合約:

模擬的最后,我們可以看到有4個存儲槽被使用了:

key: 0x0000000000000000000000000000000000000000000000000000000000000000 value: 0x0000000000000000000000000000000000000000000000000000000000000003 key: 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563 value: 0x00000000000000000000000000000000000000000000000000000000000000aa key: 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564 value: 0x00000000000000000000000000000000000000000000000000000000000000bb key: 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e565 value: 0x00000000000000000000000000000000000000000000000000000000000000cc

chunks變量的位置是0x0,用來存儲數組的長度(0x3),哈希變量的位置來找到存儲數組數據的地址:

# position = 0 >>> keccak256(bytes32(0)) '290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563'

在這個地址上數組的每個元素依次排列(0x29..63,0x29..64,0x29..65)。

動態數據打包

所有重要的打包行為是什么樣的?數組與映射比較,數組的一個優勢就是打包。擁有4個元素的uint128[]數組元素剛剛好需要2個存儲槽(再加1個存儲槽用來存儲長度)。

思考一下:

pragma solidity ^0.4.11; contract C {uint128[] s;function C() {s.length = 4;s[0] = 0xAA;s[1] = 0xBB;s[2] = 0xCC;s[3] = 0xDD;} }

在Remix中運行這個代碼,存儲器的最后看起來像這樣:

key: 0x0000000000000000000000000000000000000000000000000000000000000000 value: 0x0000000000000000000000000000000000000000000000000000000000000004 key: 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563 value: 0x000000000000000000000000000000bb000000000000000000000000000000aa key: 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564 value: 0x000000000000000000000000000000dd000000000000000000000000000000cc

只有三個存儲槽被使用了,跟預料的一樣。長度再次存儲在存儲變量的0x0位置上。4個元素被打包放入兩個獨立的存儲槽中。該數組的開始地址是變量位置的哈希值:

# position = 0 >>> keccak256(bytes32(0)) '290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563'

現在的地址是每兩個數組元素增加一次,看起來很好!

但是匯編代碼本身優化的并不好。因為使用了兩個存儲槽,所以我們會希望優化器使用兩個sstore指令來完成任務。不幸的是,由于邊界檢查(和一些其他因素),所以沒有辦法將sstore指令優化掉。

使用4個sstore指令才能完成任務:

/* "c-bytes--sstore-optimize-fail.sol":105:116 s[0] = 0xAA */ sstore /* "c-bytes--sstore-optimize-fail.sol":126:137 s[1] = 0xBB */ sstore /* "c-bytes--sstore-optimize-fail.sol":147:158 s[2] = 0xCC */ sstore /* "c-bytes--sstore-optimize-fail.sol":168:179 s[3] = 0xDD */ sstore

字節數組和字符串

bytes和string是為字節和字符進行優化的特殊數組類型。如果數組的長度小于31字節,只需要1個存儲槽來存儲整個數組。長一點的字節數組跟正常數組的表示方式差不多。

看看短一點的字節數組:

// c-bytes--long.sol pragma solidity ^0.4.11; contract C {bytes s;function C() {s.push(0xAA);s.push(0xBB);s.push(0xCC);} }

因為數組只有3個字節(小于31字節),所以它只占用1個存儲槽。在Remix中運行,存儲看起來如下:

key: 0x0000000000000000000000000000000000000000000000000000000000000000 value: 0xaabbcc0000000000000000000000000000000000000000000000000000000006

數據0xaabbcc...從左到右的進行存儲。后面的0是空數據。最后的0x06字節是數組的編碼長度。公式是長度=編碼長度/2,在這個例子中實際長度是6/2=3。

string與bytes的原理一模一樣。

長字節數組

如果數據的長度大于31字節,字節數組就跟[]byte一樣。來看一下長度為128字節的字節數組:

// c-bytes--long.sol pragma solidity ^0.4.11; contract C {bytes s;function C() {s.length = 32 * 4;s[31] = 0x1;s[63] = 0x2;s[95] = 0x3;s[127] = 0x4;} }

在Remix中運行,可以看見使用了4個存儲槽:

0x0000...0000 0x0000...0101 0x290d...e563 0x0000...0001 0x290d...e564 0x0000...0002 0x290d...e565 0x0000...0003 0x290d...e566 0x0000...0004

0x0的存儲槽不再用來存儲數據,整個存儲槽現在存儲編碼的數組長度。要獲得實際長度,使用長度=(編碼長度-1)/2公式。在這個例子中長度是(0x101 - 1)/2=128。實際的字節被保存在0x290d...e563,并且存儲槽是連續的。

字節數組的匯編代碼相當多。除了正常的邊界檢查和數組恢復大小等,它還需要對長度進行編碼/解碼,以及注意長字節數組和短字節數組之間的轉換。

為什么要編碼長度?因為編碼之后,可以很容易的測試出來字節數組是長還是短。注意對于長數組而言編碼長度總是奇數,而短數組的編碼長度總是偶數。匯編代碼只需要查看一下最后一位是否為0,為0就是偶數(短數組),非0就是奇數(長數組)。

總結

查看Solidity編譯器的內部工作,可以看見熟悉的數據結構例如映射和數組與傳統編程語言完全不同。

概括:

  • 數組跟映射一樣,非高效
  • 比映射的匯編代碼更加復雜
  • 小類型(byte,uint8,string)時存儲比映射高效
  • 匯編代碼優化的不是很好。即使是打包,每個任務都會有一個sstore指令

EVM的存儲器就是一個鍵值數據庫,跟git很像。如果你改變了任一東西,根節點的校驗和也會改變。如果兩個根節點擁有相同的校驗和,存儲的數據就能保證是一樣的。

為了體會Solidity和EVM的奇特,可以想象一下在git倉庫里數組里面的每個元素都是它自己的文件。當你改變數組里一個元素的值,實際上就相當于創建了一個提交。當你迭代一個數組時,你不能一次性的加載整個數組,你必須要到倉庫中進行查找并分別找到每個文件。

不僅僅這樣,每個文件都限制到32字節!因為我們需要將數據結構都分割成32字節的塊,Solidity編譯器的所有邏輯和優化都是很負責的,全部在匯編的時候完成。

不過32字節的限制是完全任意的。支持鍵值存儲的可以使用key來存儲任意類型的數值。也許未來我們添加新的EVM指令使用key來存儲任意的字節數組。

不過現在,EVM存儲器就是一個偽裝成32字節數組的鍵值數據庫。

可以看看ArrayUtils::resizeDynamicArray 來了解一下當恢復數組大小時編譯器的動作。正常情況下數據結構都會作為語言的標準庫來完成的,但是在Solidity中嵌入到了編譯器里面。

本系列文章其他部分譯文鏈接:

  • EVM匯編代碼的介紹(第1部分)
  • 固定長度數據類型的表示方法(第2部分)
  • ABI編碼外部方法調用的方式(第4部分)
  • 一個新合約被創建后會發生什么(第5部分)

翻譯作者: 許莉
原文地址:Diving Into The Ethereum VM Part Three



作者:Lilymoana
鏈接:https://www.jianshu.com/p/af5721c79505
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。 與50位技術專家面對面20年技術見證,附贈技術全景圖

總結

以上是生活随笔為你收集整理的深入了解以太坊虚拟机第3部分——动态数据类型的表示方法的全部內容,希望文章能夠幫你解決所遇到的問題。

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