为什么 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
的类型。
我正在使用 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
的类型。