Julia vs Mathematica:数值积分性能

Julia vs Mathematica:数值积分性能,julia,wolfram-mathematica,numerical-integration,Julia,Wolfram Mathematica,Numerical Integration,朱莉娅,我是新来的,有个问题要问你 我通过移植我的Mathematica和Python代码(主要是物理学中的科学计算等)来教自己一些Julia,看看是什么。 到目前为止,一切都很顺利。而且很快。直到现在 现在,我正在模拟一个基本的锁定放大器,本质上,它接受一个可能非常复杂的时间相关信号,Uin(t),并产生一个输出,Uout(t),在某个参考频率fref下锁相(也就是说,它突出显示了Uin(t)的分量),它与参考正弦波具有一定的相位关系)描述无关紧要,重要的是它基本上是通过计算积分来实现的(为了

朱莉娅,我是新来的,有个问题要问你

我通过移植我的Mathematica和Python代码(主要是物理学中的科学计算等)来教自己一些Julia,看看是什么。 到目前为止,一切都很顺利。而且很快。直到现在

现在,我正在模拟一个基本的锁定放大器,本质上,它接受一个可能非常复杂的时间相关信号,
Uin(t)
,并产生一个输出,
Uout(t)
,在某个参考频率
fref
下锁相(也就是说,它突出显示了
Uin(t)的分量)
,它与参考正弦波具有一定的相位关系)描述无关紧要,重要的是它基本上是通过计算积分来实现的(为了清楚起见,我实际上省略了相位):

因此,我在Mathematica和Julia中对此进行了阐述和测试: 我在
t=0
时,在
Uin(t)
中定义一个模型,传递一些参数值,然后构建一个
Uout(t)
数组,范围为
fref

  • 朱莉娅:我用这个软件包进行数值积分

    T = 0.1
    f = 100.
    Uin(t) = sin(2pi * f * t) + sin(2pi * 2f *t)
    Uout(t, fref)::Float64 = quadgk(s -> Uin(s) * sin(2pi * fref * s), t-T, t, rtol=1E-3)[1]/T
    frng = 80.:1.:120.
    print(@time broadcast(Uout, 0., frng))
    
  • Mathematica

结果:

Julia在i7-5xxx笔记本电脑上用电池供电,将操作时间定在45到55秒之间,这比Mathematica用了约2秒。两者之间的差别是巨大的,老实说,令人难以置信。我知道Mathematica在它的内核中有一些非常甜美和精致的算法,但是Julia就是Julia。所以,问题是:什么给了我们

注意:将
f
T
设置为
const
可以将Julia的时间减少到8-10秒,但是
f
T
在实际程序中不能是
const
S。除此之外,我还有什么明显的遗漏吗

编辑2020年2月2日:

速度减慢似乎是由于当值接近零时,算法试图寻找精度,例如,见下:对于fref=95,计算需要1整秒(!),而对于相邻频率值,它会立即计算(返回的结果是(res,error)的元组)。似乎quadgk函数在非常小的值时暂停):


注:这与我要求的公差无关。此外,Mathematica通常会在默认情况下达到机器精度公差,而在接近零时速度会有所减慢,numpy/scipy只是在整个过程中快速运行,但产生的结果不如Mathematica精确(在默认设置下;没有对此进行太多研究)。

我看到的代码的第一个问题是它的类型不稳定。这是因为您使用的是全局变量(请参见性能提示第一条): 编译器无法知道在函数中使用的
f
T
的类型,因此无法进行有效编译。这也是为什么当您将它们标记为const时,性能会提高:现在编译器可以保证它们不会更改其类型,因此它可以高效地编译这两个函数

如何看到您的代码不稳定 如果使用宏
@code\u warntype
运行第一个函数,如下所示:

@code_warntype Uin(0.1,f)
julia> @code_warntype Uin(0.1)
Variables
  #self#::Core.Compiler.Const(Uin, false)
  t::Float64

Body::Any
1 ─ %1 = (2.0 * Main.pi)::Core.Compiler.Const(6.283185307179586, false)
│   %2 = (%1 * Main.f * t)::Any
│   %3 = Main.sin(%2)::Any
│   %4 = (2.0 * Main.pi)::Core.Compiler.Const(6.283185307179586, false)
│   %5 = (2.0 * Main.f)::Any
│   %6 = (%4 * %5 * t)::Any
│   %7 = Main.sin(%6)::Any
│   %8 = (%3 + %7)::Any
└──      return %8
您将看到如下输出:

@code_warntype Uin(0.1,f)
julia> @code_warntype Uin(0.1)
Variables
  #self#::Core.Compiler.Const(Uin, false)
  t::Float64

Body::Any
1 ─ %1 = (2.0 * Main.pi)::Core.Compiler.Const(6.283185307179586, false)
│   %2 = (%1 * Main.f * t)::Any
│   %3 = Main.sin(%2)::Any
│   %4 = (2.0 * Main.pi)::Core.Compiler.Const(6.283185307179586, false)
│   %5 = (2.0 * Main.f)::Any
│   %6 = (%4 * %5 * t)::Any
│   %7 = Main.sin(%6)::Any
│   %8 = (%3 + %7)::Any
└──      return %8
所有这些
any
都告诉您,编译器在任何步骤都不知道输出的类型

如何修复 您可以重新定义函数,将
f
T
作为变量:

Uin(t,f) = sin(2.0pi * f * t) + sin(2.0pi * 2.0f *t)
Uout(t, fref,f,T)::Float64 = quadgk(s -> Uin(s,f) * sin(2pi * fref * s), t-T, t, rtol=1E-3)[1]/T
通过这些重新定义,您的代码运行得更快。如果您尝试使用
@code\u warntype
检查它们,您将看到编译器现在正确地推断出所有内容的类型

有关进一步的性能改进,您可以查看


特别是,通常建议的测量性能而不是使用
@time
的方法是来自包
BenchmarkTools
@btime
。这是因为当运行
@time
时,您也在测量编译时间(另一个选项是运行@time两次-第二个测量值是正确的,因为所有函数都有机会编译)。

您可以做很多事情来进一步加快编译速度。改变集成顺序确实有点帮助,使用Float32而不是Float64做了一个小改进,使用
@fastmath
做了进一步的小改进。您还可以使用
SLEEFPirates.sin\u fast

using QuadGK, ChangePrecision

@changeprecision Float32 begin
    T = 0.1
    f = 100.
    @inline @fastmath Uin(t,f) = sin(2pi * f * t) + sin(2pi * 2f *t)
    @fastmath Uout(t, fref,f,T) = first(quadgk(s -> Uin(s,f) * sin(2pi * fref * s), t-T, t, rtol=1e-2, order=10))/T

    frng = 80.:1.:120.
    @time Uout.(0., frng, f, T)
end

您的问题与容错能力的选择有关。1e-3的相对误差听起来不算太大,但实际上,当积分接近零时,误差就很大了。特别是,当fref=80.0时(以及85、90、95、而不是100、105等),会发生这种情况:

引用
quadgk的docstring

(请注意,在以下情况下,指定正原子非常有用: 范数(I)可能为零。)

让我们尝试设置一个绝对公差,例如1e-6,并进行比较。首先是代码(使用@ARamirez中的代码):

然后是基准测试(为此使用基准测试工具)


好的,速度快了5000倍。可以吗?

您可以将变量括起来,而不是全局变量。我强烈建议您阅读手册的性能提示部分:。特别是,请阅读性能提示第一条。@chrisrackaukas?在使用它们作为参数时?是的,请参见以下ARamirez的回复。@DNF我做了:(.但是,正如我所说,这些不能是常数,在REPL中它们是全局的。但是,我也在代码中这样做了,在代码中我把它们包含在函数的局部范围内,所以可能不是全局的。我最终会弄明白的,谢谢。在Mathematica中公差和位宽度是一样的吗?谢谢。好的,我想编译器会推断出来的f和T是浮点数,因为我已经给它们赋值了,但这是有意义的,我应该有che
julia> Uout(0.0, 80.0, f, T)
1.2104987553880609e-16
Uin(t, f) = sin(2π * f * t) + sin(4π * f * t)

function Uout(t, fref, f , T)
    quadgk(s -> Uin(s, f) * sin(2π * fref * s), t-T, t, rtol=1e-3)[1]/T
end
function Uout_new(t, fref, f , T) # with atol
    quadgk(s -> Uin(s, f) * sin(2π * fref * s), t-T, t, rtol=1e-3, atol=1e-6)[1]/T
end
using BenchmarkTools
T = 0.1
f = 100.0
freqs = 80.0:1.0:120.0

@btime Uout.(0.0, $freqs, $f, $T);
6.302 s (53344283 allocations: 1.09 GiB)

@btime Uout_new.(0.0, $freqs, $f, $T);
1.270 ms (11725 allocations: 262.08 KiB)