【问题标题】:Erratic timings in Julia朱莉娅的时间不稳定
【发布时间】:2017-11-14 03:18:41
【问题描述】:

在使用 Matlab 几年后,我开始学习 Julia。我首先实现了一个简单的多项式乘法(没有 FFT)来尝试理解类型稳定性的作用。该项目的很大一部分是对快速多项式乘法器的要求。但是,我有以下时间我根本无法理解。

function cauchyproduct(L::Array{Float64},R::Array{Float64})
    # good one for floats
    N = length(L)
    prodterm = zeros(1,2N-1)
    for n=1:N
        Lterm = view(L,1:n)
        Rterm = view(R,n:-1:1)
        prodterm[n] = dot(Lterm,Rterm)
    end

    for n = 1:N-1
        Lterm = view(L,n+1:N)
        Rterm = view(R,N:-1:n+1)
        prodterm[N+n] = dot(Lterm,Rterm)
    end
    prodterm
end



testLength = 10000
goodL = rand(1,testLength)
goodR = rand(1,testLength)
for j in 1:10
    @time cauchyproduct(goodL,goodR)
end
@which cauchyproduct(goodL,goodR)

我从该代码的 2 次连续运行中得到以下时间。这些从一次运行到另一次运行的时间是完全不稳定的。一般来说,我每次测试的时间范围在 0.05 秒到 2 秒之间。通常,通过 for 循环的单次运行的时间都将具有相似的时间(如下例所示),但情况并非总是如此。有时,我会交替使用它,例如 .05s .05s 1.9s .04s .05s 2.1s 等等等等。

知道为什么会这样吗?

  0.544795 seconds (131.08 k allocations: 5.812 MiB)
  0.510395 seconds (120.00 k allocations: 5.340 MiB)
  0.528362 seconds (120.00 k allocations: 5.340 MiB, 0.94% gc time)
  0.507156 seconds (120.00 k allocations: 5.340 MiB)
  0.507566 seconds (120.00 k allocations: 5.340 MiB)
  0.507932 seconds (120.00 k allocations: 5.340 MiB)
  0.527383 seconds (120.00 k allocations: 5.340 MiB)
  0.513301 seconds (120.00 k allocations: 5.340 MiB, 0.83% gc time)
  0.509347 seconds (120.00 k allocations: 5.340 MiB)
  0.509177 seconds (120.00 k allocations: 5.340 MiB)
  0.052247 seconds (120.00 k allocations: 5.340 MiB, 7.95% gc time)
  0.049644 seconds (120.00 k allocations: 5.340 MiB)
  0.047275 seconds (120.00 k allocations: 5.340 MiB)
  0.049163 seconds (120.00 k allocations: 5.340 MiB)
  0.049029 seconds (120.00 k allocations: 5.340 MiB)
  0.054050 seconds (120.00 k allocations: 5.340 MiB, 8.36% gc time)
  0.047010 seconds (120.00 k allocations: 5.340 MiB)
  0.051240 seconds (120.00 k allocations: 5.340 MiB)
  0.050961 seconds (120.00 k allocations: 5.340 MiB)
  0.049841 seconds (120.00 k allocations: 5.340 MiB, 4.90% gc time)

编辑:显示的时间是通过连续两次执行定义函数下的代码获得的。具体来说,代码块

goodL = rand(1,testLength)
goodR = rand(1,testLength)
for j in 1:10
    @time cauchyproduct(goodL,goodR)
end

在不同的运行中给出了截然不同的时间(无需重新编译上面的函数)。在所有时间中,都调用了相同的 cauchyproduct 方法(顶级版本)。希望这可以澄清问题。

编辑2:我把最后的代码块改成下面的

testLength = 10000
goodL = rand(1,testLength)
goodR = rand(1,testLength)
for j = 1:3
    @time cauchyproduct(goodL,goodR)
end


for j = 1:3
    goodL = rand(1,testLength)
    goodR = rand(1,testLength)
    @time cauchyproduct(goodL,goodR)
end

@time cauchyproduct(goodL,goodR)
@time cauchyproduct(goodL,goodR)
@time cauchyproduct(goodL,goodR)

并在新块的 2 次重复执行中获得以下时间。

时机一:

  0.045936 seconds (120.00 k allocations: 5.340 MiB)
  0.045740 seconds (120.00 k allocations: 5.340 MiB)
  0.045768 seconds (120.00 k allocations: 5.340 MiB)
  1.549157 seconds (120.00 k allocations: 5.340 MiB, 0.14% gc time)
  0.046797 seconds (120.00 k allocations: 5.340 MiB)
  0.046637 seconds (120.00 k allocations: 5.340 MiB)
  0.047143 seconds (120.00 k allocations: 5.341 MiB)
  0.049088 seconds (120.00 k allocations: 5.341 MiB, 3.88% gc time)
  0.049246 seconds (120.00 k allocations: 5.341 MiB)

时间 2:

  2.250852 seconds (120.00 k allocations: 5.340 MiB)
  2.370882 seconds (120.00 k allocations: 5.340 MiB)
  2.247676 seconds (120.00 k allocations: 5.340 MiB, 0.14% gc time)
  1.550661 seconds (120.00 k allocations: 5.340 MiB)
  0.047258 seconds (120.00 k allocations: 5.340 MiB)
  0.047169 seconds (120.00 k allocations: 5.340 MiB)
  0.048625 seconds (120.00 k allocations: 5.341 MiB, 4.02% gc time)
  0.045489 seconds (120.00 k allocations: 5.341 MiB)
  0.049457 seconds (120.00 k allocations: 5.341 MiB)

好困惑。

【问题讨论】:

  • 我已经编辑了原始帖子以尝试澄清情况。
  • “运行”是指当我使用 for 循环运行下部块时。 for 循环中的 10 个计时中的每一个“通常”都有相似的计时。但是重新运行该代码块(即执行 10 个额外的计时)会给出到处都是的时间。显示的块运行了两次 for 循环(总共 20 次)。
  • 每次运行。每组 10 个时序对于每个时序具有相似的值。但是,再次运行该块会给出 10 个新的计时值,它们的值大不相同(正如您在上面的两个 10 块中注意到的那样)。
  • 我承认提出问题的方式有点令人困惑,但我不知道为什么你被否决为-2。过于苛刻。我可以在我的机器上复制这些结果,这个问题是合理的。当投票者不解释他们在 cmets 中的行为时,可能会令人沮丧。我赞成尝试至少将其恢复为 0。
  • 顺便说一句,如果输入可以保证始终为Array{Float64,2},那么您的“好”和“坏”函数将编译为相同的代码,它可以在您当前的测试中,所以你会看不出两者之间的性能差异。请参阅here 了解更多详情。

标签: julia


【解决方案1】:

简答:您的代码有点奇怪,因此可能会以意想不到的方式触发垃圾收集,从而导致时间变化。

长答案:我同意你得到的时间有点奇怪。我不完全确定我能确定导致问题的确切原因,但我 99% 确定这与垃圾收集有关。

因此,您的代码有点奇怪,因为您允许输入任意维度的数组,即使您随后调用 dot 函数(用于获取两个 向量的点积的 BLAS 例程 )。如果你没有意识到,如果你想要一个向量,使用Array{Float64,1},矩阵Array{Float64,2}等等。或者您也可以使用别名Vector{Float64}Matrix{Float64}

我注意到的第二件奇怪的事情是,在您的测试中,您生成了rand(1, N)。这将返回一个Array{Float64,2},即一个矩阵。要获得Array{Float64, 1},即向量,您可以使用rand(N)。然后在您的函数中,您可以查看大小为 1xN 的矩阵。现在,Julia 使用列优先排序,因此对向量使用 1xN 对象将是真的低效的,并且可能是你奇怪计时的来源。在幕后,我怀疑对dot 的调用将涉及将这些东西转换为浮点的常规向量,因为dot 最终会馈送到需要这种输入类型的底层BLAS 例程。所有这些转换将意味着大量的临时存储,需要在某些时候进行垃圾收集,这可能是不同时间的来源(90% 的时间,同一代码上的不同时间是垃圾的结果收集器被触发 - 有时以非常意想不到的方式)。

因此,可能有几种方法可以改进以下内容,但我的函数的快速和肮脏版本如下所示:

function cauchyproduct(L::AbstractVector{<:Number}, R::AbstractVector{<:Number})
    length(L) != length(R) && error("Length mismatch in inputs")
    N = length(L)
    prodterm = zeros(1,2*N-1)
    R = flipdim(R, 1)
    for n=1:N
        prodterm[n] = dot(view(L, 1:n), view(R, N-n+1:N))
    end
    for n = 1:N-1
        prodterm[N+n] = dot(view(L, n+1:N), view(R, 1:N-n))
    end
    return prodterm
end

注意,我在循环之前翻转了R,因此内存不需要在循环中一遍又一遍地重新排序。毫无疑问,这导致了您奇怪的垃圾收集问题。然后,应用您的测试(我认为将数组生成移动到循环内是一个更好的主意,以防一些巧妙的缓存问题导致计时失败):

testLength = 10000
for j = 1:20
    goodL = rand(testLength);
    goodR = rand(testLength);
    @time cauchyproduct(goodL,goodR);
end

我们得到这样的东西:

  0.105550 seconds (78.19 k allocations: 3.935 MiB, 2.91% gc time)
  0.022421 seconds (40.00 k allocations: 2.060 MiB)
  0.022527 seconds (40.00 k allocations: 2.060 MiB)
  0.022333 seconds (40.00 k allocations: 2.060 MiB)
  0.021568 seconds (40.00 k allocations: 2.060 MiB)
  0.021837 seconds (40.00 k allocations: 2.060 MiB)
  0.022155 seconds (40.00 k allocations: 2.060 MiB)
  0.022071 seconds (40.00 k allocations: 2.060 MiB)
  0.021720 seconds (40.00 k allocations: 2.060 MiB)
  0.024774 seconds (40.00 k allocations: 2.060 MiB, 9.13% gc time)
  0.021714 seconds (40.00 k allocations: 2.060 MiB)
  0.022066 seconds (40.00 k allocations: 2.060 MiB)
  0.021815 seconds (40.00 k allocations: 2.060 MiB)
  0.021819 seconds (40.00 k allocations: 2.060 MiB)
  0.021928 seconds (40.00 k allocations: 2.060 MiB)
  0.021795 seconds (40.00 k allocations: 2.060 MiB)
  0.021837 seconds (40.00 k allocations: 2.060 MiB)
  0.022285 seconds (40.00 k allocations: 2.060 MiB)
  0.021380 seconds (40.00 k allocations: 2.060 MiB)
  0.023828 seconds (40.00 k allocations: 2.060 MiB, 6.91% gc time)

第一次迭代是测量编译时间,而不是运行时间,因此应该被忽略(如果你不明白我的意思,请查看官方文档的性能提示部分)。如您所见,剩余的迭代速度更快,而且相当稳定。

【讨论】:

  • 感谢 Colin 和 Evert 提供帮助的 cmets。您的解决方案似乎已经解决了这个特定问题。更大的问题是我有很多要学习的东西,以及多年来从 Matlab 学习的坏习惯,这个简单的例子就证明了这一点。我没有指定数组维度的原因是因为这个函数实际上必须执行更高维度的柯西积,所以 dot() 调用将被替换为 .* 和 sum() 调用。我想先得到一个 1d 版本。最后一个问题:我一直在使用 view() ,因为 Julia 的介绍指南说这是正确的做法。这里不是这样吗?
  • @SDK 我是在 2014 年从全职 Matlab 背景来到 Julia 的。我会说我花了一年的时间在 Julia 全职工作,以训练自己摆脱不良的 Matlab 习惯。两个快速提示:1)看看我在签名中用我的Vector{&lt;:Number} 做了什么(或者你可以做(Vector{T}) where {T&lt;:Number})。像这样使用类型系统是多重分派的核心。 2) 如果您发现自己编写了长函数(标准 Matlab 练习),那么您可能做错了 julia。 Julia 适合短函数,通常是单行函数!
  • @SDK 是的,在这里使用view 是个好主意。 view 在指定维度上创建一个数组视图,然后可以将其传递并对其执行操作,就好像它是一个较小的数组一样。这避免了复制原始数组的需要,因此可以大大加快一些例程。您可以验证我上面的函数在没有view 的情况下慢了大约 10 倍。 view 不太好的地方是当您需要的子数组在内存中不连续时,例如 view(randn(5), 4:-1:2)view(randn(5)[5,3])... TBC
  • @SDK 在这些情况下,创建在内存中连续的子数组的新副本通常会更快,因此操作(尤其是 BLAS 操作)可以利用连续性来进行更多工作更快速。更多阅读here
猜你喜欢
  • 2018-11-27
  • 2014-04-26
  • 1970-01-01
  • 2014-12-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-02-04
相关资源
最近更新 更多