为什么Julia比Python快?因为天生理念就更先进啊

2019-09-29 02:03栏目:文史百科

Julia 语言因为「快」和「简洁」可兼得而闻名,我们可以用类似 Python 的优美语句获得类似 C 的性能。那么你知道为什么 Julia 比 Python 快吗?这并不是因为更好的编译器,而是一种更新的设计理念,关注「人生苦短」的 Python 并没有将这种理念纳入其中。

图片 1

其实像以前 C 或其它主流语言在使用变量前先要声明变量的具体类型,而 Python 并不需要,赋值什么数据,变量就是什么类型。然而没想到正是这种类型稳定性,让 Julia 相比 Python 有更好的性能。

选择 Julia 的最主要原因:要比其他脚本语言快得多,让你拥有 Python/Matlab /R 一样快速的开发速度,同时像 C/Fortan 那样高效的运行速度。

Julia 的新手可能对下面这些描述略为谨慎:

许多人认为 Julia 快是因为它使用的是 JIT 编译器,即每一条语句在使用前都先使用编译函数进行编译,不论是预先马上编译或之前先缓存编译。这就产生了一个问题,即 Python/R 和 MATLAB 等脚本语言同样可以使用 JIT 编译器,这些编译器的优化时间甚至比 Julia 语言都要久。所以为什么我们会疯狂相信 Julia 语言短时间的优化就要超过其它脚本语言?这是一种对 Julia 语言的完全误解。

展开剩余93%

在本文中,我们将了解到 Julia 快是因为它的设计决策。它的核心设计决策:通过多重分派的类型稳定性是允许 Julia 能快速编译并高效运行的核心,本文后面会具体解释为什么它是快的原因。此外,这一核心决策同时还能像脚本语言那样令语法非常简洁,这两者相加可以得到非常明显的性能增益。

但是,在本文中我们能看到的是 Julia 不总像其他脚本语言,我们需要明白 Julia 语言因为这个核心决策而有一些「损失」。理解这种设计决策如何影响你的编程方式,对你生成 Julia 代码而言非常重要。

为了看见其中的不同,我们可以先简单地看看数学运算案例。

Julia 中的数学运算

总而言之,Julia 中的数学运算看起来和其他脚本语言是一样的。值得注意的一个细节是 Julia 的数值是「真数值」,在 Float64 中真的就和一个 64 位的浮点数值一样,或者是 C 语言的「双精度浮点数」。一个 Vector{Float64} 中的内存排列等同于 C 语言双精度浮点数数组,这都使得它与 C 语言的交互操作变得简单(确实,某种意义上 Julia 是构建在 C 语言顶层的),且能带来高性能(对 NumPy 数组来说也是如此)。

Julia 中的一些数学:

a = 2 2

b = a/3

c = a÷3#div tab completion, means integer division

d = 4*5

println([a;b;c;d])

output:[4.0,1.33333,1.0,20.0]

此外,数值乘法在后面跟随着变量的情况下允许不使用运算符 *,例如以下的计算可通过 Julia 代码完成:

α = 0.5

∇f = α*u; ∇f

sin

output:-2.4492935982947.64e-16

类型稳定和代码自省

类型稳定,即从一种方法中只能输出一种类型。例如,从 *(:: Float64,:: Float64) 输出的合理类型是 Float64。无论你给它的是什么,它都会反馈一个 Float64。这里是一种多重分派(Multiple-Dispatch)机制:运算符 * 根据它看到的类型调用不同的方法。当它看到 floats 时,它会反馈 floats。Julia 提供代码自省(code introspection)宏,以便你可以看到代码实际编译的内容。因此 Julia 不仅仅是一种脚本语言,它更是一种可以让你处理汇编的脚本语言!与许多语言一样,Julia 编译为 LLVM(LLVM 是一种可移植的汇编语言)。

@code_llvm 2*5

; Function *

; Location: int.jl:54

define i64 @"julia_*_33751" {

top:

%2= mul i64 %1, %0

ret i64 %2

}

这个输出表示,执行浮点乘法运算并返回答案。我们甚至可以看一下汇编:

@code_llvm 2*5

.text

; Function * {

; Location: int.jl:54

imulq %rsi, %rdi

movq %rdi, %rax

retq

nopl (%rax,%rax)

;}

这表示*函数已编译为与 C / Fortran 中完全相同的操作,这意味着它实现了相同的性能(即使它是在 Julia 中定义的)。因此,不仅可以「接近」C 语言的性能,而且实际上可以获得相同的 C 代码。那么在什么情况下会发生这种事情呢?

关于 Julia 的有趣之处在于,我们需要知道什么情况下代码不能编译成与 C / Fortran 一样高效的运算?这里的关键是类型稳定性。如果函数是类型稳定的,那么编译器可以知道函数中所有节点的类型,并巧妙地将其优化为与 C / Fortran 相同的程序集。如果它不是类型稳定的,Julia 必须添加昂贵的「boxing」以确保在操作之前找到或者已明确知道的类型。

这是 Julia 和其他脚本语言之间最为关键的不同点!

好处是 Julia 的函数在类型稳定时基本上和 C / Fortran 函数一样。因此^很快,但既然 ^(:: Int64,:: Int64)是类型稳定的,那么它应输出什么类型?

2^5

output:32

2^-5

output:0.03125

这里我们得到一个错误。编译器为了保证 ^ 返回一个 Int64,必须抛出一个错误。如果在 MATLAB,Python 或 R 中执行这个操作,则不会抛出错误,这是因为那些语言没有围绕类型稳定性构建整个语言。

当我们没有类型稳定性时会发生什么呢?我们来看看这段代码:

@code_native ^

.text

; Function ^ {

; Location: intfuncs.jl:220

pushq %rax

movabsq $power_by_squaring, %rax

callq *%rax

popq %rcx

retq

nop

;}

现在让我们定义对整数的取幂,让它像其他脚本语言中看到的那样「安全」:

function expo

ify>0

returnx^y

else

x = convert(Float64,x)

returnx^y

end

end

output:expo (generic function with 1 method)

确保它有效:

println)

expo

output:32

0.03125

当我们检查这段代码时会发生什么?

@code_native expo

.text

; Function expo {

; Location: In[8]:2

pushq %rbx

movq %rdi, %rbx

; Function >; {

; Location: operators.jl:286

; Function <; {

; Location: int.jl:49

testq %rdx, %rdx

;}}

jle L36

; Location: In[8]:3

; Function ^; {

; Location: intfuncs.jl:220

movabsq $power_by_squaring, %rax

movq %rsi, %rdi

movq %rdx, %rsi

callq *%rax

;}

movq %rax,

movb $2, %dl

xorl

版权声明:本文由威尼斯网站发布于文史百科,转载请注明出处:为什么Julia比Python快?因为天生理念就更先进啊