为什么Julia这么快?

12,807 阅读8分钟

我本来说今年不会写文章了,不过这个编辑器比知乎的好用多了啊!公式也很棒!以后不想再写知乎了,我来这里写量子计算有人看么?!真香!给你们写编辑器的程序员加个鸡腿好不好!好了回到正题

这是很多人都会问的一个问题。

我自己一直觉得自己讲不清楚。都是codegen,凭啥Julia快,Julia能用LLVM,Python还有别的各种也能用啊?Julia到底有什么好的?最近因为1.0了于是和一些人讨论了下(比如红红),然后也Google了一下,这篇文章里的内容大多整理自这些我听到和看到的观点。

然后我也被纠正了一个观点:Julia只适合科学计算。听了这些观点以后我想Julia如果生态能够做好,在这个阶段能够吸引到有技术能力的开发者尝试或许可以出现很多不错的东西。

从 Julia 的第一篇论文说起

让我们回到Julia的第一篇论文里去(我只是大概翻译了部分,感兴趣还请去看原文):

arxiv.org/pdf/1209.51…

在文章的开头部分,可以看到实际上在一开始 Jeff Bezanson,Stefan Karpinski,Viral B. Shah,Alan Edelman 所尝试要解决的是一般的两语言问题,两语言问题往往表现为对易用性(Convenience)和性能(performance)的妥协,程序员在需要描述算法的高级(high level)而复杂的逻辑时使用容易使用的动态语言,而在性能敏感的地方往往会使用C,Fortran。这样的方式对于一些应用很有效,但也是有缺点的。这在写一些并行代码的时候,算法复杂性会变得很大。而编写向量化的(vectorized)代码,对于很多问题来说非常的不自然,并且可能会产生本可以由显示的for循环所避免的中间变量。

由于需要去考虑两种语言之间的类型转换和内存管理,编写两种语言的代码可能会比用任何其中一种语言编写的代码都要复杂。而如果处理不好两层代码之间的界面,则有可能会大大增加优化的难度。

那么另外一种解决方案就是增强我们已有的动态语言的性能,例如Python,比如像PyPy这样的工程实际上已经非常成功了[1]。这些已有的工程都是尝试去优化一个已有的语言,这是非常有用的,因为已有的代码可以直接获益。但是这些方案都无法真正解决两语言问题。以解释器语言为假设的语言设计决定使得其能够生成高效代码的能力收到了破坏。正如Henry Baker对Common Lisp的观察:

...the polymorphic type complexity of the Common LISP library functions is mostly gratuitous, and both the efficiency of compiled code and the efficiency of the programmer could be increased by rationalizing this complexity. [2][3][4]

Julia在设计之初就考虑如何让其利用现代的技术去高效加速动态语言。从结果上来看Julia在提供了像Python,Lisp,Ruby这样交互式编程和动态性的同时,也有着静态编译语言一般地性能。

Julia的性能主要是由这样三点特性所获的:

  1. 通过多重派发(multiple dispatch)自然获得地充分的类型信息
  2. 对动态类型激进地(aggressive)代码特化(code specialization,比如C++的template就是一个例子,注)
  3. 利用LLVM的JIT编译

实际上到这里我们就已经看到Julia的速度不是简单地靠产生LLVM,而是由语言本身的设计所带来的

在过去尝试优化动态语言的动作中,研究人员已经观察到了实际上程序可能并不是程序员们所想象的那么动态[5]

We found that dynamic features are pervasive throughout the benchmarks and the libraries they include, but that most uses of these features are highly constrained...

从这一点来说,现有的编程语言设计可能并未找到一个良好的平衡点。有很多代码实际上都可以是静态类型的,并被更高效地执行。但是由于语言本身的设计并不能实现这一点。我们假设以下的 “动态性” 是更加有用的:

  1. 能够在加载和编译时期运行代码的能力,这可以使得编译系统和配置文件更容易
  2. 将一个一般的任意类型(Any type)作为唯一的真正的静态类型,使得可以在需要的时候忽略静态类型
  3. 不要拒绝形式上优美的代码
  4. 程序行为仅仅由运行时的类型决定(例如没有静态重载)

而Julia拒绝了一些阻碍优化的特性(例如CLOS [6]),而有如下的限制:

  1. 类型本身是不可变的
  2. 一个值的类型在其生存周期内是不可变的
  3. 局部变量的环境不会被具体化(reified)
  4. 程序代码不可变(注,但是可以产生新的代码然后被执行,这大概体现在宏的world里)
  5. 不是所有的绑定都是可变的(允许定义常数)

这些限制使得编译器可以看到所有具体的本地变量,然后仅仅根据局部信息就可以进行分析。

文章我就不全翻译了,那么Stefan在mail list里大概总结了一下,Julia的性能主要是由以下几点带来的:

  1. an expressive parametric type system, allowing optional type annotation
  2. multiple dispatch using type annotations to select implementations
  3. dataflow type inference, allowing most expressions to be concretely typed
  4. careful design of the language and standard library to allow analysis
  5. aggressive code specialization on run-time types
  6. just-in-time compilation (using LLVM).

可以看到作为语言本身特性的参数类型系统和多重派发(这甚至直接影响了Julia代码编写时的设计)是非常重要的。

同时Stefan也评论:

LLVM is great, but it's not magic. Using it does not automatically make a language implementation fast. What LLVM provides is freedom from doing your own native code generation, as well as a number standard low-level optimizations. You still have to generate good LLVM code. The LLVM virtual machine is a typed register machine with an infinite number of write-once registers – it's easier to work with than actual machine code, but not that far removed (which is the whole point).

实际上我们可以看到,说现在codegen已经烂大街的言论是非常浅薄的。而认为Julia毫无创新只是C++,R,Python的混合的言论也是(无法描述)的。

总结一下,Julia实际上是对原本的一些动态语言做了一些限制的结果,它在尝试寻找一个更优的平衡点。说它继承了Python的简单是错误的,说它继承了R也是错误的,Julia也更没有继承C++。Julia所想表达的是,我们也许可以牺牲一些不那么重要的动态性,就能够换来非常惊人的速度。

至于这样的平衡是否就是最优的,那么就仁者见仁智者见智了吧。

一些尝试

那么实际上,有一些尝试挑战C/Fortran的尝试:

纯Julia实现一个BLAS:

discourse.julialang.org/t/we-can-wr…

纯Julia实现的一个HDF5:

github.com/simonster/J…

纯Julia实现的一个JSON(根据红红评价,这个他可以做的更好):

github.com/quinnj/JSON…

从这一点看来,我的认识其实之前也是不正确的,除了更加统一的多维数组(这对物理学家非常重要,不然也不会有那么多人还用Fortran)以外,也许我们还可以有更加广泛的应用,这不仅仅限于科学计算,机器学习,而是更多的过去需要两语言问题来解决的地方。

但是相对的,过去用一种语言就可以解决的问题,或许这样一个大杀器也用起来并不顺手。我想这样大概足够客观地描述Julia了,大家也可以从中去体会到它适合什么场景不适合什么场景。

最后,我个人觉得,目前Julia不适合小白。也不适合想要找工作的人。但是它更适合那些过去被两语言问题所折磨的人。


[1]: C. F. Bolz, A. Cuni, M. Fijalkowski, and A. Rigo. Tracing the meta-level: Pypy’s tracing jit compiler. In Proceedings of the 4th workshop on the Implementation, Compilation, Optimization of Object-Oriented Languages and Programming Systems, ICOOOLPS ’09, pages 18–25, New York, NY, USA, 2009. ACM.

[2]: H. G. Baker. The nimble type inferencer for common lisp-84. Technical report, Tech. Rept., Nimble Comp, 1990.

[3]: R. A. Brooks and R. P. Gabriel. A critique of common lisp. In Proceedings of the 1984 ACM Symposium on LISP and functional programming, LFP ’84, pages 1–8, New York, NY, USA, 1984. ACM.

[4]: F. Morandat, B. Hill, L. Osvald, and J. Vitek. Evaluating the design of the R language. In J. Noble, editor, ECOOP 2012 Object-Oriented Programming, volume 7313 of Lecture Notes in Computer Science, pages 104–131. Springer Berlin / Heidelberg, 2012.

[5]: M. Furr, J.-h. D. An, and J. S. Foster. Profile-guided static typing for dynamic scripting languages. SIGPLAN Not., 44:283–300, Oct. 2009.

[6]: H. G. Baker. Clostrophobia: its etiology and treatment. SIGPLAN OOPS Mess., 2(4): 4–15, Oct. 1991.