朱莉娅:功能类型和性能



Julia中有没有一种方法可以概括如下模式?

function compute_sum(xs::Vector{Float64})
res = 0
for i in 1:length(xs)
res += sqrt(xs[i])
end
res
end

这将计算每个向量元素的平方根,然后对所有元素求和。它比";天真的";具有数组理解或map的版本,并且也不分配额外的内存:

xs = rand(1000)
julia> @time compute_sum(xs)
0.000004 seconds
676.8372556762225
julia> @time sum([sqrt(x) for x in xs])
0.000013 seconds (3 allocations: 7.969 KiB)
676.837255676223
julia> @time sum(map(sqrt, xs))
0.000013 seconds (3 allocations: 7.969 KiB)
676.837255676223

不幸的是;明显的";通用版本的wrt性能很差:

function compute_sum2(xs::Vector{Float64}, fn::Function)
res = 0
for i in 1:length(xs)
res += fn(xs[i])
end
res
end
julia> @time compute_sum2(xs, x -> sqrt(x))
0.013537 seconds (19.34 k allocations: 1.011 MiB)
676.8372556762225

原因是x -> sqrt(x)被定义为一个新的匿名函数,每次调用compute_sum2,所以每次调用它都会导致新的编译。

如果你之前定义过它,例如:

julia> f = x -> sqrt(x)

那么你有:

julia> @time compute_sum2(xs, f) # here you pay compilation cost
0.010053 seconds (19.46 k allocations: 1.064 MiB)
665.2469135020949
julia> @time compute_sum2(xs, f) # here you have already compiled everything
0.000003 seconds (1 allocation: 16 bytes)
665.2469135020949

请注意,一种自然的方法是定义一个名称如下的函数:

julia> g(x) = sqrt(x)
g (generic function with 1 method)
julia> @time compute_sum2(xs, g)
0.000002 seconds
665.2469135020949

您可以看到,x -> sqrt(x)在每次编写时都会定义一个新的匿名函数,例如:

julia> typeof(x -> sqrt(x))
var"#3#4"
julia> typeof(x -> sqrt(x))
var"#5#6"
julia> typeof(x -> sqrt(x))
var"#7#8"

请注意,如果在函数体中定义匿名函数,则情况会有所不同:

julia> h() = typeof(x -> sqrt(x))
h (generic function with 2 methods)
julia> h()
var"#11#12"
julia> h()
var"#11#12"
julia> h()
var"#11#12"

你可以看到,这一次的匿名函数每次都是一样的。

除了Bogumil的出色响应之外,我只想补充一点,推广这一点的一个非常方便的方法是使用mapreducefold等常规函数编程函数。

在这种情况下,您正在执行map转换(即sqrt(和reduce(即+(,因此您也可以使用mapreduce(sqrt, +, xs)来实现结果。这基本上没有开销,并且在性能上与手动循环相当。

如果您有一系列非常复杂的转换,您可以获得最佳性能,并且仍然可以使用Transfers.jl包使用函数。

Bogumił已经回答了关于函数类型的部分。我想指出的是,如果基准测试正确,您的实现已经尽可能高效地工作了,但可以用等效的内置功能取代:

julia> @btime compute_sum($xs)
2.149 μs (0 allocations: 0 bytes)
661.6571623823567
julia> @btime sum(sqrt, $xs)
2.149 μs (0 allocations: 0 bytes)
661.6571623823567
julia> @btime compute_sum2($xs, sqrt)
2.149 μs (0 allocations: 0 bytes)
661.6571623823567
julia> @btime mapreduce(sqrt, +, $xs)
2.149 μs (0 allocations: 0 bytes)
661.6571623823567

如果可能的话,最好使用eta等效的非lambda函数:f而不是x -> f(x)。特别是对于内置函数,因为它们有时会在上调度

其他答案都很全面,但我想指出的是,你可以省略sum([sqrt(x) for x in xs])中的方括号,得到最快的版本:

julia> using BenchmarkTools
julia> @btime compute_sum($xs)
1.779 μs (0 allocations: 0 bytes)
679.0943275393031
julia> @btime sum([sqrt(x) for x in $xs])
1.626 μs (1 allocation: 7.94 KiB)
679.0943275393028
julia> @btime sum(map(sqrt, $xs))
1.628 μs (1 allocation: 7.94 KiB)
679.0943275393028
julia> @btime sum(sqrt(x) for x in $xs)
1.337 μs (0 allocations: 0 bytes)
679.0943275393031

还要注意,在我的电脑上,Julia mastercompute_sum是最慢的,而不是最快的求和方式。

最新更新