在矩阵复制过程中,我注意到了julia的奇怪行为。
考虑以下三个功能:
function priv_memcopyBtoA!(A::Matrix{Int}, B::Matrix{Int}, n::Int)
A[1:n,1:n] = B[1:n,1:n]
return nothing
end
function priv_memcopyBtoA2!(A::Matrix{Int}, B::Matrix{Int}, n::Int)
ii = 1; jj = 1;
while ii <= n
jj = 1 #(*)
while jj <= n
A[jj,ii] = B[jj,ii]
jj += 1
end
ii += 1
end
return nothing
end
function priv_memcopyBtoA3!(A::Matrix{Int}, B::Matrix{Int}, n::Int)
A[1:n,1:n] = view(B, 1:n, 1:n)
return nothing
end
修改:1)我测试了代码是否会抛出BoundsError
,因此初始代码中缺少标有jj = 1 #(*)
的行。测试结果已经来自固定版本,所以它们保持不变。 2)我已经添加了视图变体,感谢@Colin T Bowers解决这两个问题。
似乎两个函数都应该导致或多或少相同的代码。然而,我得到了
A = fill!(Matrix{Int32}(2^12,2^12),2); B = Int32.(eye(2^12));
结果
@timev priv_memcopyBtoA!(A,B, 2000)
0.178327 seconds (10 allocations: 15.259 MiB, 85.52% gc time)
elapsed time (ns): 178326537
gc time (ns): 152511699
bytes allocated: 16000304
pool allocs: 9
malloc() calls: 1
GC pauses: 1
和
@timev priv_memcopyBtoA2!(A,B, 2000)
0.015760 seconds (4 allocations: 160 bytes)
elapsed time (ns): 15759742
bytes allocated: 160
pool allocs: 4
和
@timev priv_memcopyBtoA3!(A,B, 2000)
0.043771 seconds (7 allocations: 224 bytes)
elapsed time (ns): 43770978
bytes allocated: 224
pool allocs: 7
这是一个巨大的差异。这也令人惊讶。我预计第一个版本就像memcopy一样,对于大内存块来说很难被击败。
第二个版本具有指针算术(getindex
)的开销,分支条件(<=
)和每个赋值中的边界检查。然而,每项任务只需~3 ns
。
此外,垃圾收集器消耗的时间对于第一个功能而言变化很大。如果没有执行垃圾收集,则大的差异变小,但仍然存在。它在版本3和版本2之间仍然是~2.5的因素。
那么为什么&#34; memcopy&#34;版本不如&#34;赋值&#34;版本
答案 0 :(得分:5)
首先,您的代码包含一个错误。运行这个:
A = [1 2 ; 3 4]
B = [5 6 ; 7 8]
priv_memcopyBtoA2!(A, B, 2)
然后:
julia> A
2×2 Array{Int64,2}:
5 2
7 4
您需要在每个内部jj
循环结束时将1
重新分配回while
,即:
function priv_memcopyBtoA2!(A::Matrix{Int}, B::Matrix{Int}, n::Int)
ii = 1
while ii <= n
jj = 1
while jj <= n
A[jj,ii] = B[jj,ii]
jj += 1
end
ii += 1
end
return nothing
end
即使修复了错误,您仍然会注意到while
循环解决方案更快。这是因为julia中的数组切片创建了临时数组。所以在这一行:
A[1:n,1:n] = B[1:n,1:n]
右侧操作创建一个临时的nxn数组,然后将临时数组分配给左侧。
如果您想避免临时数组分配,请改为编写:
A[1:n,1:n] = view(B, 1:n, 1:n)
你会注意到这两种方法的时间现在非常接近,尽管while
循环仍然稍微快一些。作为一般规则,Julia中的循环很快(如在C fast中),并且显式写出循环通常会为您提供最优化的编译代码。我仍然希望显式循环比view
方法更快。
至于垃圾收集的东西,这只是你的计时方法的结果。最好使用包@btime
中的BenchmarkTools
,它使用各种技巧来避免陷阱,如计时垃圾收集等。
答案 1 :(得分:3)
为什么<?php
$hours=date("G");
$minutes=intval(date("i"));
$seconds=intval(date("s"));
$month=date("n");
$day=date("j");
$year=date("Y");
$today=mktime($hours,$minutes,$seconds,$month,$day,$year);
$futureDate=mktime($hours,$minutes,$seconds,$month,($day+30),$year);
print("Today is ".date("n-j-Y",$today)."<br />\n");
print("in 30 days it will be ".date("n-j-Y",$futureDate)."<br />\n");
?>
或其变体比一组while循环慢?我们来看看A[1:n,1:n] = view(B, 1:n, 1:n)
的作用。
A[1:n,1:n] = view(B, 1:n, 1:n)
返回一个迭代器,其中包含指向父view
的指针,以及如何计算应复制的索引的信息。 B
被解析为呼叫A[1:n,1:n] = ...
。在此之后,以及一些调用链调用,主要工作由:
_setindex!(...)
.\abstractarray.jl:883;
# In general, we simply re-index the parent indices by the provided ones
function getindex(V::SlowSubArray{T,N}, I::Vararg{Int,N}) where {T,N}
@_inline_meta
@boundscheck checkbounds(V, I...)
@inbounds r = V.parent[reindex(V, V.indexes, I)...]
r
end
#.\multidimensional.jl:212;
@inline function next(iter::CartesianRange{I}, state) where I<:CartesianIndex
state, I(inc(state.I, iter.start.I, iter.stop.I))
end
@inline inc(::Tuple{}, ::Tuple{}, ::Tuple{}) = ()
@inline inc(state::Tuple{Int}, start::Tuple{Int}, stop::Tuple{Int}) = (state[1]+1,)
@inline function inc(state, start, stop)
if state[1] < stop[1]
return (state[1]+1,tail(state)...)
end
newtail = inc(tail(state), tail(start), tail(stop))
(start[1], newtail...)
end
获取视图getindex
和索引V
。我们从I
获取视图,从B
获取索引I
。在每个步骤A
中,从视图reindex
和索引V
索引计算得到I
中的元素。它被称为B
,我们将其归还。最后r
写入r
。
在每个副本A
之后,将索引inc
递增到I
中的下一个元素,并测试是否已完成。请注意,代码来自v0.63,但在A
中,它或多或少相同。
原则上,代码可以简化为一组while循环,但它更通用。它适用于master
的任意视图和B
形式的任意切片以及任意数量的矩阵维度。大a:b:c
在我们的案例N
中。
由于函数更复杂,编译器也不会对它们进行优化。即有人建议编译器应该内联它们,但它不会这样做。这表明所显示的功能非常重要。
对于一组循环,编译器将最内层循环减少到三个添加(每个用于指向2
和A
,一个用于循环索引)和一个复制指令。
tl; dr B
的内部调用链与多个调度相结合并不重要,并处理一般情况。这会导致开销。一组while循环已针对特殊情况进行了优化。
请注意,性能取决于编译器。如果查看一维案例A[1:n,1:n] = view(B, 1:n, 1:n)
,它比while循环更快,因为它会对代码进行矢量化。然而,对于更高维度A[1:n] = view(B, 1:n)
,差异会增大。