如何通过防止内存分配来提高 Julia 代码性能?

CRq*_*tum 4 julia

我正在阅读 Julia 性能提示, https://docs.julialang.org/en/v1/manual/performance-tips/

一开始就提到了两个例子。

例一,

julia> x = rand(1000);

julia> function sum_global()
       s = 0.0
       for i in x
           s += i
       end
       return s
   end;

julia> @time sum_global()
0.009639 seconds (7.36 k allocations: 300.310 KiB, 98.32% compilation time)
496.84883432553846

julia> @time sum_global()
0.000140 seconds (3.49 k allocations: 70.313 KiB) 
496.84883432553846
Run Code Online (Sandbox Code Playgroud)

我们看到很多内存分配。

现在例子2,

julia> x = rand(1000);

julia> function sum_arg(x)
       s = 0.0
       for i in x
           s += i
       end
       return s
   end;

julia> @time sum_arg(x)
0.006202 seconds (4.18 k allocations: 217.860 KiB, 99.72% compilation time)
496.84883432553846

julia> @time sum_arg(x)
0.000005 seconds (1 allocation: 16 bytes)
496.84883432553846
Run Code Online (Sandbox Code Playgroud)

我们看到,通过将 x 放入函数的参数中,内存分配几乎消失了,速度也快了很多。

我的问题是,谁能解释一下,

  1. 为什么示例 1 需要这么多分配,为什么示例 2 不需要与示例 1 一样多的分配?我有点困惑。

  2. 在这两个例子中,我们看到第二次运行 Julia 时,它总是比第一次快。这是否意味着我们需要运行 Julia 两次?如果 Julia 只在第二次运行时很快,那么重点是什么?为什么 Julia 不先编译,然后运行,就像 Fortran 一样?

  3. 是否有阻止内存分配的一般规则?还是我们总是需要做一个@time 来确定问题?

谢谢!

Bog*_*ski 7

为什么示例 1 需要这么多分配,为什么示例 2 不需要与示例 1 一样多的分配?

示例 1 需要如此多的分配,因为x是一个全局变量(在函数范围之外定义sum_arg)。因此,变量的类型x可能随时改变,即有可能:

  1. 你定义xsum_arg
  2. 你编译 sum_arg
  3. 您重新定义x(更改其类型)并运行sum_arg

特别是,由于 Julia 支持多线程,一般而言,步骤 3 中的两个操作甚至可以并行发生(即,您可以x在一个线程中更改类型,同时sum_arg在另一个线程中运行)。

所以因为编译后sum_arg的类型x可以改变 Julia,所以在编译时sum_arg必须确保编译后的代码不依赖于x编译时存在的类型。相反,在这种情况下,Julia 允许x动态更改的类型。然而, allowed 的这种动态特性x意味着它必须在运行时(而不是编译时)进行检查。这种动态检查x会导致性能下降和内存分配。

您可以通过声明x为 a 来解决这个问题const(以const确保类型x不会改变):

julia> const x = rand(1000);

julia> function sum_global()
           s = 0.0
           for i in x
               s += i
           end
           return s
       end;

julia> @time sum_global() # this is now fast
  0.000002 seconds
498.9290555615045
Run Code Online (Sandbox Code Playgroud)

为什么 Julia 不先编译,然后运行,就像 Fortran 一样?

这正是朱莉娅所做的。但是,Julia 的好处是它会在需要时自动进行编译。这使您可以进行流畅的交互式开发过程。

如果您愿意,可以在使用该precompile函数运行之前编译该函数,然后单独运行它。然而,通常人们只是运行该函数而没有明确地执行它。

结果是,如果您使用@time

  1. 第一次运行函数时,它会返回执行时间和编译时间(正如您在粘贴的示例中看到的那样 - 您会获得编译所花费时间百分比的信息)。
  2. 在连续运行中,函数已经被编译,所以只返回执行时间。

是否有阻止内存分配的一般规则?

这些规则在您在问题中引用的手册的性能提示部分中完全给出。使用提示@time是那里的诊断提示。所有其他提示都是为获得快速代码而推荐的规则。但是,我知道该列表很长,因此根据我的经验,可以从一个较短的列表开始:

  1. 避免全局变量
  2. 避免使用抽象类型参数的容器
  3. 写类型稳定函数
  4. 避免改变变量的类型