放弃Julia?长文解析对Julia的质疑错在哪里

前言

Julia 语言1.0版本近期刚刚正式上线,作为科学和数值计算的神器,Julia引起了业内广泛关注。Julia 语言以速度著称,但在1.0版本上线之前其性能也曾受到诸多质疑,本文是开发者Tom Kwong对质疑声音的回应——为什么“放弃Julia”的批评是轻率的。本文使用Julia0.6版本。

编译:集智翻译组

来源:

tk3369.wordpress.com

原题:

An Updated Analysis for the “Giving Up on Julia” Blog Post

背景

2016年3月的一篇博客,大肆吐槽了一下Julia语言。从这篇博客的评论和最近社区上对Julia语言的讨论上来看, 那篇博客确实是有点争议的。

博客地址:

http://zverovich.net/2016/05/13/giving-up-on-julia.html

那么这篇博客的目的就是来反驳一下那些错误的观点,然后确定一下什么才是我们需要真正去关注的。我会尽量公平地列举不同之处,这样你们就可以根据例子来具体判断。

关于Julia底层性能测试的抱怨

作者引用了几个关于性能测试的github issue。

1.parse_integer benchmark(https://github.com/JuliaLang/julia/issues/4662)的C语言实现不是用用惯用标准编码的,比如strlen函数。2013年10月时核心开发团队承认了这个问题并很快的对其进行了修复(两天内)。

2.也有人提问了Java和Octave实现的不同性能测试的问题。在Github上经过了几轮讨论之后,最终归结于大家对于性能测试的不同理念。我们到底是想要通过性能测试来理解对于相同算法的不同语言间实现的性能还是想知道在这些语言之间用最优化的代码来进行比较呢?Julia的性能测试解释到:

需要主要到的是,性能测试代码并不是用绝对最大性能写的(最快递归计算斐波那契数列第二十个元素的代码是6765)。相反,编写这些性能测试是为了测试每种语言中实现相同算法和代码模式中的性能。

关于优化

在任意可拓展的语言中人们都可以优化任何东西并取得良好的性能。在Python的例子中,我们可以应用Cython,Numba,Numpy和其他的技巧。这是Stephan Karpinski所描述的典型的双语言问题。

即:

1.要优化一个事物有多困难,要付出多大的努力。

2.优化是否会在代码中引入额外的复杂性并阻碍生产力。

Hello World的性能

作者对于一个简单的Hello World程序的运行时间有点不满。包含启动时间很明显是很奇葩且片面的。

问题就是,这有关系吗?也许有,也许没有。

1.如果你有一堆只需要一秒钟就能运行的短时间应用程序,那么较长的启动时间可能对你产生很大的影响。

2.如果你有一个大概要耗时30分钟的大型计算项目,那么0.5秒的启动时间就没什么意义了。对于大多数商业应用程序来说,这才是比较常见的情况,更别提数值运算和科学计算社区了。

博客

《如何让Python跟Julia一样快》

作者指出在博客"如何让Python跟Julia一样快"中有很多方式可以Python更快。这个引用感觉就像是在说“嘿,我们Python开发者已经知道如何让程序跑的更快了,而且没用理由切换到其他语言。”

斐波那契性能测试

我的测试结果显示,Cython-Typed的速度和Julia相当。

只是有几个注意事项

博客的性能测试结果是Julia 80us和Cython-Typed 24us。我不能复现这样的表现。我只能解释说,时代变了,Julia的性能也提高了。

斐波那契函数使用了LRU缓存做了优化。从性能测试角度来看,这不是一个公平的比较,因为算法本身改变了。所以说我们可以忽略这些结果。

NUmba使用LRU-enhanced的进一步优化了代码。出于同样的原因考虑,我们也将忽略这些结果。

Python

In [43]: def fib(n):

...: if n

...: return n

...: return fib(n-1)+fib(n-2)

In [44]: %timeit fib(20)

3.27 ms ± 48.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Cython

In [48]: %%cython

...: def fib_cython(n):

...: if n

...: return n

...: return fib_cython(n-1)+fib_cython(n-2)

In [51]: %timeit fib_cython(20)

1.48 ms ± 329 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Cython Typed

In [71]: %%cython

...: cpdef long fib_cython_type(long n):

...: if n

...: return n

...: return fib_cython_type(n-1)+fib_cython_type(n-2)

In [72]: %timeit fib_cython_type(20)

47.8 µs ± 182 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each

Julia

julia> fib(n) = n

fib (generic function with 1 method)

julia> @btime fib(20)

48.075 μs (0 allocations: 0 bytes)

快速排序

作者将快速排序函数转化为Cython,最后得出结论Numpy的排序时间是最快的。

1.性能测试算法 - Julia(356us) vs Cython (1030) 。

2.基排函数 - Julia(233us) vs Numpy(292us)。

Python

In [83]: def qsort1(a, lo, hi):

...: i = lo

...: j = hi

...: while i

...: pivot = a[(lo+hi) // 2]

...: while i

...: while a[i]

...: i += 1

...: while a[j] > pivot:

...: j -= 1

...: if i

...: a[i], a[j] = a[j], a[i]

...: i += 1

...: j -= 1

...: if lo

...: qsort1(a, lo, j)

...: lo = i

...: j = hi

...: return a

In [84]: %timeit qsort1(lst, 0, len(lst)-1)

13.7 ms ± 140 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Cython

In [100]: %%cython

...: cdef double[:] qsort2(double[:] a, long lo, long hi):

...: cdef:

...: long i, j

...: double pivot

...: i = lo

...: j = hi

...: while i

...: pivot = a[(lo+hi) // 2]

...: while i

...: while a[i]

...: i += 1

...: while a[j] > pivot:

...: j -= 1

...: if i

...: a[i], a[j] = a[j], a[i]

...: i += 1

...: j -= 1

...: if lo

...: qsort2(a, lo, j)

...: lo = i

...: j = hi

...: return a

...:

...: def qsort2_py(a, b, c):

...: return qsort2(a, b, c)

In [105]: %timeit qsort2_py(np.random.rand(5000), 0, 4999)

1.03 ms ± 110 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Numpy

In [61]: %timeit np.sort(lst)

292 µs ± 4.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Julia Microbenchmark

julia> function qsort!(a,lo,hi)

i, j = lo, hi

while i

pivot = a[(lo+hi)>>>1]

while i

while a[i] pivot; j -= 1; end

if i

a[i], a[j] = a[j], a[i]

i, j = i+1, j-1

end

end

if lo

lo, j = i, hi

end

return a

end

qsort! (generic function with 1 method)

julia> sortperf(n) = qsort!(rand(n), 1, n)

sortperf (generic function with 1 method)

julia> @btime sortperf(5000)

355.957 μs (2 allocations: 39.14 KiB)

Julia Base.sort

julia> @btime sort(rand(5000); alg=QuickSort)

233.293 μs (11 allocations: 78.66 KiB)

曼德布罗特集

我的测试结果显示 Numba(159us) vs Julia(72us)。

Numba

In [106]: @jit

...: def mandel_numba(z):

...: maxiter = 80

...: c = z

...: for n in range(maxiter):

...: if abs(z) > 2:

...: return n

...: z = z*z + c

...: return maxiter

In [107]: @jit

...: def mandelperf_numba_mesh():

...: width = 26

...: height = 21

...: r1 = np.linspace(-2.0, 0.5, width)

...: r2 = np.linspace(-1.0, 1.0, height)

...: mandel_set = np.empty((width,height), dtype=int)

...: for i in range(width):

...: for j in range(height):

...: mandel_set[i,j] = mandel_numba(r1[i] + 1j*r2[j])

...: return mandel_set

In [109]: %timeit mandelperf_numba_mesh()

:1: NumbaWarning: Function "mandelperf_numba_mesh" failed type inference: Invalid usage of Function() with parameters ((int64 x 2), dtype=Function())

* parameterized

File "", line 7

[1] During: resolving callee type: Function()

[2] During: typing of call at (7)

@jit

:1: NumbaWarning: Function "mandelperf_numba_mesh" failed type inference: cannot determine Numba type of

File "", line 8

@jit

:1: NumbaWarning: Function "mandelperf_numba_mesh" was compiled in object mode without forceobj=True, but has lifted loops.

@jit

159 µs ± 7.55 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Julia

julia> function mandel(z)

c = z

maxiter = 80

for n = 1:maxiter

if myabs2(z) > 4

return n-1

end

z = z^2 + c

end

return maxiter

end

mandel (generic function with 1 method)

julia> mandelperf() = [ mandel(complex(r,i)) for i=-1.:.1:1., r=-2.0:.1:0.5 ]

mandelperf (generic function with 1 method)

julia> @btime mandelperf()

72.942 μs (1 allocation: 4.44 KiB)

整型转化

注意,我删除了声明语句,因为这个基准测试似乎没有必要使用声明语句。我的测试结果显示:

1.原始 – Numpy (2340 μs) vs. Julia (176 μs)。

2.外循环随机数生成 – Cython (378 µs) vs Julia (176 μs)。

Julia

julia>functionparseintperf(t)

localn, m

fori=1:t

n = rand(UInt32)

s = hex(n)

m = UInt32(parse(Int64,s,16))

end

end

parseintperf (generic function with 1 method)

julia> @btime parseintperf(1000)

176.061 μs (2000 allocations: 93.75 KiB)

Cython(外循环)

In [116]: %%cython

...: import numpy as np

...: import cython

...:

...: @cython.boundscheck(False)

...: @cython.wraparound(False)

...: cpdef parse_int_vec_cython():

...: cdef:

...: long i,m

...: long[:] n

...: n = np.random.randint(0,2**31-1,1000)

...: for i in range(1,1000):

...: m = int(hex(n[i]),16)

...:

In [118]: %timeit parse_int_vec_cython()

378 µs ± 5.29 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Julia(外循环)

julia> function parseintperf2(t)

n = rand(UInt32, t)

for i=1:t

s = hex(n[i])

m = UInt32(parse(Int64,s,16))

end

end

parseintperf2 (generic function with 1 method)

julia> @btime parseintperf2(1000)

176.053 μs (2003 allocations: 97.97 KiB)

呸,让我们回到最初的博客帖子。

语言

作者对Julia语言本身就有一些批评:

不平衡(Unbalanced)的end

我个人认为这并不是什么问题,而是个人喜欢。大多数语言都有一个块的结尾标记。但是我不明白不平衡是什么意思。

使用::

这不是问题,是个人喜好。

非正统的操作符,标点,和多行注释语法

Julia的标点似乎与许多编程语言并没有什么太大的不同。这感觉又是一个个人喜好问题。

1基数的索引(one-based-indexing)

这是我最喜欢的。两个阵营都有优点。这真的取决于你的使用和具体情境。正如大家在论坛上指出的那样,有一些包(比如OffserArray和PemutedDimsArray)使用0作为基数会更加自然。挺好的,至少我们有选择权。

标准文档系统使用Markdown

几个月前我开始使用Julia编程,发现这个文档系统很直观和简单。然而,我认为作者在一篇后续的博客中提供了更多的信息,我认为作者提出了一个不错的观点。看来意见是发生了分歧。

在Julia的案例中,这个选择是在早期使用reST的几个痛点下决定的。在做了各种各样的研究之后。在我看来,ReStructuredText更加严格和实用,而Markdown更加容易学习和实用。

最后,这里其实并没有对与错。我们知道开发人员从来没有在一件事上达成过一致。幸运的是,开发人员总数可以选择自己做决定。我们完全尊重那些选择ReStructuredText而不是Markdown的项目,反之亦然。

安全

作者认为,使用ccall时Julia很容易产生内存区段错误。我发现它与现状的Julia版本不符。由于错误的库名称和数据类型,我无法生成一个内存区段错误。然而,我可以收到一个很好的错误信息。

julia> val = ccall((:getenv, "libc.so.6"), Ptr, (Ptr,), var)

ERROR: error compiling anonymous: could not load library "libc.so.6"

dlopen(libc.so.6.dylib, 1): image not found

julia> val = ccall((:getenv, "libc.dylib"), Ptr, (Ptr,), 123)

ERROR: MethodError: no method matching unsafe_convert(::Type}, ::Int64)

Closest candidates are:

unsafe_convert(::Type}, ::Symbol) at pointer.jl:35

unsafe_convert(::Type}, ::String) at pointer.jl:37

unsafe_convert(::Type, ::T

Printf/Sprintf

对于生成大量宏的@sprintf和@printf,作者好像对此有点不满。

那时候可能是真的,但是现在有很多解决方法。这是一个正在经历快速创新的领域。一些Julia的包可以解决这个问题。比如:Formatting.jl和StringLiterals.jl。

下面这行代码只生成63行本地代码,相比之下,作者在过去说有500+行。

julia> @code_native Formatting.printfmt(" ", "abc", 12)

如果我编译下面的C代码,我只会得到21行汇编指令。想要打败C语言还是有点困难的。

void f(char *buffer, const char *a, double b) {

sprintf(buffer, "this is a %s %g", a, b);

}

单元测试

作者认为,Julia的单元测试库是受限的:

单元测试库是非常基本的,至少对于C++和Java而言是这样的。可以说,FactCheck是最受欢迎的选择,但是除了奇怪的API之外,它是相当受限的,而且也基本不再开发。

当我在开发SASLib.jl的时候,我只使用Base.Test。我觉得它对用户是非常友好的,并且它做了我正需要的事情。作者提到了FactCheck包,这似乎是一个合理的附加条件。我以前没用过FactCheck,所以我在这里就不发表额外的见解了。

发展

有趣的是,作者说他考虑过为Julia项目做贡献,但是他不喜欢Julia的运行时是由几种编程语言C/C++,Lisp,和Julia构建的。

我不明白这有什么大不了的。如果我统计一下对于Julia 0.6各个语言分别有多少行代码:

Julia 175,147

C/C++ 70,694

Femtolisp 8,270

C/C++的代码对于Julia的基础运行时是非常需要的。然后Julia语言的大部分使用Julia语言本身构建的。所以我们讨论一下Femtolisp 3.3%的代码,这是一个类schema的lisp实现。

我最近有过为Julia项目做贡献的经历。论坛上的社区成员非常友好,核心开发团队也总是在线。如果我想做更多贡献,那就没什么障碍,我可以很容易地得到很多帮助。

最后,作者引用了一篇第三方的问题,题目是“Julia是如何放慢脚步,并从热点中消失”。我不知道当时2015年的环境,但是考虑到Julia刚从2017年得到种子投资。我预计它会加速而不是减缓。时间会告诉我们,当Julia接近1.0里程碑时,这个动力是否会继续下去。

最后

我希望这篇博文对任何评价Julia的人都有用。

推荐课程

https://campus.swarma.org/gcou=388

该教程视频同时发布在哔哩哔哩弹幕网:

视频地址一:https://www.bilibili.com/video/av28248187

视频地址二:https://www.bilibili.com/video/av28178443

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180814G1PZGG00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励