我想了解这个例子中发生了什么:
在此实施中:
using LinearAlgebra
using StaticArrays, BenchmarkTools
function foo(u,p)
a,b,c,d = p;
x,y = u;
return SA_F64[a*x - b*x*y, -c*y + d*x*y];
end
function bar(f, u, h, p)
return SVector{2,Float64}(u + h*f(u,p));
end
function baz(f, u, func, p)
n_p = 100000;
h = 1.0/n_p;
out = zeros(n_p, 2);
for i in 1:n_p
u = func(f, u, h, p);
out[i,:]=u
end
return out;
end
u = SA_F64[1., 2.];
p = SA_F64[1.5, 1.0, 3.0, 1.0];
@btime baz(foo, u, bar, p)
我得到的执行时间为
6.033 ms (399492 allocations: 12.20 MiB)
。这需要太多内存来计算。
我发现一个实现在做:
function baz(f::T, u, func, p) where T
n_p = 100000;
h = 1.0/n_p;
out = zeros(n_p, 2);
for i in 1:n_p
u = func(f, u, h, p);
out[i,:]=u
end
return out;
end
最终结果完全相同,但计算时间为:
1.000 ms (2 allocations: 1.53 MiB)
。这太不可思议了!
规范
baz(f::T, u, func, p) where T
做了什么来获得这种性能改进?
这与 Julia 性能提示有关,标题为“注意 Julia 何时避免专业化:
作为一种启发式方法,Julia 在三种特定情况下避免自动专门化参数类型参数:
、Type
和Function
。当参数在方法中使用时,Julia 总是会专门化,但如果参数只是传递给另一个函数,则不会。Vararg
从根本上来说,Julia 的性能优势来自于推断类型并在此基础上专门化代码:当我们调用
sum(50:5:500000)
时,Julia 推断参数是 StepRange
,并且编译特定于范围参数的特定 sum
代码并运行。这会产生专门针对这种类型的非常快的机器代码。
但是,如上所述,如果
Function
参数没有在代码中直接使用(即调用),则不会发生这种特化 - 这里就是这种情况,因为 f
被传递给 baz
,但没有在那里使用,而是传递给bar
。因此,Julia 在运行时才将 f
视为某些 Function
,而不是特定的 foo
函数。上面链接的页面继续说:
这通常不会影响运行时的性能,并且可以提高编译器性能。如果您发现它确实对您的情况在运行时产生性能影响,您可以通过向方法声明添加类型参数来触发专门化。
因此,添加类型参数
T
是向编译器发出的一个信号,告诉编译器“要专门研究这里的特定函数,而不是将其视为泛型 Function
”。