首先,EVM的設計初衷是什麼?它爲何被設計成目前咱們看的樣子呢?根據以太坊官方提供的設計原理說明,EVM的設計目標主要針對如下方面:git
若是讀者瀏覽一下這個文檔,會發現EVM的設計看上去都很是的合理。那麼問題在哪裏呢?問題就出在它和目前主流的技術以及設計範例都格格不入。EVM若是做爲一個毫無限制的非現實世界中的設計確實很不錯。接下來筆者會圍繞EVM各個方面的問題逐一進行描述,首先從筆者最不能忍受的一點開始。 256bit整數目前大多數的處理器主要有如下4種選擇來實現快速的數學運算:github
固然,雖然在一些狀況下32bit比16bit要快,以及在x86架構中8bit數學運算並非徹底支持(無原生的除法和乘法支持),但基本上若是你採用以上的任意一種,均可以保證數學運算在若干個時鐘週期中完成,而且這個過程很是迅速,每每是納秒級的。所以,咱們能夠說,這些位長的整數是目前主流處理器可以「原生地」支持的,不須要任何額外的操做。EVM出於所謂運算速度和效率方面考慮,採用了非主流的256bit整數。算法
讓咱們經過對比x86彙編碼來看看它的表現。數據庫
首先是兩個32bit整數相加的x86彙編碼(也就是大多數PC的處理器採用的):編程
mov eax, dword [number1]
add eax, dword [number2]數組
而後是2個64bit整數相加,這裏假設採用64位處理器:緩存
mov rax, qword [number1]
add rax, qword [number2]安全
接下來是在32位x86計算機上兩個256bit整數相加:網絡
mov eax, dword [number]add dword [number2], eaxmov eax, dword [number1+4]架構
adc dword [number2+4], eax
mov eax, dword [number1+8]
adc dword [number2+8], eax
mov eax, dword [number1+12]
adc dword [number2+12], eax
mov eax, dword [number1+16]
adc dword [number2+16], eax
mov eax, dword [number1+20]
adc dword [number2+20], eax
mov eax, dword [number1+24]
adc dword [number2+24], eax
mov eax, dword [number1+28]
adc dword [number2+28], eax
固然還有在64位x86計算機上兩個256bit整數相加:mov rax, qword [number]add qword [number2], raxmov rax, qword [number1+8]
adc qword [number2+8], rax
mov rax, qword [number1+16]
adc qword [number2+16], rax
mov rax, qword [number1+24]
adc qword [number2+24], rax
經過以上比較足以說明採用256bit整數遠比採用處理器原生支持的整數長度要複雜。EVM之因此選擇這種設計,主要是由於僅支持256bit整數會比增長額外的用於處理其餘位寬整數的opcodes來的簡單得多。僅有的非256bit操做是一系列的push操做,用於從memory中獲取1-32字節的數據,以及一些專門針對8bit整數的操做。
那麼對於全部操做都採用這種低效的整數位寬的設計初衷是什麼呢?
「4字節或8字節字長限制了更大的內存尋址和複雜的密碼學運算,同時無限制的值將很難實現安全的gas模型」
關於地址,我必須認可,可以僅用單個操做實現兩個地址的比較確實很酷。可是,在x86機器上採用32bit整數實現相同功能也並不複雜(無SSE和任何其餘優化):
mov esi, [address1]mov edi, [address2]mov ecx, 32 / 4
repe cmpsd
jne not_equal
; if reach here, then they're equal
假設address1和address2 都是肯定的地址,僅須要6+5+5=16字節的opcodes,而若是地址都在棧上,則僅須要6+3+3=12字節的opcode。關於另外一個理由「複雜的密碼學運算」,筆者從幾個月前第一次看到這個理由,直到如今都沒有看到過一個不涉及地址或哈希值比較的256bit整數的應用實例。密碼學運算若是在區塊鏈上運行顯然過於昂貴了。筆者在github上搜索了一個多小時,試圖找到一個在solidity合約中用到密碼學運算的實例,結果卻一無所得。幾乎全部的密碼學運算對於目前的計算機來講都是複雜的,因此在以太坊公有鏈上進行這種運算是很是昂貴的(必須消耗大量的gas,更不用說把密碼學算法用solidity實現所須要的工做量)。固然,若是是一條私有鏈,gas消耗可能不是問題。但若是你是這條鏈的擁有者,你應該也不會選擇用低效的EVM智能合約來實現密碼學運算,而會選擇採用C++,Go,或者其餘一些編程語言實現。綜上所述,EVM僅支持256bit整數的理由徹底不成立。筆者認爲這是EVM最根本也是最明顯的問題,除此以外,EVM還有很多問題,下面咱們一一道來。
EVM的內存分配模型
EVM中主要有3個用於存儲數據的地方:
棧存儲有許多限制,因此有時候你必須使用臨時內存(永久內存比較昂貴)。在EVM沒有allocate或相似的操做,因此必須經過直接寫數據來獲取內存空間。這看起來很是智能,但實際上卻有很多問題。好比,若是你須要尋址到0x10000,你的合約將分配64K字長(也就是64K的256bit的word)的內存而且你須要支付64K字長對應的gas。有個比較簡單的變通方法,就是你能夠跟蹤你上一次被分配的內存,當你須要時能夠繼續使用未使用的內存。這是有頗有效的方法,直到你須要的內存超過了剩餘可用的內存。咱們假設你寫個某個算法,須要100字長的內存。你分配並使用了該內存,支付了100字長內存對應的gas,而後退出了這個函數。以後你回到了另外一個函數中,它只須要1字長的內存,系統又從新分配了另外1字長的內存,這樣你總共使用了101字長的內存。EVM中沒有辦法釋放內存。理論上你能夠經過記錄最後使用的內存地址,實現內存的釋放和複用,但這僅在你能肯定這段內存不會再被引用的前提下才具備可行性。若是在這100個word中你須要用到第50和第90個word,那麼你必須先把他們拷貝到其餘地方(好比棧上)而後再釋放原來的內存。EVM並無爲此提供相關的工具。是否對智能合約中函數對分配內存的使用進行檢查徹底取決於你,若是你決定複用這些內存,但又沒有檢測出異常狀況,那麼你的智能合約將面臨潛在的重大bug。因此你要麼承擔複用內存帶來的風險,要麼支付足夠多的gas以獲取安全的內存分配。
除此以外,分配內存所須要花費的gas並非線性的。好比你分配了100字長的內存,以後又分配1字長內存,這最後1字長內存的花費將明顯高於你一開始就只分配1字長內存的花費。這又大大增長了保證內存安全所需的花費。
既然如此,那爲何非要使用內存呢?爲何不使用棧?實際上EVM中棧有明顯的限制。EVM中的棧
EVM是一個基於棧的虛擬機。這就意味着對於大多數操做都使用棧,而不是寄存器。基於棧的機器每每比較簡單,且易於優化,但其缺點就是比起基於寄存器的機器所須要的opcode更多。
因此EVM有許多特有的操做,大多數都只在棧上使用。好比SWAP和DUP系列操做等,具體請參見EVM文檔。如今咱們試着編譯以下合約:
pragma solidity ^0.4.13;contract Something{
function foo(address a1, address a2, address a3, address a4, address a5, address a6){
address a7;
address a8;
address a9;
address a10;
address a11;
address a12;
address a13;
address a14;
address a15;
address a16;
address a17;
}
}
你將看到以下錯誤:CompilerError: Stack too deep, try removing local variables.這個錯誤是由於當棧深超過16時發生了溢出。官方的「解決方案」是建議開發者減小變量的使用,並使函數儘可能小。固然還有其餘幾種變通方法,好比把變量封裝到struct或數組中,或是採用關鍵字memory(不知道出於何種緣由,沒法用於普通變量)。既然如此,讓咱們試一試這個採用struct的解決方案:
pragma solidity ^0.4.13;contract Something{
struct meh{
address x;
}
function foo(address a1, address a2, address a3, address a4, address a5, address a6){
address a7;
address a8;
address a9;
address a10;
address a11;
address a12;
address a13;
meh memory a14;
meh memory a15;
meh memory a16;
meh memory a17;
}
}
結果呢?CompilerError: Stack too deep, try removing local variables.咱們明明採用了memory關鍵字,爲何仍是有問題呢?關鍵在於,雖然此次咱們沒有在棧上存放17個256bit整數,但咱們試圖存放13個整數和4個256bit內存地址。這當中包含一些Solidity自己的問題,但主要問題仍是EVM沒法對棧進行隨機訪問。據我所知,其餘一些虛擬機每每採用如下兩種方法之一來解決這個問題:
然而,在EVM中,棧是惟一免費的存放數據的區域,其餘區域都須要支付gas。所以,這至關於鼓勵儘可能使用棧,由於其餘區域都要收費。正由於如此,咱們纔會遇到上文所述的基本的語言實現問題。
bytecode大小
在EVM設計文檔中,設計者聲稱他們的目標是使得EVM的bytecode既簡單又高度壓縮。然而,這就像是試圖寫出既詳盡又簡潔的代碼同樣,實際上二者是存在必定矛盾。要實現一個簡單的指令集就須要儘可能限制操做的種類,並保持每種操做的儘可能簡單;然而,要實現高度壓縮的bytecode則須要引入擁有豐富操做的指令集。
即便是「高度壓縮的bytecode」這一目標也沒有在EVM中實現,他們更加側重於實現易於生成gas模型的指令集。我並非說這是錯的,只是想代表做爲官方聲明的EVM最重要的目標之一最終並無實現這一事實。同時,EVM設計文檔中給出了一個數據:C語言實現的「Hello World」簡單程序生成4000字節的bytecode。這一結果並不正確,很大程度取決於編譯環境以及優化程度。在他們所述的C程序中,應該同時包含了ELF數據,relocation數據以及alignment優化等。筆者嘗試編譯了一個很是簡單的C程序(只有一個程序骨架),只須要46字節的x86機器碼;同時還用C語言寫了一個簡單的greeter type程序(Solidity示例程序),最終生成大約700字節bytecode,而一樣的Solidity示例程序則須要1000字節bytecode。
我固然明白簡化指令集是出於某些安全性因素考慮,但這顯然會致使區塊鏈更加臃腫。若是EVM智能合約的bytecode儘量小的話確實是有害的。咱們徹底能夠經過增長標準庫或是支持能夠批處理某些基本操做的opcode來減少bytecode。
256bit整數(補充)
256bit整數確實使人頭疼,因此這裏再作一些補充。最使人費解的是256bit整數被用到了一些根本不必的地方。好比,咱們根本不可能在合約中使用超過4B(32bit)單位的gas,那麼你猜在EVM中採用什麼長度的整數來做爲gas的計量呢?沒錯,固然是256bit。內存使用也很是昂貴,那內存大小的計量呢?天然也是256bit,當你的合約須要用到比宇宙中原子數量還多的地址時這個數字或許真的能派上用場。雖然我不認同在尋址或是永久內存的變量中使用256bit整數,但不得不說它使得計算某些數據的hash時可以避免衝突,所以這還能勉強接受。但對於任一個instance,本能夠採用任何整數長度,EVM仍是使用了256bit。甚至JUMP也使用256bit,但他們限制了最大的JUMP地址爲0x7FFFFFFFFFFFFFFF,至關於限制在64bit整數範圍內。最後,以太坊中的幣值固然也採用了256bit數來計算。ETH的最小單位是wei,因此總的幣的數量(單位爲wei)爲1000000000000000000 * 200000000 (200M只是估計值,目前僅有約92M)。而2^256約爲1.157920892373162e+77,這足以表示全部已存在的全部ETH外加比全宇宙原子數還多的wei……歸根結底,256bit整數在EVM所設計的大多數應用中都沒有必要。
缺乏標準庫
若是你曾經開發過Solidity智能合約的話,你應該也會碰到這個問題,由於Solidity中根本就沒有標準庫。若是你想比較兩個字符串,Solidity中根本就沒有相似strcmp或memcmp的標準庫函數供你調用,你必須本身用代碼實現或在網上拷貝代碼來實現。Zeppin項目使這一狀況獲得必定改善,他們提供了一個可供合約使用的標準庫(經過將代碼包含在合約中或是調用外部合約)。然而,這種方式的限制也很明顯,主要是在gas消耗方面。好比判斷字符串是否相等,進行兩次SHA3操做而後比較hash值顯然要比循環比較每一個字符所要花費的gas要少。若是存在預編譯好的標準庫,並設定合理的gas價格,這將更加有利於整個智能合約生態的發展。目前的狀況是,人們只能不斷的從一些開源軟件中複製黏貼代碼,首先這些代碼的安全性沒法保證,再加上人們會爲了更小的gas消耗而不斷修改代碼,這就有可能對他們的合約引入更嚴重的安全性問題。
gas經濟模型中的博弈論
我打算寫一篇新的博客單獨闡述這個主題。EVM不只使寫出好的代碼變得很困難,還令其變得很是昂貴。好比,在區塊鏈上存儲數據須要耗費大量的gas。這意味着在智能合約中緩存數據的代價會很是大,所以每每在每次合約運行時從新計算數據。隨着合約被不斷執行,愈來愈多的gas和時間都被花在了重複計算徹底相同的數據上。實際上單純經過交易在區塊鏈上存儲數據並不會消耗太多的gas,由於這並不會直接增長區塊的大小(無論以太坊仍是Qtum都是如此)。真正花費比較大的實際上是那些發送給合約的數據,由於這將直接增長區塊的大小。在以太坊中,經過交易在區塊鏈上記錄32byte的數據比在合約中存儲相同的數據消耗的gas要少一些,而若是是64byte的數據,則消耗的數據就少得多了(29,704 gas v.s. 80,000gas)。在合約中儲存數據會有「virtual」的花費,但比大多數人想象的要少得多。基本上就是遍歷區塊鏈上數據庫的花費。Qtum和以太坊採用的RLP和LevelDB數據庫系統在這方面很是高效,但持續的成本並非線性的。
EVM鼓勵這種低效率的代碼的另外一緣由就是其不支持直接調用智能合約中某個具體的函數。這固然是出於安全性考慮,若是容許直接調用在ERC20代幣合約中的withdraw函數,結果確實會是災難性的。可是這在標準庫調用中將會很是高效。目前EVM中要麼執行智能合約的全部代碼,要麼一點也不執行,徹底不可能只執行其中部分代碼。程序老是從頭開始運行,沒法跳過Solidity ABI引導代碼。因此這致使的結果就是一些小函數被不斷複製(由於經過外部調用將更加昂貴),而且鼓勵開發者在同一個合約中包含儘可能多的函數。調用一個100bytes的合約並不比調用10000bytes的合約昂貴,儘管全部代碼都必須加載到內存中。
最後一點,就是EVM中沒法直接獲取合約中存儲的數據。合約代碼必須先被徹底加載並執行,而且包含你所請求的數據,最終經過合約調用返回值的形式返回數據(還得保證沒有多個返回值)。同時,當你不肯定你須要的是哪一個數據,須要來來回回地調用合約時,第二次調用合約所須要的gas並無任何折扣(不過至少合約還在緩存中,對節點來講第二次調用稍微便宜一些)。實際上徹底能夠在不加載整個外部合約的基礎上訪問其數據,這其實和獲取當前合約的存儲數據沒什麼兩樣,爲何偏要採用如此昂貴且低效的方式呢?
難以調試和測試
這個問題不只僅是因爲EVM的設計缺陷,也和其實現方式有關。固然,有一些項目正在作相關工做使整個過程變得簡單,好比Truffle項目。然而EVM的設計又使這些工做變得很困難。EVM惟一能拋出的異常就是「OutOfGas」,而且沒有調試日誌,也沒法調用外部代碼(好比test helpers和mock數據),同時以太坊區塊鏈自己很難生成一條測試網絡的私鏈,即便成功,私鏈的參數和行爲也與公鏈不一樣。Qtum至少還有regtest模式可用,而在EVM中使用mock數據等進行測試則真的很是困難。據我所知目前尚未任何針對Solidity的調試器,雖然有一款我知道的EVM assembly調試器,但其使用體驗極差。EVM和Solidity都沒有建立用於調試的符號格式或是數據格式,而且目前沒有任何一個EIP提出要創建像DWARF同樣標準的調試數格式。
不支持浮點數
對於那些支持EVM不須要浮點數的人來講,最經常使用的理由就是「沒有人會在貨幣中採用浮點數」。這實際上是很是狹隘的想法。浮點數有不少應用實例,好比風險建模,科學計算,以及其餘一些範圍和近似值比準確值更加劇要的狀況。這種認爲智能合約只是用於處理貨幣相關問題的想法是很是侷限的。
不可修改的代碼
智能合約在設計時須要考慮的重要問題之一就是是可升級性,由於合約的升級是必然的。在EVM中代碼是徹底不可修改的,而且因爲其採用哈佛計算機結構,也就不可能將代碼在內存中加載並執行,代碼和數據是被徹底分離的。目前只可以經過部署新的合約來達到升級的目的,這可能須要複製原合約中的全部代碼,並將老的合約重定向到新的合約地址。給合約打補丁或是部分升級合約代碼在EVM中是徹底不可能的。
小結
不能否認,EVM做爲第一個區塊鏈虛擬機存在諸多問題,這和絕大多數新生事物同樣(好比Javascript)。而且因爲它的設計比較非主流,我認爲不會有主流的編程語言可以移植到EVM上。這種設計能夠說對於近50年來的大多數編程範例來講都不太友好。好比JUMPDEST使得jump table優化更加困難,不支持尾遞歸,詭異且不靈活的內存模型,棧的限制,固然還有256bit整數等等。這種種問題都使得移植主流編程語言的代碼變得困難重重。我想這就是目前EVM只能支持專門定製的開發語言的緣由。這是在是件使人遺憾的事。