将 @time 报告的 Julia 中大向量的分配加倍

Double the allocations reported by @time for large vectors in Julia

考虑以下使用 Julia 编写的简单程序:

function foo_time(x)
    @time x.^2
    return nothing
end
n = 1000;
foo_time(collect(1:n));

如果我 运行 在我的控制台中,然后 @time 报告 1 分配,这正是我所期望的。但是,如果我将 n 更改为 10000,则 @time 报告 2 个分配。

更重要的是,如果我在没有句法循环融合的情况下(换句话说,没有点)将函数链接在一起,那么我似乎得到了预期分配的两倍。例如,写 (x + x).^2 + x 而不是 x.^2 会产生 3 次分配 n = 1000,但会产生 6 次分配 n = 10000。 (虽然该模式并不严格继续:例如,(x + x + x).^2 只为 n = 10000 产生 5 次分配。)

为什么向量的大小会影响发生的分配次数?幕后发生了什么?

这在 JupyterLab 控制台和普通的 Julia REPL 中都会发生。

为什么有一个小向量分配和两个大向量分配?

真的,这无关紧要,这是数组工作原理的内部细节。 Julia Array 本质上有两个部分:内部 header(跟踪数组的维度和元素类型等)和数据本身。当数组很小时,将这两个数据段捆绑在一起有优势,但当数组很大时,将它们分开会有优势。这不是广播的事情,它只是数组分配的事情:

julia> f(n) = (@time Vector{Int}(undef, n); nothing)
f (generic function with 1 method)

julia> f(2048)
  0.000003 seconds (1 allocation: 16.125 KiB)

julia> f(2049)
  0.000003 seconds (2 allocations: 16.141 KiB)

然后希望您能明白为什么当涉及临时对象时,这会导致大型数组的分配数量加倍 — 每个数组的 header 一个,每个数组的数据一个。

简而言之——不要太担心分配的数量。有时分配实际上可以提高性能。然而,当您看到大量分配时需要担心——尤其是当您看到它们与数组中的元素数量成正比时。

我同意 Matt 的观点,对于这个简单的任务,分配数量不是一个好的指标。

如果您想深入了解细节并准确了解您的代码是如何编译和执行的,我建议您使用这些宏 @code_llvm@code_lowered@code_native@code_typed@code_warntype。这些宏之间的所有细节都在 Julia 文档 here and there.

中进行了详细解释
julia> f(x) = x.^2
f (generic function with 1 method)

julia> @code_lowered f(randn(10000))
CodeInfo(
1 ─ %1 = (Core.apply_type)(Base.Val, 2)
│   %2 = (%1)()
│   %3 = (Base.broadcasted)(Base.literal_pow, Main.:^, x, %2)
│   %4 = (Base.materialize)(%3)
└──      return %4
)

julia> f2(x) = (x + x).^2 + x
f2 (generic function with 1 method)

julia> @code_lowered f2(randn(10000))
CodeInfo(
1 ─ %1 = x + x
│   %2 = (Core.apply_type)(Base.Val, 2)
│   %3 = (%2)()
│   %4 = (Base.broadcasted)(Base.literal_pow, Main.:^, %1, %3)
│   %5 = (Base.materialize)(%4)
│   %6 = %5 + x
└──      return %6
)