您可能知道这一点,但在现实生活中所有这些都应该是in a function;如果您使用全局变量,您的性能将是灾难性的,而使用一个函数,您只需单线程实现就可以遥遥领先。虽然“慢速”编程语言的用户通常会立即使用并行性来提高性能,但对于 Julia,您最好的方法通常是首先分析单线程实现的性能(像分析器一样使用 tools)并修复您发现的任何问题.尤其是对于 Julia 的新手来说,以这种方式使您的代码速度提高十倍或一百倍的情况并不少见,在这种情况下,您可能会觉得这就是您所需要的。
确实,有时单线程实现会更快,因为线程会引入自己的开销。我们可以在这里很容易地说明这一点。我将对上面的代码进行一次修改:不是在每次迭代中添加 1,而是添加 i % 2,如果 i 是奇数则添加 1,如果 i 是偶数则添加 0。我这样做是因为一旦你把它放在一个函数中,如果你所做的只是加 1,Julia 的编译就足够聪明,可以弄清楚你在做什么,并且只返回答案而不实际运行循环;我们想要运行循环,所以我们必须让它变得更复杂一些,这样编译器就无法提前找出答案。
首先,让我们尝试上面最快的线程实现(我用julia -t4 启动Julia 以使用4 个线程):
julia> acc2 = Threads.Atomic{Int}(0)
Base.Threads.Atomic{Int64}(0)
julia> @btime Threads.@threads for i in 1:1000
global acc2
Threads.atomic_add!(acc2, i % 2)
end
12.983 μs (21 allocations: 1.86 KiB)
julia> @btime Threads.@threads for i in 1:1000000
global acc2
Threads.atomic_add!(acc2, i % 2)
end
27.532 ms (22 allocations: 1.89 KiB)
这是快还是慢?让我们先把它放在一个函数中,看看它是否有帮助:
julia> function lockadd(n)
acc = Threads.Atomic{Int}(0)
Threads.@threads for i = 1:n
Threads.atomic_add!(acc, i % 2)
end
return acc[]
end
lockadd (generic function with 1 method)
julia> @btime lockadd(1000)
9.737 μs (22 allocations: 1.88 KiB)
500
julia> @btime lockadd(1000000)
13.356 ms (22 allocations: 1.88 KiB)
500000
因此,通过将其放入函数中,我们获得了 2 倍(在更大的工作中)。然而,更好的线程策略是无锁线程:给每个线程自己的acc,然后在末尾添加所有单独的accs。
julia> function threadedadd(n)
accs = zeros(Int, Threads.nthreads())
Threads.@threads for i = 1:n
accs[Threads.threadid()] += i % 2
end
return sum(accs)
end
threadedadd (generic function with 1 method)
julia> using BenchmarkTools
julia> @btime threadedadd(1000)
2.967 μs (22 allocations: 1.97 KiB)
500
julia> @btime threadedadd(1000000)
56.852 μs (22 allocations: 1.97 KiB)
500000
对于更长的循环,我们获得了超过 200 倍的性能!这确实是一个非常好的加速。
不过,让我们尝试一个简单的单线程实现:
julia> function addacc(n)
acc = 0
for i in 1:n
acc += i % 2
end
return acc
end
addacc (generic function with 1 method)
julia> @btime addacc(1000)
43.218 ns (0 allocations: 0 bytes)
500
julia> @btime addacc(1000000)
41.068 μs (0 allocations: 0 bytes)
500000
这比小型作业的线程实现快 70 倍,甚至在大型作业上也更快。为了完整起见,让我们将其与使用全局状态的相同代码进行比较:
julia> @btime for i in 1:1000
global acc
acc += i % 2
end
20.158 μs (1000 allocations: 15.62 KiB)
julia> @btime for i in 1:1000000
global acc
acc += i % 2
end
20.455 ms (1000000 allocations: 15.26 MiB)
太可怕了。
当然,并行性在某些情况下会产生影响,但它通常用于更复杂的任务。除非您已经优化了单线程实现,否则您仍然不应该使用它。
所以故事的两个重要寓意:
- 阅读 Julia 的性能提示,分析代码的性能,并修复任何瓶颈
- 只有在用尽所有单线程选项后才能实现并行。