朱莉娅:三维数组(性能)

3

在阅读 Julia 的 性能技巧 后,我没有找到有关如何加速三维数组代码的建议。

据我所知,当第三维 d 较小时,d-element Array{Array{Float64,2},1} 的表现最佳。然而,我不确定当 d 较大时是否仍然适用。

是否有关于 Julia 中此主题的教程?


示例1a(d = 50)

x = [zeros(100, 10) for d=1:50];

@time for d=1:50
    x[d] = rand(100,10);
end

0.000100 seconds (50 allocations: 396.875 KB)

示例1b(d=50)

y=zeros(100, 10, 50);

@time for d=1:50
    y[:,:,d] = rand(100,10);
end

0.000257 seconds (200 allocations: 400.781 KB)

示例2a(d=50000)

x = [zeros(100, 10) for d=1:50000];

@time for d=1:50000
    x[d] = rand(100,10);
end

0.410813 seconds (99.49 k allocations: 388.328 MB, 81.88% gc time)

示例2b(d=50000)

y=zeros(100, 10, 50000);

@time for d=1:50000
    y[:,:,d] = rand(100,10);
end

0.185929 seconds (298.98 k allocations: 392.898 MB, 6.83% gc time)
1个回答

4
据我了解,当第三维度d很小时,d元素数组{Array {Float64,2},1}的性能最佳。然而,当d很大时,我不确定是否仍然如此。
不是,更重要的是你如何使用它。A = Array {Array {Float64,2},1}是指向矩阵的指针数组。数组的值是指针或引用。因此,A [i]返回一个引用,即它很便宜。A2 = Array {Float64,3}是连续的浮点数数组。它实际上只是一个索引设置在一条线性内存块上(并且具有线性索引A2 [i],它使用该线性形式运行整个过程)。
后者具有一些优点,因为它是连续的。没有间接性,所以循环遍历所有A2的值将更快。A必须解引用两个指针才能获得一个值,所以如果您不知道如何仅解引用每个内部矩阵一次,则简单的3D循环将会更慢。此外,您可以通过@view A2[:,:,1]等来获取矩阵的视图,但您必须注意,A2[:,:,1]本身会复制矩阵。A[1]自然是一个视图,因为它返回对矩阵的引用,如果您想要复制,就必须明确执行copy(A[1])。因为A只是一个指针的线性数组,所以将新矩阵push!到其中非常便宜,因为这只是增加一个相对较小的数组(并且push!自动摊销)以在末尾添加一个新指针(这就是为什么像DifferentialEqautions.jl这样的东西使用数组来构建时间序列而不是更传统的矩阵)。
所以它们是不同的工具,具有不同的优缺点。
至于你的时间表,你正在做两件不同的事情。x[d] = rand(100,10) 创建了一个新矩阵并将其引用添加到x中。 y[:,:,d] = rand(100,10) 创建了一个新矩阵,并循环遍历y的值来更改y的值。你可以看到这就是为什么更慢。但你忽略了无需分配的情况。
function f2()
    y=zeros(100, 10, 50);

    @time for i in eachindex(y)
        y[i] = rand()
    end
    y
end

在小的情况下,这与数组创建相匹配。在第一种情况下,您不能轻率地这样做,但正如我所说,如果您解引用矩阵指针一次,那么效果会很好。
function f()
    x = [zeros(100, 10) for d=1:5000];

    @time @inbounds for d=1:50
        xd = x[d]
        for i in eachindex(xd)
            xd[i] = rand()
        end
    end
    x
end

因此,对于某些情况下来说,数组的数组可以是很好的数据结构。库RecursiveArrayTools.jl就是为了更好地利用它而创建的。例如,A3 = VectorOfArrays(A)通过惰性地将A[i,j,k]转换为A[k][i,j],使A3具有与A2相同的索引结构。然而,它保留了A的优势,但会自动确保以像f一样的正确方式广播。另一个类似的工具是ArrayPartition,它允许以广播性能的方式进行异构类型处理。

所以,是的,这并不总是正确的工具,但是当使用得当时,这些异构和递归数组是非常好的工具。


我不确定我完全理解了。在你的最后一个例子中,如果删除 xd = x[d] 并在随后的循环中使用 x[d][i] = rand(),是否会有任何区别?如果有,你能解释一下为什么吗? - merch
在我的情况下,内部循环只解除引用单个指针。x[d][i] 解除引用两个指针。第二种情况是否被完全优化取决于编译器是否正确地完成了它。或许可以,也或许不行。 - Chris Rackauckas
好的,只是为了澄清这一点。当您创建 xd 时,您取消引用单个指针。在内部循环的每次迭代中,您再次取消引用单个指针。这是正确的吗?如果我理解正确,您是说最好让编译器仅取消引用单个指针,并且 RecursiveArrayTools.jl 可以自动完成这个过程。 - merch
1
RecursiveArrayTools.jl使用笛卡尔索引,这似乎允许编译器进行此优化。我在此处显示的显式方式始终只在内部循环中具有单个取消引用操作。x[d][i]取决于编译器是否能够识别并优化重写您的循环,就像我展示的那样(因为两个引用比1个慢)。至于它是否在这种情况下执行此操作,我还没有检查。 - Chris Rackauckas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接