为什么 Juliabox 教程中的 Julia 插值不起作用?

Why does Julia interpolation not work in juliabox tutorial?

我正在使用 juliabox.com 来学习 Julia,但我遇到了一个非常基本的问题。我不能做我被告知要做的插值:

sum($foo) 只是不像描述的那样工作,它只是 returns "syntax: "$"expression outside quote"。这是在 https://www.juliabox.com/notebook/notebooks/tutorials/introductory-tutorials/intro-to-julia/Exploring_benchmarking_and_performance.ipynb .

是教程有问题,还是我有问题?

编辑:明确地说,我在这里的困惑是不知道“$”在这种情况下与@benchmark 配对。本教程没有说明这一点,所以我认为 sum($foo) 没有理由不起作用。现在我明白了。 (也许教程的措辞可以更清楚。)

您使用的教程旨在专门教您如何正确地对 Julia 代码进行基准测试。

理解 $ 的关键是它将一个值插入到基准表达式中,因此它的行为就像一个 Julia 在编译时知道其类型的变量 (https://github.com/JuliaCI/BenchmarkTools.jl/blob/master/doc/manual.md#interpolating-values-into-benchmark-expressions)。

为什么需要这个? Julia 程序的一个主要性能问题是使用全局变量 (https://docs.julialang.org/en/latest/manual/performance-tips/#Avoid-global-variables-1)。在以下代码中:

julia> using BenchmarkTools

julia> x = rand(10);

julia> @benchmark sum(x)
BenchmarkTools.Trial:
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     18.492 ns (0.00% GC)
  median time:      21.306 ns (0.00% GC)
  mean time:        30.284 ns (17.51% GC)
  maximum time:     38.387 μs (99.93% GC)
  --------------
  samples:          10000
  evals/sample:     995

变量x是全局的。

如果你写 $x 而不是 x 那么变量 x 将是本地的(因此它的类型在编译时对于 Julia 来说是已知的)。请注意,此 插值技巧 仅用于基准测试 - 不适用于实际代码:

julia> @benchmark sum($x)
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     4.199 ns (0.00% GC)
  median time:      5.399 ns (0.00% GC)
  mean time:        5.538 ns (0.00% GC)
  maximum time:     48.301 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

而性能差异恰恰是由于第一次 x 是全局的,而第二次 x 是局部的。

为了让 Julia 在编译时知道 x 的类型,请考虑以下代码:

julia> const y = x;

julia> @benchmark sum(y)
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     3.799 ns (0.00% GC)
  median time:      5.200 ns (0.00% GC)
  mean time:        5.490 ns (0.00% GC)
  maximum time:     30.900 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

julia> @benchmark sum($y)
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     4.199 ns (0.00% GC)
  median time:      5.699 ns (0.00% GC)
  mean time:        5.615 ns (0.00% GC)
  maximum time:     30.600 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

在这种情况下,y 是一个全局常量。因此编译器知道它的类型,即使它是全局的,所以在这种情况下,如果你写 y$y 并不重要。

现在您可能会问为什么 sum 不必被 $ 前缀。答案是 sum 是一个函数,因此它的类型在编译时是已知的。

另一种思考 $ 的方式(我稍微简化了一点,但这里实际上做了一些不同的事情,你可以使用 @macroexpand 宏来研究细节)是它把这个:

julia> f() = for i in 1:10^6
          sum(x)
       end

进入这个

julia> g(x) = for i in 1:10^6
          sum(x)
       end

现在,如果您使用简单的 @time 测量两个函数的时间,您将得到:

julia> @time f()
  0.032786 seconds (1.05 M allocations: 18.224 MiB)

julia> @time f()
  0.024807 seconds (1.00 M allocations: 15.259 MiB, 13.19% gc time)

julia> @time g(x)
  0.017912 seconds (53.07 k allocations: 2.990 MiB, 17.93% gc time)

julia> @time g(x)
  0.001044 seconds (4 allocations: 160 bytes)

(你应该看第二个时间,因为第一个包括编译时间)

总结

全局变量名称前缀 $ 仅用于基准测试目的。它确保您在类型稳定的上下文中获得有关函数性能的信息(这通常是您感兴趣的内容)。

额外注意事项

对 Julia 代码进行基准测试有时很棘手,因为它的编译器在优化代码方面非常积极。

例如比较:

julia> using BenchmarkTools

julia> const z = 1
1

julia> @benchmark sin(cos(z))
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     1.999 ns (0.00% GC)
  median time:      2.201 ns (0.00% GC)
  mean time:        2.394 ns (0.00% GC)
  maximum time:     28.301 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

julia> @benchmark sin(cos($z))
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     25.477 ns (0.00% GC)
  median time:      33.030 ns (0.00% GC)
  mean time:        35.307 ns (0.00% GC)
  maximum time:     106.747 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     993

您可能想知道为什么在这种情况下使用 $ 会使执行时间变慢。原因是因为 z 是一个常量,所以 sin(cos(z)) 在编译期间被完全评估(在 运行 时没有计算发生),所以发生的事情类似于:

julia> f() = sin(cos(z))
f (generic function with 1 method)

julia> @code_llvm f()

;  @ REPL[30]:1 within `f'
; Function Attrs: uwtable
define double @julia_f_16511() #0 {
top:
  ret double 0x3FE075ED0B926F7C
}

(你会看到如果调用 f() 它实际上不执行任何计算)。

另一方面,sin(cos($z)) 的扩展方式让 Julia 创建了一个新的局部变量,将其命名为 v,然后将 z 的值赋给它,并且最终在 运行 时计算 sin(cos(v))(但知道 v 的类型是 Int)。

请注意,这比:

julia> x = 1
1

julia> @benchmark sin(cos(x))
BenchmarkTools.Trial:
  memory estimate:  32 bytes
  allocs estimate:  2
  --------------
  minimum time:     39.246 ns (0.00% GC)
  median time:      54.638 ns (0.00% GC)
  mean time:        67.345 ns (8.80% GC)
  maximum time:     41.383 μs (99.82% GC)
  --------------
  samples:          10000
  evals/sample:     981

在这种情况下,编译器不知道 x 的类型。