为什么这个版本的矩阵副本这么慢?

时间:2018-05-16 20:11:05

标签: performance compilation copy julia

在矩阵复制过程中,我注意到了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;版本

2 个答案:

答案 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中。

由于函数更复杂,编译器也不会对它们进行优化。即有人建议编译器应该内联它们,但它不会这样做。这表明所显示的功能非常重要。

对于一组循环,编译器将最内层循环减少到三个添加(每个用于指向2A,一个用于循环索引)和一个复制指令。

tl; dr B的内部调用链与多个调度相结合并不重要,并处理一般情况。这会导致开销。一组while循环已针对特殊情况进行了优化。

请注意,性能取决于编译器。如果查看一维案例A[1:n,1:n] = view(B, 1:n, 1:n),它比while循环更快,因为它会对代码进行矢量化。然而,对于更高维度A[1:n] = view(B, 1:n),差异会增大。