问题描述
我正在做一个项目,它在一个巨大的数组中包含一些简单的数组操作。 即这里的一个例子
function singleoperation!(A::Array,B::Array,C::Array)
@simd for k in eachindex(A)
@inbounds C[k] = A[k] * B[k] / (A[k] +B[k]);
end
我尝试对其进行并行化以获得更快的速度。为了并行化它,我使用了 distirbuded 和 share 数组函数,它只是对我刚刚展示的函数进行了一些修改:
@everywhere function paralleloperation(A::SharedArray,B::SharedArray,C::SharedArray)
@sync @distributed for k in eachindex(A)
@inbounds C[k] = A[k] * B[k] / (A[k] +B[k]);
end
end
然而,即使我使用 4 个线程(在 R7-5800x 和 I7-9750H cpu 上尝试),两个函数之间也没有时间差异。我可以知道我可以在此代码中改进的任何内容吗?非常感谢!我将在下面发布完整的测试代码:
using distributed
addprocs(4)
@everywhere begin
using SharedArrays
using BenchmarkTools
end
@everywhere function paralleloperation!(A::SharedArray,C::SharedArray)
@sync @distributed for k in eachindex(A)
@inbounds C[k] = A[k] * B[k] / (A[k] +B[k]);
end
end
function singleoperation!(A::Array,C::Array)
@simd for k in eachindex(A)
@inbounds C[k] = A[k] * B[k] / (A[k] +B[k]);
end
end
N = 128;
A,B,C = fill(0,N,N),fill(.2,fill(.3,N);
AN,BN,CN = SharedArray(fill(0,N)),SharedArray(fill(.2,SharedArray(fill(.3,N));
@benchmark singleoperation!(A,C);
BenchmarkTools.Trial: 1612 samples with 1 evaluation.
Range (min … max): 2.582 ms … 9.358 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 2.796 ms ┊ GC (median): 0.00%
Time (mean ± σ): 3.086 ms ± 790.997 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
@benchmark paralleloperation!(AN,CN);
BenchmarkTools.Trial: 1404 samples with 1 evaluation.
Range (min … max): 2.538 ms … 17.651 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 3.154 ms ┊ GC (median): 0.00%
Time (mean ± σ): 3.548 ms ± 1.238 ms ┊ GC (mean ± σ): 0.08% ± 1.65%
解决方法
正如评论所指出的,这看起来更像是多线程而不是多处理的工作。详细的最佳方法通常取决于您是受 CPU 限制还是受内存带宽限制。对于示例中如此简单的计算,很可能是后者,在这种情况下,您将通过添加额外线程达到收益递减点,并且可能想要转向具有显式内存建模的东西,和/或GPU。
然而,一种非常简单的通用方法是使用内置于 LoopVectorization.jl 的多线程
A = rand(10000,10000)
B = rand(10000,10000)
C = zeros(10000,10000)
# Base
function singleoperation!(A,B,C)
@inbounds @simd for k in eachindex(A)
C[k] = A[k] * B[k] / (A[k] + B[k])
end
end
using LoopVectorization
function singleoperation_lv!(A,C)
@turbo for k in eachindex(A)
C[k] = A[k] * B[k] / (A[k] + B[k])
end
end
# Multithreaded (make sure you've started Julia with multiple threads)
function threadedoperation_lv!(A,C)
@tturbo for k in eachindex(A)
C[k] = A[k] * B[k] / (A[k] + B[k])
end
end
这给了我们
julia> @benchmark singleoperation!(A,C)
BenchmarkTools.Trial: 31 samples with 1 evaluation.
Range (min … max): 163.484 ms … 164.022 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 163.664 ms ┊ GC (median): 0.00%
Time (mean ± σ): 163.701 ms ± 118.397 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
█ ▄ ▄▄ ▁ ▁ ▁
▆▁▁▁▁▁▁▁▁▁▁▁▁▆█▆▆█▆██▁█▆▁▆█▁▁▁▆▁▁▁▁▆▁▁▁▁▁▁▁▁█▁▁▁▆▁▁▁▁▁▁▆▁▁▁▁▆ ▁
163 ms Histogram: frequency by time 164 ms <
Memory estimate: 0 bytes,allocs estimate: 0.
julia> @benchmark singleoperation_lv!(A,C)
BenchmarkTools.Trial: 31 samples with 1 evaluation.
Range (min … max): 163.252 ms … 163.754 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 163.408 ms ┊ GC (median): 0.00%
Time (mean ± σ): 163.453 ms ± 130.212 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
▃▃ ▃█▃ █ ▃ ▃
▇▁▁▁▁▁▇▁▁▁▇██▇▁▇███▇▁▁█▇█▁▁▁▁▁▁▁▁█▁▇▁▁▁▁▁▁▁▁▇▁▁▁▁▁▁▁▁▁▁▇▇▁▇▁▇ ▁
163 ms Histogram: frequency by time 164 ms <
Memory estimate: 0 bytes,allocs estimate: 0.
julia> @benchmark threadedoperation_lv!(A,C)
BenchmarkTools.Trial: 57 samples with 1 evaluation.
Range (min … max): 86.976 ms … 88.595 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 87.642 ms ┊ GC (median): 0.00%
Time (mean ± σ): 87.727 ms ± 439.427 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
▅ █ ▂ ▂
▅▁▁▁▁██▁▅█▁█▁▁▅▅█▅█▁█▅██▅█▁██▁▅▁▁▅▅▁▅▁▅▁▅▁▅▁▅▁▁▁▅▁█▁▁█▅▁▅▁▅█ ▁
87 ms Histogram: frequency by time 88.5 ms <
Memory estimate: 0 bytes,allocs estimate: 0.
现在,单线程 LoopVectorization @turbo
版本与单线程 @inbounds @simd
版本几乎完美绑定的事实对我来说暗示我们可能在这里受到内存带宽限制(通常是 {{1} } 明显比 @turbo
快,所以平局表明实际计算不是瓶颈)——在这种情况下,多线程版本只是通过让我们访问更多的内存带宽来帮助我们(尽管减少了返回,假设有一些主内存总线,无论它可以与多少个内核通信,都只能走得这么快)。
为了更深入地了解,让我们尝试使算术更难一点:
@inbounds @simd
那就够了
function singlemoremath!(A,C)
@inbounds @simd for k in eachindex(A)
C[k] = cos(log(sqrt(A[k] * B[k] / (A[k] + B[k]))))
end
end
using LoopVectorization
function singlemoremath_lv!(A,C)
@turbo for k in eachindex(A)
C[k] = cos(log(sqrt(A[k] * B[k] / (A[k] + B[k]))))
end
end
function threadedmoremath_lv!(A,C)
@tturbo for k in eachindex(A)
C[k] = cos(log(sqrt(A[k] * B[k] / (A[k] + B[k]))))
end
end
现在我们更接近 CPU 限制了,现在线程和 SIMD 向量化是 2.6 秒和 90 毫秒之间的差异!
如果您的实际问题与示例问题一样受内存限制,您可以考虑使用 GPU、针对内存带宽进行优化的服务器,和/或使用在内存建模方面投入大量精力的包.
您可能会检查的其他一些软件包可能包括 Octavian.jl(CPU)、Tullio.jl(CPU 或 GPU)和 GemmKernels.jl(GPU)。