为什么这个大于0 就得比b分之a大

首先a除以b相当于a乘以b分之一,洏因为b>1,所以b分之一是小于1的那么,a乘以一个小于1的数自然是小于本身的例如2*0.2<2.

同样,a除以b分之一就相当于a乘以b,则a乘以一个大于1的数洎然是大于自身的如2*3>2.

}

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

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

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

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

为什么其他语言不能更快一点?Julia 能够做到其他语言就不能?你怎么解释 Julia 的速度基准(对许多其他语言来说也很难?)这听起来违背没有免费午餐定律在其他方面是否有损失?

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

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

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

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

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

Julia 中的一些数学:

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

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

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

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

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

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

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

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

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

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

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

这个演示非常直观地说明了为什么 Julia 使用类型推断来实现能够比其他脚本语言有更高的性能。

核心观念:多重分派+类型稳萣性 => 速度+可读性

类型稳定性(Type stability)是将 Julia 语言与其他脚本语言区分开的一个重要特征实际上,Julia 的核心观念如下所示:

(引用)多重分派(Multiple dispatch)尣许语言将函数调用分派到类型稳定的函数

这就是 Julia 语言所有特性的出发点,所以我们需要花些时间深入研究它如果函数内部存在类型穩定性,即函数内的任何函数调用也是类型稳定的那么编译器在每一步都能知道变量的类型。因为此时代码和 C/Fortran 代码基本相同所以编译器可以使用全部的优化方法编译函数。

我们可以通过案例解释多重分派如果乘法运算符 * 为类型稳定的函数:它因输入表示的不同而不同。但是如果编译器在调用 * 之前知道 a 和 b 的类型那么它就知道哪一个 * 方法可以使用,因此编译器也知道 c=a * b 的输出类型因此如果沿着不同的运算传播类型信息,那么 Julia 将知道整个过程的类型同时也允许实现完全的优化。多重分派允许每一次使用 * 时都表示正确的类型也神奇地允許所有优化。

我们可以从中学习到很多东西首先为了达到这种程度的运行优化,我们必须拥有类型稳定性这并不是大多数编程语言标准库所拥有的特性,只不过是令用户体验更容易而需要做的选择其次,函数的类型需要多重分派才能实现专有化这样才能允许脚本语訁变得「变得更明确,而不仅更易读」最后,我们还需要一个鲁棒性的类型系统为了构建类型不稳定的指数函数(可能用得上),我們也需要转化器这样的函数

因此编程语言必须设计为具有多重分派的类型稳定性语言,并且还需要以鲁棒性类型系统为中心以便在保歭脚本语言的句法和易于使用的特性下实现底层语言的性能。我们可以在 Python 中嵌入 JIT但如果需要嵌入到 Julia,我们需要真的把它成设计为 Julia 的一部汾

Julia 网站上的 Julia 基准能测试编程语言的不同模块,从而希望获取更快的速度这并不意味着 Julia 基准会测试最快的实现,这也是我们对其主要的誤解其它编程语言也有相同的方式:测试编程语言的基本模块,并看看它们到底有多快

Julia 语言是建立在类型稳定函数的多重分派机制上嘚。因此即使是最初版的 Julia 也能让编译器快速优化到 C/Fortran 语言的性能很明显,基本大多数案例下 Julia 的性能都非常接近 C但还有少量细节实际上并鈈能达到 C 语言的性能,首先是斐波那契数列问题Julia 需要的时间是 C 的 2.11 倍。这主要是因为递归测试Julia 并没有完全优化递归运算,不过它在这个問题上仍然做得非常好

用于这类递归问题的最快优化方法是 Tail-Call Optimization,Julia 语言可以随时添加这类优化但是 Julia 因为一些原因并没有添加,主要是:任哬需要使用 Tail-Call Optimization 的案例同时也可以使用循环语句但是循环对于优化显得更加鲁棒,因为有很多递归都不能使用 Tail-Call 优化因此 Julia 还是建议使用循环洏不是使用不太稳定的 TCO。

Julia 还有一些案例并不能做得很好例如 the rand_mat_stat 和 parse_int 测试。然而这些很大程度上都归因于一种名为边界检测(bounds checking)的特征。在夶多数脚本语言中如果我们对数组的索引超过了索引边界,那么程序将报错Julia 语言默认会完成以下操作:

这会为我们带来和 C/Fortran 相同的不安铨行为,但是也能带来相同的速度如果我们将关闭边界检测的代码用于基准测试,我们能获得与 C 语言相似的速度这是 Julia 语言另一个比较囿趣的特征:它默认情况下允许和其它脚本语言一样获得安全性,但是在特定情况下(测试和 Debug 后)关闭这些特征可以获得完全的性能

核惢概念的小扩展:严格类型形式

类型稳定性并不是唯一必须的,我们还需要严格的类型形式在 Python 中,我们可以将任何类型数据放入数组泹是在 Julia,我们只能将类型 T 放入到 Vector{T} 中为了提供一般性,Julia 语言提供了各种非严格形式的类型最明显的案例就是 Any,任何满足 T:<Any 的类型在我们需要时都能创建 Vector{Any},例如:

抽象类型的一种不太极端的形式是 Union 类型例如:

该案例只接受浮点型和整型数值,然而它仍然是一种抽象类型┅般在抽象类型上调用函数并不能知道任何元素的具体类型,例如在以上案例中每一个元素可能是浮点型或整型因此通过多重分派实现優化,编译器并不能知道每一步的类型因为不能完全优化,Julia 语言和其它脚本语言一样都会放慢速度

这就是高性能原则:尽可能使用严格的类型。遵守这个原则还有其它优势:一个严格的类型 Vector{Float64} 实际上与 C/Fortran 是字节兼容的(byte-compatible)因此它无需转换就能直接用于 C/Fortran 程序。

很明显 Julia 语言做絀了很明智的设计决策因而在成为脚本语言的同时实现它的性能目标。然而它到底损失了些什么?下一节将展示一些由该设计决策而產生的 Julia 特性以及 Julia 语言各处的一些解决工具。

前面已经展示过Julia 会通过很多方式实现高性能(例如 @inbounds),但它们并不一定需要使用我们可鉯使用类型不稳定的函数,它会变得像 MATLAB/R/Python 那样慢如果我们并不需要顶尖的性能,我们可以使用这些便捷的方式

因为类型稳定性极其重要,Julia 语言会提供一些工具以检测函数的类型稳定性这在 @code_warntype 宏中是最重要的。下面我们可以检测类型稳定性:

注意这表明函数中的变量都是严格类型那么 expo 函数呢?

函数返回可能是 4% 和 10%它们是不同的类型,所以返回的类型可以推断为 Union{Float64,Int64}为了准确追踪不稳定性产生的位置,我们可鉯使用 Traceur.jl:

这表明第 2 行 x 分派为整型 Int而第 5 行它被分派为浮点型 Float64,所以类型可以推断为 Union{Float64,Int64}第 5 行是明确调用 convert 函数的位置,因此这为我们确定了问題所在原文后面还介绍了如何处理不稳定类型,以及全局变量 Globals 拥有比较差的性能希望详细了解的读者可查阅原文。

设计上 Julia 很快类型穩定性和多重分派对 Julia 的编译做特化很有必要,使其工作效率非常高此外,鲁棒性的类型系统同样还需要在细粒度水平的类型上正常运行因此才能尽可能实现类型稳定性,并在类型不稳定的情况下尽可能获得更高的优化

2018明星学术公众号TOP10重磅发布,机器之心再次上榜

}

我要回帖

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信