作为Julia的起点,我决定实现一个简单的Strassen产品:
@inbounds function strassen_product(A :: Array{Num, 2}, B :: Array{Num, 2}, k = 2) :: Array{Num, 2} where {Num <: Number}
A_height, A_width = size(A)
B_height, B_width = size(B)
@assert A_height == A_width == B_height == B_width "Matrices are noth both square or of equal size."
@assert isinteger(log2(A_height)) "Size of matrices is not a power of 2."
if A_height ≤ k
return A * B
end
middle = A_height ÷ 2
A₁₁, A₁₂ = A[1:middle, 1:middle], A[1:middle, middle+1:end]
A₂₁, A₂₂ = A[middle+1:end, 1:middle], A[middle+1:end, middle+1:end]
B₁₁, B₁₂ = B[1:middle, 1:middle], B[1:middle, middle+1:end]
B₂₁, B₂₂ = B[middle+1:end, 1:middle], B[middle+1:end, middle+1:end]
P₁ = strassen_product(A₁₁ + A₂₂, B₁₁ + B₂₂)
P₂ = strassen_product(A₂₁ + A₂₂, B₁₁ )
P₃ = strassen_product(A₁₁, B₁₂ - B₂₂)
P₄ = strassen_product(A₂₂, B₂₁ - B₁₁)
P₅ = strassen_product(A₁₁ + A₁₂, B₂₂ )
P₆ = strassen_product(A₂₁ - A₁₁, B₁₁ + B₁₂)
P₇ = strassen_product(A₁₂ - A₂₂, B₂₁ + B₂₂)
C₁₁ = P₁ + P₄ - P₅ + P₇
C₁₂ = P₃ + P₅
C₂₁ = P₂ + P₄
C₂₂ = P₁ + P₃ - P₂ + P₆
return [ C₁₁ C₁₂ ;
C₂₁ C₂₂ ]
end
一切顺利。实际上,我喜欢@inbounds
之类的不安全优化的整个想法,实际上确实会对性能产生很大的影响,但充其量不会达到几毫秒。
现在,由于我没有for循环,因此要进一步优化,将使用那些A₁₁
等矩阵的视图,这样就不会进行复制。
因此,我在包含索引的4行前面打了@views
。我当然会出错,因为几行后,递归调用需要Array{...}
参数而不是SubArray{...}
。因此,我将参数的类型和返回类型更改为AbstractArray{Num, 2}
。这次它起作用了,因为AbstractArray
是数组类型的基本类型,但是...性能急剧下降,速度实际上降低了10倍,分配量增加了很多。
我的测试用例是这样:
A = rand(1:10, 4, 4)
B = rand(1:10, 4, 4)
@time C = strassen_product(A, B)
使用@views
+ AbstractArray
时:
0.457157 seconds (1.96 M allocations: 98.910 MiB, 5.56% gc time)
使用无视图版本时:
0.049756 seconds (126.92 k allocations: 5.603 MiB)
差异是巨大的,应该更快的版本要慢9-10倍,分配大约15倍,而其他版本的空间几乎是其20倍。
编辑:这不是这两种情况的第一次运行,而是大约10次测试运行的“中间值”最大。不是第一次运行,当然也不是最小或最大峰值。
编辑:我正在使用1.0版。
我的问题是:为什么会这样?我没有得到什么?我的理由是使用视图而不是副本会更快...错了吗?
答案 0 :(得分:2)
是的,视图版本比复制版本花费更长的时间,但分配的内存更少。这就是为什么。
使用视图代替副本不一定意味着可以提高速度(尽管它减少了内存分配。)提高速度取决于程序中的许多事情。首先,您创建的每个视图都是针对矩阵图块的。 Julia中的矩阵主要存储在列中的内存中,这使得从内存中检索两列比检索两行更容易进行CPU工作,因为列的元素是连续存储的。
矩阵的图块未连续存储在内存中。创建图块的副本将访问矩阵中的每个必需元素,并将它们写入内存中的连续块,而在图块上创建视图仅存储limit。尽管创建副本要比在图块上创建视图花费更多的时间和更多的内存访问,但是副本连续存储在内存中,这意味着对于CPU 而言,访问更容易,向量化更容易并且缓存更容易,以便以后使用使用。
您正在访问创建的图块不止一次,并且每次新访问都在一次完整的递归调用之后进行,每个递归调用都需要花费一些时间和内存访问,这样您已经加载到缓存中的图块可能会丢失。因此,同一图块上的每个新访问都可能需要从内存中完全重新加载。但是,完全重新加载 view 磁贴比完全重新加载 copy 磁贴需要更多的时间。 这就是为什么view
版本比复制版本花费更长的时间的原因,尽管view
版本分配的内存更少并且使用的访问次数更少。
看看文档Consider using views for slices和Copying data is not always bad上的性能提示。
数组连续存储在内存中,从而将自身借给CPU 向量化和由于缓存而减少的内存访问。这些是 推荐访问列主数组的相同原因 订单(请参见上文)。不规则的访问模式和不连续的视图 可能会大大降低阵列的计算速度,因为 非顺序内存访问。
在将不规则访问的数据复制到连续数组之前, 在其上进行操作可能会导致较大的加速,例如在示例中 下面。在这里,矩阵和向量的访问量为800,000 它们的随机改组索引在相乘之前。复制 视图进入纯数组,即使使用 复制操作的成本...
但是,差异应该不如您的结果所示那么大。 此外,4x4矩阵的乘积甚至不需要一毫秒。我相信,在每次调用函数时,您还需要重新加载函数定义,这使得JIT编译版本已过时,而Julia具有一次又一次地编译您的函数。您还可能会创建新函数类型不稳定的情况。我建议您使用@btime
中的BenchmarkTools
来衡量分配和时间。
您还应该使用更大的矩阵进行测试。在4x4阵列上创建2x2视图仍会将数据写入内存,其大小与2x2副本相当。小数据定时也容易产生噪音。
@inbounds function strassen_product(A, B, k = 2)
A_height, A_width = size(A)
# B_height, B_width = size(B)
# @assert A_height == A_width == B_height == B_width "Matrices are noth both square or of equal size."
# @assert isinteger(log2(A_height)) "Size of matrices is not a power of 2."
if A_height ≤ k
return A * B
end
middle = A_height ÷ 2
A₁₁, A₁₂ = @view(A[1:middle, 1:middle]), @view(A[1:middle, middle+1:end])
A₂₁, A₂₂ = @view(A[middle+1:end, 1:middle]), @view(A[middle+1:end, middle+1:end])
B₁₁, B₁₂ = @view(B[1:middle, 1:middle]), @view(B[1:middle, middle+1:end])
B₂₁, B₂₂ = @view(B[middle+1:end, 1:middle]), @view(B[middle+1:end, middle+1:end])
P₁ = strassen_product(A₁₁ + A₂₂, B₁₁ + B₂₂)
P₂ = strassen_product(A₂₁ + A₂₂, B₁₁ )
P₃ = strassen_product(A₁₁, B₁₂ - B₂₂)
P₄ = strassen_product(A₂₂, B₂₁ - B₁₁)
P₅ = strassen_product(A₁₁ + A₁₂, B₂₂ )
P₆ = strassen_product(A₂₁ - A₁₁, B₁₁ + B₁₂)
P₇ = strassen_product(A₁₂ - A₂₂, B₂₁ + B₂₂)
C₁₁ = P₁ + P₄ - P₅ + P₇
C₁₂ = P₃ + P₅
C₂₁ = P₂ + P₄
C₂₂ = P₁ + P₃ - P₂ + P₆
return [ C₁₁ C₁₂ ;
C₂₁ C₂₂ ]
end
@inbounds function strassen_product2(A, B, k = 2)
A_height, A_width = size(A)
#B_height, B_width = size(B)
#@assert A_height == A_width == B_height == B_width "Matrices are noth both square or of equal size."
#@assert isinteger(log2(A_height)) "Size of matrices is not a power of 2."
if A_height ≤ k
return A * B
end
middle = A_height ÷ 2
A₁₁, A₁₂ = A[1:middle, 1:middle], A[1:middle, middle+1:end]
A₂₁, A₂₂ = A[middle+1:end, 1:middle], A[middle+1:end, middle+1:end]
B₁₁, B₁₂ = B[1:middle, 1:middle], B[1:middle, middle+1:end]
B₂₁, B₂₂ = B[middle+1:end, 1:middle], B[middle+1:end, middle+1:end]
P₁ = strassen_product2(A₁₁ + A₂₂, B₁₁ + B₂₂)
P₂ = strassen_product2(A₂₁ + A₂₂, B₁₁ )
P₃ = strassen_product2(A₁₁, B₁₂ - B₂₂)
P₄ = strassen_product2(A₂₂, B₂₁ - B₁₁)
P₅ = strassen_product2(A₁₁ + A₁₂, B₂₂ )
P₆ = strassen_product2(A₂₁ - A₁₁, B₁₁ + B₁₂)
P₇ = strassen_product2(A₁₂ - A₂₂, B₂₁ + B₂₂)
C₁₁ = P₁ + P₄ - P₅ + P₇
C₁₂ = P₃ + P₅
C₂₁ = P₂ + P₄
C₂₂ = P₁ + P₃ - P₂ + P₆
return [ C₁₁ C₁₂ ;
C₂₁ C₂₂ ]
end
以下是@btime
中Benchmarktools
的测试
A = rand(1:10, 256, 256)
B = rand(1:10, 256, 256)
@btime C = strassen_product(A, B); # view version
@btime D = strassen_product2(A, B); #copy version
结果:
438.294 ms (4941454 allocations: 551.53 MiB)
349.894 ms (4529747 allocations: 620.04 MiB)