脸书主导的Libra区块链是用Rust写的。而Libra中所推出的Move语言和Move虚拟机,主打概念是对于资源归属权的管理和控制,这又不免和Rust产生了千丝万缕的联系。究竟Libra团队是因为Move而选择了Rust,还是因为Rust产生灵感而设计出了Move,我们不得而知。我们不妨先看看Rust。
Rust是一个编译型语言,其最核心的特点就是编译型,且没有GC。所以,我们先……
插播什么是GC
垃圾回收(Garbage Collection,简称GC)是一个头疼的问题。程序的多个模块之间互相调用、传输或共享数据时,为了高效,应尽可能避免复制内存中的数据,而是传递一个内存指针(引用)过去。这样一来,系统就必须搞清楚每一个资源(最主要是内存空间)还有多少个程序模块可能访问。仅当没有人再要它的时候,它就成了垃圾(可回收的),被GC回收。因此,GC的典型工作原理就是:一方面要有对资源的引用计数,另一方面定期清理引用计数降低为0的资源。
C/C++语言没有GC。因此程序员必须自己管理资源的申请和释放,就是malloc/free,以及new/delete。但这很容易出错:如果你提前free/delete了一块内存,就很容易造成缓冲区溢出漏洞;如果你忘记free/delete了内存,就会造成内存泄漏,程序占用内存越跑越大;如果同一块内存你free了两次,程序直接就崩溃了。所有脚本语言,以及Java、Go,都有自带的GC,这使得程序员不需要手动管理内存资源的释放。
GC有什么弊端呢?那就是GC的工作必须对程序本身是完全无感、透明的。对于多线程程序,这意味着必须暂停所有线程的工作,而让GC专心运行。因此,一些专家认为,GC会让一个程序丧失实时性,执行时间变的无上限,效率不可估计。但真实情况是,正常情况下GC的开销完全可以忽略,况且咱们用的本来都不是实时的操作系统,谈什么实时性?而非正常情况下,GC开销异常时,恰恰是需要有经验的程序员解决的问题。
我认为让程序员把时间花在异常情况下的优化,比花在未见异常的时候精心管理内存,要更合理。毕竟优化的第一定律是:先不要优化。
另辟蹊径的Rust
Rust采用了一种全新的机制来避免GC。它也需要程序员在开发时主动配合来管理内存。
具体来讲,Rust认为内存资源和程序中的对象实例(变量)是紧密绑定的。当你创建一个对象时,就也分配了它的资源;一个对象走出作用范围时,它的资源就会被释放。资源不能复制,但可以在对象之间“借”或者“送”。
程序员要做的是,明确指名资源的“借”和“送”的操作。资源借出去了,你自己就没有了,但等对方消失了,资源就自然还回来了;资源送出去了,你自己也没有了,而且再也要不回来了。这样一来,只要那些拥有资源的对象走出了作用范围,被销毁了,就可以安全地把它的资源一并回收。
既然这一切资源的借和送,是在程序中明确指定的,那么Rust编译器就可以在编译时明确什么时候分配、什么时候释放资源。
原理上,Rust是通过语法方便了原本C/C++程序员需要记在小本子上的“资源属于谁”的问题。所以,如果你玩不转C/C++,分不清楚栈和堆,就很难理解Rust的资源借和送。
Rust和Move
看到这里,你有没有觉得Rust对于内存资源的理解,恰恰就是Move所宣称的对于链上资源的理解——资源不能被复制,只能在账户或者Move对象之间流动。
目前Move还处于早期开发阶段。Move IR的语法本身就大量借用了Rust的语法。可以肯定地说,Move的设计思想本身也很大程度上受到了Rust的启发。
然而,毕竟Move是个新语言,借鉴思想并不意味着必须用Rust实现呀。可能脸书选择Rust只是因为Go是谷歌在维护的缘故。但比既然要做Libra这么大的颠覆性的事情,没有这点胸怀,怎么可能成功?难道以后你脸书就不邀请谷歌加入Libra联盟吗?
Rust vs Go,槽点满满
对一个语言的运用,我觉得有这几个层次:
- 能读懂
- 能调试
- 能修改或写功能模块
- 能设计新项目架构
- 能设计新架构以重构旧项目
首先承认,我对Rust的运用直到现在还停留在1-2之间的程度,而起初仅仅为了看Libra代码才开始学。如果下面喷的不对,欢迎留言指出。
对于Go,我曾经用3个小时的时间认真完成了Tour(https://tour.golang.org/),之后就达到了2-3之间的程度,看懂state-of-the-art工程不成问题。我天真的以为,学Rust也可以花几小时看个Tutorial,加上对C语言malloc/free炉火纯青的运用,应该不难。事实上Rust要达到2以上的水平,绝非几个小时够用的——语言特性太多了。
计算机软件架构的最高水平,就是设计一门简洁而恰到好处的语言。Go所说的只有50页的语言spec,真的不是吹的,其本身的简洁和表达能力的丰富,的确堪称神来之笔。
接下来,我克隆了libra代码库,然后天真的跑了cargo build。足足半个多小时,我都吃了一顿饭回来了,才编译完成。我记得编译一遍内核也就差不多这么久了。此后,哪怕是在代码中加一个print!,重新编译大约都需要1分钟时间,这还取决于修改代码的模块是不是在依赖关系的上层。
最可怕的还不是编译时间,而是一共500多个组件,每个组件编译后的目标文件大概都要几十M,加载一起竟然一共有18G!一个工程18G!大家感受一下。要是同时开发几个Rust项目,电脑硬盘立马告急。要说Rust维护者里面有固态硬盘厂的股份,我绝对相信。
而最终生成的可执行程序,就拿客户端程序client举例,竟然有390M!390M,在20年前就是一个完整的光盘游戏。
没有对比就没有伤害。我的go-libra客户端,核心功能和rust libra客户端没有欠缺,在清除全部缓存的情况下,go build编译用时10秒,最终的可执行程序大小是13M。go build比cargo build快了2个数量级,产生的可执行程序体积小了1个半数量级。
就是这样残酷。