【问题标题】:Julia Threads.@threads doesn't work in a simple exampleJulia Threads.@threads 在一个简单的示例中不起作用
【发布时间】:2020-01-15 18:17:01
【问题描述】:

我在 Julia 中运行双循环。代码非常简单。

w = rand(1000,1000)
function regular_demo(w::Array{Float64, 2})
    n = size(w)[1]
    G = zeros(n,n)
    @inbounds for j in 1:n-1
    wjj = w[j, j]
        for i in j+1:n
            wii = w[i, i]
            wij = w[i, j]
            sum = wii + 2wij + 3wjj 
            sum_inverse = inv(sum)
            G[j,j] = G[j,j] + sum_inverse
            G[i,i] = G[i,i] + sum_inverse
            G[i,j] = G[i,j] - sum_inverse
            G[j,i] = G[j,i] - sum_inverse         
        end
    end
G
end
regular_demo(w)
function thread_demo(w::Array{Float64, 2})
    n = size(w)[1]
    G = zeros(n,n)
    @inbounds Threads.@threads for j in 1:n-1
    wjj = w[j, j]
        for i in j+1:n
            wii = w[i, i]
            wij = w[i, j]
            sum = wii + 2wij + 3wjj 
            sum_inverse = inv(sum)
            G[j,j] = G[j,j] + sum_inverse
            G[i,i] = G[i,i] + sum_inverse
            G[i,j] = G[i,j] - sum_inverse
            G[j,i] = G[j,i] - sum_inverse         
        end
    end
G
end
thread_demo(w)

regular_demo(w) 和 thread_demo(w) 的唯一区别是函数第 4 行中的@inbounds Threads.@threads。如果我对同一个矩阵 w 同时运行 regular_demo(w) 和 thread_demo(w)。输出矩阵 G 不同。如何在 thread_demo(w) 中实现线程以获得与 regular_demo(w) 相同的输出矩阵 G。

【问题讨论】:

    标签: julia


    【解决方案1】:

    正确的函数应该如下所示

    function thread_demo(w::Array{Float64, 2})
        n = size(w)[1]
        G = [Threads.Atomic{Float64}(0.0) for i in 1:n, j in 1:n]
        a =  n % 2 == 1 ? 1 : 0
        @inbounds Threads.@threads for j2 in 1:n-1
            j = j2 % 2 == 1 ? j2 : n - j2 + a
            wjj = w[j, j]
            for i in j+1:n
                sum_inverse = inv(w[i, i] + 2w[i, j] + 3wjj )
                for i in 1:1000
                    sum_inverse = inv(sum_inverse)
                end #used for performance testing
                Threads.atomic_add!(G[j,j], sum_inverse)
                Threads.atomic_add!(G[j,i], -sum_inverse)
                Threads.atomic_add!(G[i,i], sum_inverse)
                Threads.atomic_add!(G[i,j], -sum_inverse)
            end
        end
        G
    end
    

    您的代码中需要更正以下两点:

    • Threads.@threads 将元素分成相等的部分,但是内部循环的大小会随着 j 值的增大而减小。这需要重新计算 j,以便在线程间获得或多或少相等的作业分配
    • 您的G 标识符与j 的各种值重叠。这意味着您需要在线程之间同步以对相同数据进行操作。您可以为此使用锁(例如Threads.SpinLock)或原子值。这里我使用原子值,因为它们在这个例子中更自然。

    您可以通过运行以下测试来检查我的代码是否有效:

    gg1 = regular_demo(w);
    gg2 = thread_demo(w);
    @assert all(gg1 .≈ getproperty.(gg2, :value))
    

    您需要使用近似相等运算符,因为并行性可以改变算术运算的顺序,并且您会得到稍微不同的结果。

    现在的问题是原子很昂贵。我的代码将比您的 regular_demo 函数花费更长的时间来执行,因为需要线程之间的协调并且循环内的操作非常便宜。但是,您的场景很可能比这个 MWE 复杂得多,并且使用我的代码,您将观察到显着的性能提升(为了测试并行性,您可以尝试在内部循环中添加类似 for i in 1:1000; sum_inverse = inv(sum_inverse); end 的内容并查看自己)。

    请注意,为了让多线程在 Julia 中工作,您需要将 JULIA_NUM_THREADS 变量设置为所需线程的数量(不大于逻辑 CPU 核心的数量)。

    窗户:

    set JULIA_NUM_THREADS=4
    

    Linux:

    export JULIA_NUM_THREADS=4
    

    请注意,某些 IDE 设置(例如 Juno)默认设置线程数等于 CPU 内核数。

    最后但同样重要的是,您可以通过运行 Threads.nthreads() 来测试您的配置:

    julia> Threads.nthreads()
    4
    

    【讨论】:

    • 太棒了! ?? 很好的答案。
    • 非常感谢!但我还有一个问题。如果 w = rand(1001,1001),似乎函数 thread_demo 的结果与函数 regular_demo 的结果不匹配。似乎该代码仅在 n = size(w)[1] 为偶数时才有效。如果 n = size(w)[1] 是奇数怎么办?实际上,n 可以是偶数或奇数。如何修改函数 thread_demo 使其适用于偶数 n 和奇数 n。感谢您的帮助!
    • 我明白了。我们可以在函数thread_demo的第二行添加代码: if n % 2 == 1 a = 2 else a = 1 end。我们可以将第 5 行更改为 j = j2 % 2 == a ? j2:n-j2。现在函数 thread_demo 适用于偶数 n 和奇数 n。感谢您的指导!
    • @Mizzle 谢谢 - 我添加了行以正确处理 w 的偶数和奇数大小。
    • 多线程并不是一个神奇的咒语。它不会自动使您的代码运行得更快。您必须确保您的代码可以并行运行,并且不会同时写入同一位置。否则,您将需要使用锁或原子写入来确保您不会拼凑内部程序逻辑。
    猜你喜欢
    • 2021-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-04
    • 2011-11-07
    相关资源
    最近更新 更多