最近博客园上在炒关于C#性能的问题,其实应该说是.NET性能的问题,其中某位仁兄提出,他希望C#能够直接编译为原生代码,而不是在CLR这样一个托管运行时上执行,因为虚拟机啊,JIT什么的性能差。后来发到TL上以后,也有朋友认为,“基于虚拟机的语言都是大公司为了利益在推动,说白了就是政治”,因此“对C#提高性能的建议感到可笑,因为它本来就不是用来开发高性能程序的”,再有,“C、C++已经明确不和这些后进争所谓的‘容易开发’的头衔”,那么其他语言为什么要和C++它们比较性能呢?我是托管运行时,或者虚拟机的忠实拥护者,这里谈一下我在这方面的看法。
我并不反对编译为原生代码的语言,尤其是C语言,它的意义在于提供了一种对硬件完全控制的手段,对硬件提供了一种最直接的抽象,几乎可以映射到最终流程控制方式,因此无可替代。C++作为C语言的超集,提供了更丰富的抽象能力(如面向对象和模版化),只是语言本身过于复杂,超过了以我的智商可以承受的范围,因此我学了几次都没怎么学会,现在更是忘得差不多了。不过我认为,越来越多的语言会构建在托管平台上,而不是直接编译成原生代码。因为一个统一的托管运行时会带来很多好处。
首先,统一运行时提供了跨平台的能力,Java便是一典型。.NET上有mono,使用也很广泛,也有不少Unity3D,Gnome DO等成功案例。Novell,包括其他一些公司也在销售基于mono的商业产品(如MonoTouch及Infragistics的ASP.NET Controls组件),我本身也在两年多前就在生产环境上使用了mono,您现在看到的这个博客也是基于Ubuntu Server、mono 2.6 、Apache以及微软开源的ASP.NET MVC 2构建的。虽说从某些层面(如API兼容性)上说,mono的跨平台性远不如Java平台,但它也是一个比较成熟的执行环境,并具备相当程度的跨平台能力——尤其是在mono上开发,MS .NET上运行的时候(虽然我不建议这么做)。如今支持mono的产品、类库数不胜数,我时常调侃道,“如果您的产品不支持mono,还真不好意思和人打招呼”。只可惜,不少人都用一些“不是亲娘生的”类似的调调来否定mono,在我看来没有经过调查研究的看法只能属于“臆断”,而且更是一种FUD了。
即便退一步来说,我们不“跨操作系统”吧。有人说,.NET就支持Windows么,何必搞个虚拟机,还JIT那么麻烦。但事实上,“跨平台”并非指的是简单的“跨操作系统”,而是“跨执行环境”,如Silverlight。事实上,跨计算机体系结构本身也是种跨平台(当然,操作系统其实已经进行了一定的统一抽象了)。因此,虚拟机的目的,是为上层执行体抽象出了统一的运行环境——这其实还是跨平台,这平台不仅仅是指操作系统,整体运行环境之间的差异也是运行时所“抽象”的一部分。比如在并发环境中,不同CPU架构的流水线上的乱序方式不一样,同样的代码执行的效果就可能不同。最典型的例子,便是JVM之前的内存一致性模型控制的比较宽松,导致经典的double check模式在某些CPU上是会失败的。现在Java标准也变得严格了,和.NET CLR一样避免了Store Reordering。这意味着在某些CPU上,会在特定的地方加上Memory Barrier保证执行效果的一致性。在我看来这是更好的可移植性。C++或是C语言等实现“可移植性”的方式,往往是通过为不同环境提供不同的编译器,生成不同的结果,而且会使用“宏”等方式,在代码里写出有平台针对性的代码。
有了统一的运行时,也可以让多语言互操作更为容易,如果没有JVM或CLR,就很难像现在这么轻松地在Scala/Java/Jython/JRuby,或是C#/F#/IronPython/IronRuby,甚至是未来的语言之间进行直接地互操作,更难做到“无缝集成”了。在合适的场景下选用合适的语言,是提高生产力的重要手段。如果没有JVM平台,就很难使用Scala来代替Java这样的劣质语言,而现在Scala便能够保证充分利用Java平台上类库等积淀。
有了“多语言”,那么便会引出虚拟机的另一个作用:让语言实现者和虚拟机实现者的工作可以分离开来,各自优化。虚拟机的实现者可以尽力优化虚拟机的各种表现,为虚拟机加入各种优化措施,而无需让虚拟机的上层语言分别调整。例如JVM性能提高之后,Scala、Java、Jython、JRuby等语言的性能都可以提高,否则各种语言分别优化的代价太高了(当然某些情况还是需要特殊对待的)。要说这方面的实例,在.NET平台上有个开源的组件DLR(动态语言运行时),是在CLR上提供编写动态语言的统一辅助类库。以前它有个缺点,便是启动速度比较慢,因为动态代码的JIT耗时较长,而动态语言的很多场景是“即用即抛”的。后来,DLR增加了一个优化策略,便是一开始直接对抽象语法树直接解释执行,而执行多次之后才使用后台线程将代码JIT成原生代码。有了如此改进,基于DLR的动态语言,如IronRuby和IronPython的启动速度都提高了。
虚拟机/运行时本身可以做的优化也很多,如果真觉得性能不够,那么完全可以在运行前在本地编译成native code,这和直接从源代码变成native code从结果上看没什么区别。但是现实是,很少有人去这么做,因为这么做往往只是节省了JIT的开销,对性能提高效果不大。在不同环境中,此外还有各种优化,比如使用解释执行,而不是JIT以次节省内存消耗,或是在运行时回收JIT的代码(印象中在.NET Compact Framework里有这样的策略,求详情),或是在运行时根据代码逻辑进行二次编译。下面会谈一个例子。另一种典型的优化,一直在研究却还没有真正实现的,虚拟机便是“自动并行”。关于这点,Anders在上次的演讲中多次提到过,要实现这点还需要有各种支持,如声明式编程,提供“无副作用”标记,甚至在语言级别的支持等等。
之前那位朋友提到,C/C++已经明确不与后进语言比生产力了,后进语言也没办法和它们比较性能。对这个观点我持保留意见,因为基于虚拟机的做法,其优化空间还有很多,理论上也可以做到更为彻底,在许多情况下性能完全有超越静态编译语言的可能。这是许多人的看法,而事实也是如此,在某些场景下Java的性能也已经超过了C++。如回到上面的二次编译优化,对于性能优化也大有好处。举一个简单的例子,面向对象语言会出现很多虚方法调用(尤其是在符合一些关于设计的最佳实践时,如“基于抽象编程”),调用需方法需要查方法表找方法入口,最普通的做法就是必须每次根据对象的实际类型查找方法表,找到地址,然后调用。伪代码如下:
调用
但是在实际执行过程中,可能有某个特定类型出现的次数特别多,甚至完全只会出现一种具体类型的实例(抽象只是为了扩展性或是单元测试等等),但因为虚方法的调用语义,我们就无法对这次调用进行内联优化等等。不过在托管的运行时中,如果发现某个特性类型出现次数特别多,则还是可能将那个类型的方法内联进来,然后只在“不是那个实例”的情况下才去查找方法入口。伪代码就是:
根据实际类型找到函数入口
调用
else
执行内联后的类型A的代码
end
类似这种的优化不是空中楼阁,它们在HotSpot(即SunOracle的JVM)是确切实现的。Hotspot的JIT,尤其加上-server开关时,它的优化会变得十分激进,而CLR的JIT与它相比就像是静态编译器了。C++在科学运算中性能高,往往是靠大量的inline和精细资源控制,但是这说实话都还是静态的“手动优化”,如果比实际工程的真实运行情况,比如上面说的虚方法跳转,还真不见得C++可以超过托管代码。虚拟机做的则是动态优化,对于长期执行的代码甚至可能执行的越来越高效,还能够针对环境改变作出调整。总而言之,托管代码可以运用的优化策略实在太多了。随着技术发展,托管代码的速度可以进一步提高,而静态编译代码的空间就小得多了。
对此Milo Yip老大做过一些补充,他指出这方面不同的C++ compiler有不同優化方法,例如VC2008開始有的Profile-Guided Optimizations:
Virtual Call Speculation - If a virtual call, or other call through a function pointer, frequently targets a certain function, a profile-guided optimization can insert a conditionally-executed direct call to the frequently-targeted function, and the direct call can be inlined.
我简单地思考了一下,从理论上说,各种优化策略都可以通过静态编译的方式写入原生代码,因此JIT能做的事情,native code理论上都能做。不过,这就意味着要在每个程序中带上“负责优化”的代码,而不光是程序的“功能代码”。用虚拟机,就意味着所有的程序都共享了虚拟机的这么一套优化机制。如果虚拟机的优化手段改进了,那么所有基于虚拟机的程序都能获利,而静态编译的程序,往往只能靠“重新编译”来得到优化了。
此外,对于面向对象语言来说,虚函数是很常见的,虚函数之间各种组合调用,分支、路径也特别多,到处都可能使用虚函数。我“猜想”,像VC这样的编译器,为每个虚函数调用之处都提供了基于profiling的内联优化是不太现实的。而且,为每个虚函数调用的地方都提供“内联”和“不内联”两种版本也不太可能。而基于虚拟机的话,它就可以动态的进行各种优化,每段JIT后的代码都可以回收再动态生成,这种优化是静态编译难以比拟的。
诚然,就目前来说,就性能方面,许多情况下还是静态编译配合手动细节优化可能获得更好的效率,但利用托管运行时获得的好处也是非常可观的,而且我也一直没有遇到过这方面的效率问题。同时,我认为托管运行时的愿景也十分现实可靠,因此就我看来,托管代码是未来趋势,除了越来越小的特定领域,越来越多的程序和语言会构建于托管平台上。事实上,正因为上面这些好处(例如独立优化),越来越多的语言也开始加入虚拟机这样的策略了。例如Rubinius,PyPy等新的Ruby和Python语言实现,其实都是有个虚拟机这样的机制存在。
最后我再多说一句:我并不是说追求性能的做法不对,我也从来不会说追求性能本身没有意义。但是我不同意说“考察托管语言性能”没有意义,更是完全不同意说托管代码虚拟机“本身意义就不大”,“完全是大公司在推动”,或是“政治因素”云云。技术就是技术,这些技术上投入了无数天才的精力和创意,这不是什么“政治”之类说法就能带过的