为什么朱莉娅第一次打电话进入我的模块需要很长时间?

rev*_*ure 23 performance julia

基本上我的情况就是这个.我有一个模块(也导入了许多其他模块).

我有一个脚本,如:

import MyModule

tic()
MyModule.main()

tic()
MyModule.main()
Run Code Online (Sandbox Code Playgroud)

在MyModule中:

__precompile__()

module MyModule
    export main

    function main()
        toc()
        ...
    end
end
Run Code Online (Sandbox Code Playgroud)

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

Fen*_*ang 45

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

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

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

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

这意味着Julia预编译远比大多数静态语言的编译要多得多.例如,考虑以下Julia包,5000000050000000以非常低效的方式计算数字:

module TestPackage

export n

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

end
Run Code Online (Sandbox Code Playgroud)

在我的机器上:

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)
Run Code Online (Sandbox Code Playgroud)

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

__precompile__()

module TestPackage

export n

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

end
Run Code Online (Sandbox Code Playgroud)

并查看预编译期间和之后的性能:

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
Run Code Online (Sandbox Code Playgroud)

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


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

module TestPackage

println("Hello, World")

end
Run Code Online (Sandbox Code Playgroud)

这就是它的行为方式:

julia> using TestPackage
Hello, World

julia> workspace()

julia> using TestPackage
Hello, World
Run Code Online (Sandbox Code Playgroud)

现在让我们添加__precompile__()指令,结果现在是:

julia> using TestPackage
INFO: Precompiling module TestPackage.
Hello, World

julia> workspace()

julia> using TestPackage
Run Code Online (Sandbox Code Playgroud)

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

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

我们按如下方式重新设计包:

__precompile__()

module TestPackage

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

end
Run Code Online (Sandbox Code Playgroud)

产生以下结果:

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
Run Code Online (Sandbox Code Playgroud)

上面例子的要点可能是惊喜,并且希望能够阐明.理解预编译的第一步是理解它与通常编译静态语言的方式不同.像Julia这样的动态语言中的预编译意味着:

  • 所有顶级语句都在预编译时执行,而不是在加载时执行.
  • 必须将在加载时执行的任何语句移动到该__init__函数.

这也应该更清楚地说明为什么默认情况下不打开预编译:它并不总是安全的!软件包开发人员必须检查以确保他们没有使用任何具有副作用或不同结果的顶级语句,并将其移动到该__init__函数中.

那么这与首次调用模块的延迟有什么关系呢?那么,让我们看一个更实际的例子:

__precompile__()

module TestPackage

export cube

square(x) = x * x
cube(x) = x * square(x)

end
Run Code Online (Sandbox Code Playgroud)

并做同样的测量:

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)
Run Code Online (Sandbox Code Playgroud)

预编译后,加载变得更快.那是因为在预编译期间,语句square(x) = x^2cube(x) = x * square(x)执行.这些是与其他语句一样的顶级语句,它们涉及一定程度的工作.必须解析,降低表达式square以及cube模块内的名称和绑定.(还有一个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
Run Code Online (Sandbox Code Playgroud)

这里发生了什么?cube当有明确的__precompile__()指令时,为什么需要再次编译?为什么编译的结果不能保存?

答案非常简单:

  • cube(::Int)预编译期间从未编译.这可以从以下三个事实可以看出:预编译执行,类型推断和代码生成不发生,直到执行(除非迫不得已),该模块不包含执行cube(::Int).
  • 一旦我输入cube(5)REPL,这就不再是预编译时间了.我的REPL运行的结果没有得到保存.

以下是解决问题的方法:在所需的参数类型上执行 cube函数.

__precompile__()

module TestPackage

export cube

square(x) = x * x
cube(x) = x * square(x)

# precompile hints
cube(0)

end
Run Code Online (Sandbox Code Playgroud)

然后

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
Run Code Online (Sandbox Code Playgroud)

还有一些首次使用的开销; 但是,请特别注意首次运行的分配编号.这次,我们已经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
Run Code Online (Sandbox Code Playgroud)

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

有时候,你想强迫Julia编译一些东西(如果在预编译期间发生这种情况,可能会将其保存到缓存中)而不实际运行它.这就是precompile可以添加到预编译提示中的函数的用途.


最后,请注意预编译的以下限制:

  • 预编译仅缓存包的模块的结果,用于包的功能.如果您依赖其他模块的功能,那么这些功能将不会被预编译.
  • 预编译仅支持可序列化的结果.特别是,作为C对象并包含C指针的结果通常不可序列化.这包括BigIntBigFloat.


Chr*_*kas 8

快速回答是,第一次运行它必须编译的函数时,所以你要测量编译时间.如果您不知道这一点,请参阅性能提示.

但我会假设你知道这一点,但它仍然困扰着你.原因是因为Julia中的模块不能编译:模块是动态范围.当您在REPL中玩游戏时,您正在使用Main模块.当您使用Juno并单击模块中的代码时,它将评估模块中的代码,从而为您提供在非主模块中动态播放的快速方法(我认为您可以将REPL范围更改为另一个模块也是).模块是动态的,因此它们无法编译(当您看到模块预编译时,它实际上只是预编译了许多在其中定义的函数).(这就是为什么动态的事情eval发生在模块的全局范围内).

所以当你main输入一个模块时,这与在REPL中使用它没有什么不同.因此,全局范围的模块具有与REPL相同的类型稳定性/推理问题(但REPL仅是Main模块的全局范围).所以,就像在REPL中一样,第一次调用它必须编译的函数.