首先,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只能支持专门定制的开发语言的缘由。这是在是件使人遗憾的事。