好问题。
斐波那契函数的这种多线程实现不比单线程版本快。该功能仅在博客文章中作为新线程功能如何工作的玩具示例进行了展示,强调它允许在不同功能中生成许多线程,并且调度程序将找出最佳工作负载。
问题在于@spawn 的开销约为1µs,因此如果您生成一个线程来执行所需时间少于1µs 的任务,您可能会损害您的性能。 fib(n) 的递归定义具有 1.6180^n [1] 阶的指数时间复杂度,因此当您调用 fib(43) 时,您会产生一些秩序 1.6180^43 线程。如果每个人都需要1µs 来生成,那么仅生成和调度所需的线程就需要大约 16 分钟,这甚至不考虑进行实际计算和重新合并/同步线程所需的时间这需要更多时间。
只有当计算的每个步骤与@spawn 开销相比,计算的每个步骤都需要很长时间时,才会为计算的每个步骤生成一个线程这样的事情才有意义。
请注意,在减少 @spawn 的开销方面还有一些工作要做,但从多核硅芯片的物理特性来看,我怀疑它对于上述 fib 的实现是否足够快。
如果您对我们如何修改线程化的fib 函数感到好奇,那么最简单的做法是只生成一个fib 线程,如果我们认为它需要比@987654335 长得多的时间@ 跑步。在我的机器上(在 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 在 cmets 中提问:这似乎是使用 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 宏将多次运行函数,跳过编译时间和平均结果。