performance - 为什么 Julia 在第一次调用我的模块时要花很长时间?

标签 performance julia

基本上我的情况是这样的。我有一个模块(它还导入了许多其他模块)。

我有一个像这样的脚本:

import MyModule

tic()
MyModule.main()

tic()
MyModule.main()

在我的模块中:
__precompile__()

module MyModule
    export main

    function main()
        toc()
        ...
    end
end

第一toc()调用输出大约 20 秒。第二个输出2.3e-5。任何人都可以猜测时间的去向吗? Julia 是否在第一次调用模块时进行了某种初始化,我怎么知道那是什么?

最佳答案

预编译可能会令人困惑。我将尝试解释它是如何工作的。

Julia 通过首先解析模块,然后一次一个运行所谓的“顶级”语句来加载模块。如果解释器不支持该特定的顶级语句,则降低每个顶级语句,然后解释(如果可能)或编译和执行。

什么 __precompile__ do 实际上相当简单(取模细节):它在预编译时执行上面列出的所有步骤。请注意,上述步骤包括执行,如果您更熟悉静态编译语言,这可能会令人惊讶。通常,在不执行动态代码的情况下预编译它是不可能的,因为代码的执行会导致诸如创建新函数、方法和类型之类的更改。

预编译运行和常规运行之间的区别在于来自预编译运行的可序列化信息被保存到缓存中。可序列化的事物包括来自解析和降低的 AST 以及类型推断的结果。

这意味着 Julia 预编译比大多数静态语言的编译更进一步。例如,考虑以下计算数字 5000000050000000 的 Julia 包。以一种非常低效的方式:

module TestPackage

export n

n = 0
for i in 1:10^8
    n += i
end

end

在我的机器上:
julia> @time using TestPackage
  2.151297 seconds (200.00 M allocations: 2.980 GB, 8.12% gc time)

julia> workspace()

julia> @time using TestPackage
  2.018412 seconds (200.00 M allocations: 2.980 GB, 2.90% gc time)

现在让我们给 __precompile__()指令,将包更改为
__precompile__()

module TestPackage

export n

n = 0
for i in 1:10^8
    n += i
end

end

并查看预编译期间和之后的性能:
julia> @time using TestPackage
INFO: Precompiling module TestPackage.
  2.696702 seconds (222.21 k allocations: 9.293 MB)

julia> workspace()

julia> @time using TestPackage
  0.000206 seconds (340 allocations: 16.180 KB)

julia> n
5000000050000000

这里发生的事情是模块在预编译时运行,结果被保存。这与静态语言的编译器通常所做的不同。

预编译可以改变包的行为吗?当然。如前所述,预编译是在预编译时有效地运行包,而不是在加载时。这对于纯函数无关紧要(因为 referential transparency 保证它们的结果总是相同的),对于大多数非纯函数也无关紧要,但在某些情况下确实很重要。假设我们有一个除了 println("Hello, World!") 什么都不做的包加载时。没有预编译,它看起来像这样:
module TestPackage

println("Hello, World")

end

这就是它的行为方式:
julia> using TestPackage
Hello, World

julia> workspace()

julia> using TestPackage
Hello, World

现在让我们添加 __precompile__()指令,结果现在是:
julia> using TestPackage
INFO: Precompiling module TestPackage.
Hello, World

julia> workspace()

julia> using TestPackage

第二次加载就没有输出了!那是因为计算,println , 在编译包时已经完成,所以不再重复。对于那些习惯于编译静态语言的人来说,这是第二个惊喜。

这当然会引发不能仅在编译时完成的初始化步骤的问题;例如,如果我的包需要初始化的日期和时间,或者需要创建、维护或删除文件和套接字等资源。 (或者,在一个简单的情况下,需要将信息打印到终端。)所以有一个特殊的函数,它不是在预编译时调用,而是在加载时调用。这个函数叫做__init__功能。

我们重新设计我们的包如下:
__precompile__()

module TestPackage

function __init__()
    println("Hello, World")
end

end

产生以下结果:
julia> using TestPackage
INFO: Recompiling stale cache file /home/fengyang/.julia/lib/v0.6/TestPackage.ji for module TestPackage.
Hello, World

julia> workspace()

julia> using TestPackage
Hello, World

上面例子的重点可能是令人惊讶,并希望能说明问题。理解预编译的第一步是理解它与静态语言的典型编译方式不同。像 Julia 这样的动态语言中的预编译意味着:
  • 所有顶级语句都在预编译时执行,而不是在加载时执行。
  • 任何要在加载时执行的语句都必须移动到 __init__功能。

  • 这也应该更清楚为什么默认情况下不打开预编译:它并不总是安全的!包开发人员必须检查以确保他们没有使用任何具有副作用或不同结果的顶级语句,并将它们移至 __init__功能。

    那么这与第一次调用模块时的延迟有什么关系呢?好吧,让我们看一个更实际的例子:
    __precompile__()
    
    module TestPackage
    
    export cube
    
    square(x) = x * x
    cube(x) = x * square(x)
    
    end
    

    并进行相同的测量:
    julia> @time using TestPackage
    INFO: Recompiling stale cache file /home/fengyang/.julia/lib/v0.6/TestPackage.ji for module TestPackage.
      0.310932 seconds (1.23 k allocations: 56.328 KB)
    
    julia> workspace()
    
    julia> @time using TestPackage
      0.000341 seconds (352 allocations: 17.047 KB)
    

    预编译后,加载变得更快。这是因为在预编译期间,语句 square(x) = x^2cube(x) = x * square(x)被执行。这些是与其他任何其他语句一样的顶级语句,它们涉及一定程度的工作。表达式必须被解析、降低,并且名称 squarecube绑定(bind)在模块内部。 (还有 export 语句,成本较低,但仍需要执行。)但正如您注意到的:
    julia> @time using TestPackage
    INFO: Recompiling stale cache file /home/fengyang/.julia/lib/v0.6/TestPackage.ji for module TestPackage.
      0.402770 seconds (220.37 k allocations: 9.206 MB)
    
    julia> @time cube(5)
      0.003710 seconds (483 allocations: 26.096 KB)
    125
    
    julia> @time cube(5)
      0.000003 seconds (4 allocations: 160 bytes)
    125
    
    julia> workspace()
    
    julia> @time using TestPackage
      0.000220 seconds (370 allocations: 18.164 KB)
    
    julia> @time cube(5)
      0.003542 seconds (483 allocations: 26.096 KB)
    125
    
    julia> @time cube(5)
      0.000003 seconds (4 allocations: 160 bytes)
    125
    

    这里发生了什么?为什么cube需要再次编译,当明显有__precompile__()指示?为什么编译的结果没有被保存?

    答案相当简单:
  • cube(::Int)在预编译期间从未编译过。这可以从以下三个事实中看出:预编译是执行,类型推断和代码生成直到执行才发生(除非强制),并且模块不包含 cube(::Int) 的执行。 .
  • 一旦我输入 cube(5)在 REPL 中,这不再是预编译时。我的 REPL 运行结果没有得到保存。

  • 以下是解决问题的方法:对所需的参数类型执行多维数据集函数。
    __precompile__()
    
    module TestPackage
    
    export cube
    
    square(x) = x * x
    cube(x) = x * square(x)
    
    # precompile hints
    cube(0)
    
    end
    

    然后
    julia> @time using TestPackage
    INFO: Recompiling stale cache file /home/fengyang/.julia/lib/v0.6/TestPackage.ji for module TestPackage.
      0.411265 seconds (220.25 k allocations: 9.200 MB)
    
    julia> @time cube(5)
      0.003004 seconds (15 allocations: 960 bytes)
    125
    
    julia> @time cube(5)
      0.000003 seconds (4 allocations: 160 bytes)
    125
    

    仍然有一些首次使用的开销;但是,请特别注意首次运行的分配编号。这一次,我们已经为cube(::Int)推断并生成了代码。预编译时的方法。推理和代码生成的结果被保存,并且可以从缓存加载(这更快并且需要更少的运行时分配)而不是重做。当然,与我们的玩具示例相比,实际负载的好处更显着。

    但:
    julia> @time cube(5.)
      0.004048 seconds (439 allocations: 23.930 KB)
    125.0
    
    julia> @time cube(5.)
      0.000002 seconds (5 allocations: 176 bytes)
    125.0
    

    由于我们只执行了 cube(0) ,我们只推断和编译了cube(::Int)方法,等第一次运行 cube(5.)仍然需要推理和代码生成。

    有时,您希望强制 Julia 编译某些内容(可能将其保存到缓存中,如果在预编译期间发生这种情况)而不实际运行它。这就是precompile可以添加到预编译提示中的函数是为了。

    最后,请注意预编译的以下限制:
  • 预编译仅缓存来自包模块的结果,用于包的功能。如果您依赖于其他模块的函数,那么这些函数将不会被预编译。
  • 预编译只支持可序列化的结果。特别是,作为 C 对象并包含 C 指针的结果通常是不可序列化的。这包括 BigIntBigFloat .
  • 关于performance - 为什么 Julia 在第一次调用我的模块时要花很长时间?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40116045/

    相关文章:

    c# - 比较 3 个不同列表计数的有效方法

    macros - Julia:如何创建一个返回其参数的宏?

    julia - Julia 中的参数仿函数

    macros - Julia 宏中的无效赋值错误

    objective-c - 编写许多小方法对 Objective-C 有性能影响吗?

    Python:使用相同的参数多次调用同一个函数,还是将结果保存为中间值?

    c++ - 从一组最接近直线的点中找到点的最快算法是什么?

    visual-studio - VS 2008,分析多个测试

    julia - 在 Julia 中使用 ForwardDiff 进行自动微分

    julia - 在 Julia 中广播数组和求和,无需临时矩阵分配