为什么这个 Julia 片段比 Python 等价的代码慢这么多?(有字典)

Jan*_*ano 10 python julia

我在 Python Jupyter 中有以下代码:

n = 10**7
d = {}

%timeit for i in range(n): d[i] = i

%timeit for i in range(n): _ = d[i]

%timeit d[10]
Run Code Online (Sandbox Code Playgroud)

以下时间:

763 ms ± 19.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
692 ms ± 3.74 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
39.5 ns ± 0.186 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
Run Code Online (Sandbox Code Playgroud)

这在朱莉娅身上

using BenchmarkTools
d = Dict{Int64, Int64}()
n = 10^7
r = 1:n

@btime begin
    for i in r
        d[i] = i
    end
end
    
@btime begin
    for i in r
        _ = d[i]
    end
end

@btime d[10]
Run Code Online (Sandbox Code Playgroud)

随着时间:

  2.951 s (29999490 allocations: 610.34 MiB)
  3.327 s (39998979 allocations: 762.92 MiB)
  20.163 ns (0 allocations: 0 bytes)
Run Code Online (Sandbox Code Playgroud)

我不太能理解的是,为什么 Julia 的字典值分配和循环检索(前两个测试)似乎要慢得多,但同时在单键检索(最后一个测试)中要快得多)。在循环中它似乎慢 4 倍,但如果不在循环中则快两倍。我是 Julia 的新手,所以我不确定我是否在做一些不理想的事情,或者这是否在意料之中。

Bog*_*ski 13

由于您在顶级范围内进行基准测试@btime$因此您必须插入变量,因此对代码进行基准测试的方法是:

julia> using BenchmarkTools

julia> d = Dict{Int64, Int64}()
Dict{Int64, Int64}()

julia> n = 10^7
10000000

julia> r = 1:n
1:10000000

julia> @btime begin
           for i in $r
               $d[i] = i
           end
       end
  842.891 ms (0 allocations: 0 bytes)

julia> @btime begin
           for i in $r
               _ = $d[i]
           end
       end
  618.808 ms (0 allocations: 0 bytes)

julia> @btime $d[10]
  6.300 ns (0 allocations: 0 bytes)
10
Run Code Online (Sandbox Code Playgroud)

在 Jupyter Notebook 的同一台机器上运行 Python 3 的时间是:

n = int(10.0**7)
d = {}
%timeit for i in range(n): d[i] = i
913 ms ± 87.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit for i in range(n): _ = d[i]
816 ms ± 92.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit d[10]
50.2 ns ± 2.97 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
Run Code Online (Sandbox Code Playgroud)

但是,对于第一个操作,我假设您更想对此进行基准测试:

julia> function f(n)
           d = Dict{Int64, Int64}()
           for i in 1:n
               d[i] = i
           end
       end
f (generic function with 1 method)

julia> @btime f($n)
  1.069 s (72 allocations: 541.17 MiB)
Run Code Online (Sandbox Code Playgroud)

反对这一点:

def f(n):
    d = {}
    for i in range(n):
        d[i] = i
%timeit f(n)
1.18 s ± 65.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Run Code Online (Sandbox Code Playgroud)

还应该注意的是,使用特定的值n可能会产生误导,因为 Julia 和 Python 不能保证在同一时刻将它们的集合大小调整为相同的新大小(为了存储字典,您通常会分配比需要更多的内存)避免哈希冲突,这里实际上测试的特定值n可能很重要)。

编辑

请注意,如果我将全局变量声明为constall 很快,那么编译器可以优化代码(它知道绑定到全局变量的值的类型不能改变);因此$不需要使用:

julia> using BenchmarkTools

julia> const d = Dict{Int64, Int64}()
Dict{Int64, Int64}()

julia> const n = 10^7
10000000

julia> const r = 1:n
1:10000000

julia> @btime begin
           for i in r
               d[i] = i
           end
       end
  895.788 ms (0 allocations: 0 bytes)

julia> @btime begin
           for i in $r
               _ = $d[i]
           end
       end
  582.214 ms (0 allocations: 0 bytes)

julia> @btime $d[10]
  6.800 ns (0 allocations: 0 bytes)
10
Run Code Online (Sandbox Code Playgroud)

如果您好奇拥有对线程的本机支持有什么好处,这里是一个简单的基准测试(此功能是语言的一部分):

julia> Threads.nthreads()
4

julia> @btime begin
           Threads.@threads for i in $r
               _ = $d[i]
           end
       end
  215.461 ms (23 allocations: 2.17 KiB)
Run Code Online (Sandbox Code Playgroud)

  • 如果没有“$”,正如您在基准测试中看到的那样,您有数亿个分配。原因是(除其他外,但让我指出一个原因)Julia 本身支持多线程。因此,如果您使用全局变量,编译器无法确定该变量在其他线程执行代码期间不会反弹为不同类型的值。因此它做了比需要的更多的工作(因为它必须准备好将变量绑定到“任何”事物)。当您使用“$”时,该值将被插入到执行的代码块中,因此 Julia 知道它的类型不会改变。 (5认同)