好问题。
Fibonacci函数的这种多线程实现并不比单线程版本快。该功能仅在博客文章中显示为新线程功能如何工作的玩具示例,强调了它允许在不同功能中产生许多线程,并且调度程序将找出最佳工作负载。
问题在于,@spawn
它的开销很小1µs
,因此,如果生成一个线程来执行一个少于的任务,则1µs
可能会损害性能。的递归定义fib(n)
具有阶数1.6180^n
[1]的指数时间复杂度,因此在调用时fib(43)
,会生成一些阶数1.6180^43
线程。如果每个人都需要1µs
生成,则仅花费约16分钟即可生成并安排所需的线程,这甚至不考虑执行实际计算和重新合并/同步线程所花费的时间,甚至更多时间。
像这样的事情,在您为计算的每个步骤生成线程时,只有与@spawn
开销相比,计算的每个步骤花费很长时间,才有意义。
请注意,目前正在努力减少的开销@spawn
,但由于多核有机硅芯片的物理特性,我怀疑它对于上述fib
实现是否足够快。
如果您对如何将线程fib
函数修改为真正有益的方法感到好奇,那么最简单的方法是仅fib
在我们认为1µs
运行时间比运行时间长的情况下才产生线程。在我的机器上(在16个物理内核上运行),我得到了
function F(n)
if n < 2
return n
else
return F(n-1)+F(n-2)
end
end
julia> @btime F(23);
122.920 μs (0 allocations: 0 bytes)
因此,这比产生线程的成本高出两个数量级。这似乎是一个很好的起点:
function fib(n::Int)
if n < 2
return n
elseif n > 23
t = @spawn fib(n - 2)
return fib(n - 1) + fetch(t)
else
return fib(n-1) + fib(n-2)
end
end
现在,如果我使用BenchmarkTools.jl [2]遵循正确的基准测试方法,我会发现
julia> using BenchmarkTools
julia> @btime fib(43)
971.842 ms (1496518 allocations: 33.64 MiB)
433494437
julia> @btime F(43)
1.866 s (0 allocations: 0 bytes)
433494437
@Anush在评论中提出:这似乎是使用16核速度提高2的一个因素。是否有可能使速度接近16倍?
是的。上面函数的问题是函数主体大于F
,具有很多条件,函数/线程生成等等。我请你比较@code_llvm F(10)
@code_llvm fib(10)
。这意味着fib
julia难以优化。这种额外的开销对于小n
案件来说是与众不同的。
julia> @btime F(20);
28.844 μs (0 allocations: 0 bytes)
julia> @btime fib(20);
242.208 μs (20 allocations: 320 bytes)
不好了!所有从未被触及的额外代码n < 23
使我们的速度降低了一个数量级!不过有一个简单的解决方法:何时n < 23
,不要递归到fib
,而是调用单线程F
。
function fib(n::Int)
if n > 23
t = @spawn fib(n - 2)
return fib(n - 1) + fetch(t)
else
return F(n)
end
end
julia> @btime fib(43)
138.876 ms (185594 allocations: 13.64 MiB)
433494437
这使结果更接近我们对这么多线程的期望。
[1] https://www.geeksforgeeks.org/time-complexity-recursive-fibonacci-program/
[2] BenchmarkTools.jl中的BenchmarkTools @btime
宏将多次运行函数,从而跳过了编译时间和平均结果。