Tl;博士:
- Julia 包有一个预编译阶段。这有助于使所有进一步的
using 调用更快,代价是第一个调用存储一些编译数据。这仅在每次包更新时触发。
-
using 必须拉入包,这需要一点时间(取决于可以预编译多少)。
- 预编译不是“完整的”,所以当你第一次运行一个函数时,即使是从一个包中,它也必须编译。
- Julia 开发人员知道这一点,并且已经计划通过使预编译更加完整来摆脱 (2) 和 (3)。还有计划减少编译时间,我不知道具体细节。
- 所有 Julia 函数都专门针对给定的类型,并且每个函数都是一个单独的类型,因此 DiffEq 的内部函数专门针对您给定的每个 ODE 函数。
- 在大多数需要长时间计算的情况下,(5) 实际上并不重要,因为您不会经常更改函数(如果是,请考虑更改参数)。
- 但是 (6) 在交互式使用时确实很重要。它让人感觉不那么“流畅”。
- 我们可以摆脱对 ODE 函数的这种特殊化,但这不是默认设置,因为它会导致 2x-4x 的性能命中。也许将来会成为默认设置。
- 我们在这个问题上的预编译后计时仍然比 SciPy 的包装 Fortran 求解器等解决此类问题的时间好 20 倍。所以这都是编译时问题,而不是运行时问题。编译时间基本上是恒定的(调用相同函数的较大问题具有大致相同的编译),因此这实际上只是一个交互性问题。
- 我们(以及整个 Julia)在未来可以并且会在交互方面做得更好。
完整说明
这真的不是一个DifferentialEquations.jl 的东西,这只是一个Julia 包的东西。 25s 必须包括预编译时间。第一次加载 Julia 包时,它会进行预编译。然后在下一次更新之前不需要再次发生这种情况。这可能是最长的初始化时间,而且对于DifferentialEquations.jl 来说也很长,但同样只有在您每次更新包代码时才会发生这种情况。然后,每次using 的初始化成本都会很小。 DiffEq 相当大,所以初始化需要一点时间:
@time using DifferentialEquations
5.201393 seconds (4.16 M allocations: 235.883 MiB, 4.09% gc time)
然后如 cmets 中所述,您还拥有:
@time using Plots
6.499214 seconds (2.48 M allocations: 140.948 MiB, 0.74% gc time)
然后,第一次运行
function lorenz(t,u,du)
du[1] = 10.0*(u[2]-u[1])
du[2] = u[1]*(28.0-u[3]) - u[2]
du[3] = u[1]*u[2] - (8/3)*u[3]
end
u0 = [1.0;1.0;1.0]
tspan = (0.0,100.0)
prob = ODEProblem(lorenz,u0,tspan)
@time sol = solve(prob,reltol=1e-8,abstol=1e-8,saveat=collect(0:0.01:100))
6.993946 seconds (7.93 M allocations: 436.847 MiB, 1.47% gc time)
但是第二次和第三次:
0.010717 seconds (72.21 k allocations: 6.904 MiB)
0.011703 seconds (72.21 k allocations: 6.904 MiB)
那么这里发生了什么? Julia 第一次运行一个函数时,它会编译它。所以当你第一次运行solve 时,它会在运行时编译它的所有内部函数。所有进行的时间都将没有编译。 DifferentialEquations.jl 也专门研究函数本身,所以如果我们改变函数:
function lorenz2(t,u,du)
du[1] = 10.0*(u[2]-u[1])
du[2] = u[1]*(28.0-u[3]) - u[2]
du[3] = u[1]*u[2] - (8/3)*u[3]
end
u0 = [1.0;1.0;1.0]
tspan = (0.0,100.0)
prob = ODEProblem(lorenz2,u0,tspan)
我们将再次产生一些编译时间:
@time sol =
solve(prob,reltol=1e-8,abstol=1e-8,saveat=collect(0:0.01:100))
3.690755 seconds (4.36 M allocations: 239.806 MiB, 1.47% gc time)
原来是这样,现在是为什么。这里有几件事。首先,Julia 包没有完全预编译。它们不会在会话之间保留实际方法的缓存编译版本。这是 1.x 版本列表中要做的事情,这将摆脱第一次命中,类似于仅调用 C/Fortran 包,因为它会提前大量编译 (AOT)职能。所以这很好,但现在只需注意有一个启动时间。
现在让我们谈谈更改功能。 Julia 中的每个函数都会自动专门处理其参数 (see this blog post for details)。这里的关键思想是 Julia 中的每个函数都是一个单独的具体类型。所以,由于这里的问题类型是参数化的,改变函数会触发编译。请注意,就是这种关系:您可以更改函数的参数(如果您有参数),您可以更改初始条件等,但它只是更改触发重新编译的类型。
值得吗?也许。我们希望专门为难以计算的东西快速处理。编译时间是恒定的(即你可以求解一个 6 小时的 ODE,它仍然需要几秒钟),因此计算成本高的计算不会在这里受到影响。在这里运行数千个参数和初始条件的蒙特卡罗模拟不会受到影响,因为如果您只是更改初始条件和参数的值,那么它将不会重新编译。但是,在您更改功能的地方进行交互使用确实会受到第二次左右的影响,这并不好。 Julia 开发人员对此的一个回答是,在 Julia 1.0 之后花费时间来加快编译时间,这是我不知道细节的事情,但我确信这里有一些唾手可得的成果。
我们可以摆脱它吗?是的。 DiffEq Online 不会为每个函数重新编译,因为它面向在线使用。
function lorenz3(t,u,du)
du[1] = 10.0*(u[2]-u[1])
du[2] = u[1]*(28.0-u[3]) - u[2]
du[3] = u[1]*u[2] - (8/3)*u[3]
nothing
end
u0 = [1.0;1.0;1.0]
tspan = (0.0,100.0)
f = NSODEFunction{true}(lorenz3,tspan[1],u0)
prob = ODEProblem{true}(f,u0,tspan)
@time sol = solve(prob,reltol=1e-8,abstol=1e-8,saveat=collect(0:0.01:100))
1.505591 seconds (860.21 k allocations: 38.605 MiB, 0.95% gc time)
现在我们可以更改函数而不产生编译成本:
function lorenz4(t,u,du)
du[1] = 10.0*(u[2]-u[1])
du[2] = u[1]*(28.0-u[3]) - u[2]
du[3] = u[1]*u[2] - (8/3)*u[3]
nothing
end
u0 = [1.0;1.0;1.0]
tspan = (0.0,100.0)
f = NSODEFunction{true}(lorenz4,tspan[1],u0)
prob = ODEProblem{true}(f,u0,tspan)
@time sol =
solve(prob,reltol=1e-8,abstol=1e-8,saveat=collect(0:0.01
:100))
0.038276 seconds (242.31 k allocations: 10.797 MiB, 22.50% gc time)
并且 tada,通过将函数包装在 NSODEFunction(内部使用 FunctionWrappers.jl)中,它不再专门针对每个函数,并且您在每个 Julia 会话中点击编译时间一次(然后一旦缓存,每个包一次更新)。但请注意,这大约是 so I am not sure if it will be enabled by default 的 2x-4x 成本。我们可以在问题类型构造函数中默认实现这一点(即默认情况下没有额外的专业化,但用户可以以交互性为代价选择更快的速度)但我不确定这里有什么更好的默认值(随意用你的想法评论这个问题)。但它肯定会在 Julia 更改其关键字参数后很快被记录下来,因此“无编译”模式将成为使用它的标准方式,即使不是默认方式。
但只是为了把它放在透视图中,
import numpy as np
from scipy.integrate import odeint
y0 = [1.0,1.0,1.0]
t = np.linspace(0, 100, 10001)
def f(u,t):
return [10.0*(u[1]-u[0]),u[0]*(28.0-u[2])-u[1],u[0]*u[1]-(8/3)*u[2]]
%timeit odeint(f,y0,t,atol=1e-8,rtol=1e-8)
1 loop, best of 3: 210 ms per loop
我们正在研究是否应该将这种交互便利性默认设置为比此处 SciPy 的默认设置快 5 倍而不是 20 倍(尽管我们的默认设置通常比 SciPy 使用的默认设置更准确,但这是另一个数据时间可以在基准中找到或只是询问)。一方面它易于使用是有意义的,但另一方面,如果重新启用长计算的专业化并且不知道蒙特卡洛(这是你真正想要速度的地方),那么那里的很多人会受到 2 到 4 倍的性能影响,这可能需要额外的几天/几周的计算。呃……艰难的选择。
因此,最终混合了优化选择和 Julia 缺少的一些预编译功能,这些功能会影响交互性而不会影响真正的运行时速度。如果您希望使用一些大的 Monte Carlo 来估计参数,或者解决大量的 SDE,或者解决一个大的 PDE,我们可以解决这个问题。那是我们的第一个目标,我们确保尽可能地达到目标。但是在 REPL 中玩耍确实有 2-3 秒的“故障”,我们也不能忽视(当然比在 C/Fortran 中玩耍要好,但对于 REPL 来说仍然不理想)。为此,我已经向您展示了已经在开发和测试的解决方案,因此希望明年的这个时候我们可以针对特定案例提供更好的答案。
PS
另外两点需要注意。如果您只使用 ODE 求解器,您可以执行 using OrdinaryDiffEq 以继续下载/安装/编译/导入所有 DifferentialEquations.jl (this is described in the manual)。此外,像这样使用saveat 可能不是解决这个问题的最快方法:用更少的点来解决它并在必要时使用密集输出可能会更好。
编辑
I opened an issue detailing how we can reduce the "between function" compilation time without losing the speedup that specializing gives。我认为这是我们可以在短期内优先考虑的事情,因为我同意我们可以在这里做得更好。