【问题标题】:Problem with memory allocation in Julia codeJulia 代码中的内存分配问题
【发布时间】:2022-01-19 06:36:23
【问题描述】:

我在 Python/Numpy 中使用了一个函数来解决 combinatorial game theory 中的一个问题。

import numpy as np
from time import time

def problem(c):
    start = time()
    N = np.array([0, 0])
    U = np.arange(c)
    
    for _ in U:
        bits = np.bitwise_xor(N[:-1], N[-2::-1])
        N = np.append(N, np.setdiff1d(U, bits).min())

    return len(*np.where(N==0)), time()-start 

problem(10000)

然后我用 Julia 编写它,因为我认为 Julia 使用即时编译会更快。

function problem(c)
    N = [0]
    U = Vector(0:c)
    
    for _ in U
        elems = N[1:length(N)-1]
        bits = elems .⊻ reverse(elems)
        push!(N, minimum(setdiff(U, bits))) 
    end
    
    return sum(N .== 0)
end

@time problem(10000)

但第二个版本要慢得多。对于 c = 10000,Python 版本需要 2.5 秒。在 Core i5 处理器上,Julia 版本需要 4.5 秒。由于 Numpy 操作是用 C 实现的,我想知道 Python 是否确实更快,或者我正在编写一个浪费时间复杂度的函数。

Julia 中的实现分配了大量内存。如何减少分配次数以提高其性能?

【问题讨论】:

  • 问自己“语言 X 比语言 Y 快吗?”通常是红鲱鱼。除了诸如不同实现之类的技术细节之外,大多数语言都不是“纯”使用的——比如你的 Python 程序通过numpy 调用编译语言的代码——而且没有免费的午餐:JIT 以牺牲长期性能为代价短期热身。 您在 Julia 中的特定程序比在 Python 中的您的特定程序慢。
  • 从 1 个数据点推断“X 是否优于 Y”有点危险,你不觉得吗?
  • 我认为这不应该被关闭。它需要一些编辑,但具体的比较是在主题上。
  • Julia 中的代码编写效率不高。主要的低效率是它进行了大量的数据复制和分配(请注意,在 Python 中索引创建一个视图,而不是 Julia,您必须选择加入)。我重新编写了它,以使 Julia 执行时间提高约 40 倍。我已经编辑了问题以反映这一点并投票重新打开
  • setdiff 的性能在 Python 和 Julia 之间可能有所不同,但我不明白为什么 Julia 应该在这里变慢 - 这是一个可能经过优化的基本库函数。如果是,那么它应该在 Julia 中修复。我没有在我当前的机器上安装 Python 来进行基准测试。另一方面,从原始代码中可以清楚地看出它在几个地方进行了不必要的分配,因此我的评论。

标签: python julia game-theory


【解决方案1】:

原代码可以改写如下:

function problem2(c)
    N = zeros(Int, c+2)
    notseen = falses(c+1)

    for lN in 1:c+1
        notseen .= true
        @inbounds for i in 1:lN-1
            b = N[i] ⊻ N[lN-i]
            b <= c && (notseen[b+1] = false)
        end
        idx = findfirst(notseen)
        isnothing(idx) || (N[lN+1] = idx-1)
    end
    return count(==(0), N)
end

首先检查函数是否产生相同的结果:

julia> problem(10000), problem2(10000)
(1475, 1475)

(我还检查了生成的N 向量是否相同)

现在让我们对这两个函数进行基准测试:

julia> using BenchmarkTools

julia> @btime problem(10000)
  4.938 s (163884 allocations: 3.25 GiB)
1475

julia> @btime problem2(10000)
  76.275 ms (4 allocations: 79.59 KiB)
1475

所以它的速度提高了 60 倍以上。

我为提高性能所做的就是避免分配。在 Julia 中,它既简单又高效。如果代码的任何部分不清楚,请发表评论。请注意,我专注于展示如何提高 Julia 代码的性能(而不是试图仅仅复制 Python 代码,因为 - 正如在原始帖子下所评论的那样 - 进行语言性能比较非常棘手)。我认为最好集中讨论如何使 Julia 代码更快。


编辑

确实更改为Vector{Bool} 并删除bc 关系上的条件(数学上适用于c 的这些值)提供了更好的速度:

julia> function problem3(c)
           N = zeros(Int, c+2)
           notseen = Vector{Bool}(undef, c+1)

           for lN in 1:c+1
               notseen .= true
               @inbounds for i in 1:lN-1
                   b = N[i] ⊻ N[lN-i]
                   notseen[b+1] = false
               end
               idx = findfirst(notseen)
               isnothing(idx) || (N[lN+1] = idx-1)
           end
           return count(==(0), N)
       end
problem3 (generic function with 1 method)

julia> @btime problem3(10000)
  20.714 ms (3 allocations: 88.17 KiB)
1475

【讨论】:

  • for i in 1:lN-1 循环必须写成例如Numba 或 Cython。我认为这在 Python 中并不容易实现(但我可能错了)。
  • 我并不是说一定要以完全相同的方式重写,而是对仅使用 NumPy 的版本更感兴趣。我只是认为 setdiff 花费了大部分时间,并且可以类似地替换它(但仍然使用 NumPy 操作)。
  • notseen[b+1] = (b &gt; c) &amp;&amp; notseen[b+1] 移动分支,也许会稍微快一点。
  • 哈!使用notseen = fill(false, c+1) 可以让您使用notseen[b+1] = (b &gt; c) &amp; notseen[b+1] 而不会产生位旋转的开销,只需少量内存开销即可将时间减半。
  • 在我的笔记本电脑上:python:2.6 秒,julia:problem:4.4 秒,problem2:0.09,problem3:0.04
猜你喜欢
  • 1970-01-01
  • 2015-03-27
  • 2011-01-07
  • 2011-03-18
  • 2010-10-24
  • 1970-01-01
相关资源
最近更新 更多