AnyMath 文档

提前编译

本文档描述了Julia中ahead-of-time(aot)编译系统的设计和结构。 生成系统映像和包映像时使用此系统。 这里描述的大部分实现位于 aotcompile。cpp, 静态数据。c,而 处理器。cpp

导言

虽然Julia通常编译代码实时(JIT),但可以提前编译代码并将生成的代码保存到文件中。 这可能是有用的原因有很多:

  1. 以减少启动Julia进程所需的时间。

  2. 以减少在JIT编译器中花费的时间,而不是执行代码(首次执行时间,TTFX)。

  3. 以减少JIT编译器使用的内存量。

高级别概览

以下描述是在用户编译新AOT模块时在内部发生的端到端管道的当前实现细节的快照,例如在他们键入时发生 使用Foo. 随着我们实现更好的处理方法,这些细节可能会随着时间的推移而改变,因此当前的实现可能与下面描述的数据流和函数不完全匹配。

编译代码图像

首先,必须识别需要编译为本机代码的方法。 这只能通过实际执行要编译的代码来完成,因为需要编译的方法集取决于传递给方法的参数的类型,并且具有某些类型组合的方法调用可能在运行时才知道。 在此过程中,将跟踪编译器看到的确切方法以供以后编译,从而生成编译跟踪。

注意当前在编译映像时,Julia在与执行AOT编译的过程不同的过程中运行跟踪生成。 在预编译期间尝试使用调试器时,这可能会产生影响。 使用调试器调试预编译的最佳方法是使用rr调试器,记录整个进程树,使用 rr ps 以识别相关的失败过程,然后使用 rr重播-p PID 重播失败的过程。

一旦确定了要编译的方法,它们就会传递给 jl_create_system_image 函数。 此函数设置将在将本机代码序列化到文件时使用的许多数据结构,然后调用 [医]创造性 与方法的阵列。 [医]创造性 在方法上运行codegen会生成一个或多个LLVM模块。 jl_create_system_image 然后记录一些有用的信息,说明代码从模块中产生了什么。

然后将模块传递给 jl_dump_native,连同由 jl_create_system_image. jl_dump_native 包含将模块序列化为位码、对象或程序集文件所需的代码,具体取决于传递给Julia的命令行选项。 然后将序列化的代码和信息作为存档写入文件。

最后一步是在由 jl_dump_native. 完成此步骤后,将生成包含已编译代码的共享库。

加载代码图像

加载代码映像时,链接器生成的共享库被加载到内存中。 然后从共享库加载系统映像数据。 此数据包含有关编译到共享库中的类型、方法和代码实例的信息。 此数据用于将运行时的状态还原到编译代码映像时的状态。

如果代码映像是使用多版本控制编译的,则加载程序将根据当前计算机上可用的CPU功能选择要使用的每个函数的适当版本。

对于系统映像,由于没有加载其他代码,运行时的状态现在与编译代码映像时的状态相同。 对于包映像,与编译代码时相比,环境可能发生了变化,因此必须对照全局方法表检查每个方法,以确定它是否仍然是有效的代码。

编译方法

跟踪编译的方法

Julia有一个命令行标志来记录由JIT编译器编译的所有方法, --跟踪-编译=文件名. 当一个函数被编译并且这个标志有一个文件名时,Julia将用它被调用的方法和参数类型向该文件打印一个预编译语句。 因此,这会生成一个预编译脚本,可在稍后的aot编译过程中使用。 该https://julialang.github.io/PrecompileTools.jl/stable/[PrecompileTools]软件包具有工具,可以使软件包开发人员更容易利用此功能。

jl_create_system_image

jl_create_system_image 保存以后恢复运行时状态所需的所有Julia特定元数据。 这包括代码实例、方法实例、方法表和类型信息等数据。 此函数还设置将本机代码序列化为文件所需的数据结构。 最后,它调用 [医]创造性 创建一个或多个LLVM模块,其中包含传递给它的方法的本机代码。 [医]创造性 负责在传递给它的方法上运行codegen。

jl_dump_native

jl_dump_native 负责将包含本机代码的LLVM模块序列化为文件。 除模块,所产生的系统图像数据 jl_create_system_image 被编译为全局变量。 此方法的输出是包含代码和系统映像数据的位码、对象和/或程序集存档。 jl_dump_native 通常是发射本机代码时较大的时间接收器之一,大部分时间都花在优化LLVM IR和发射机器代码上。 因此,该函数能够对优化和机器代码发射步骤进行多线程处理。 此多线程是根据模块的大小参数化的,但可以通过设置 JULIA_IMAGE_THREADS环境变量。 默认的最大线程数是可用线程数的一半,但将其设置为较低可以减少编译期间的峰值内存使用。 jl_dump_native 当与Julia加载器集成时,还可以生成针对多个体系结构优化的本机代码。 这是通过设置触发 JULIA_CPU_目标环境变量,由优化管道中的多版本控制传递介导。 为了使其与多线程一起工作,在将模块拆分为在其自己的线程上发出的子模块之前,添加了一个注释步骤,并且此注释步骤使用整个模块中可用的信息来决定为不同的体系结构克隆哪些函数。 一旦注释发生,单个线程就可以并行地为不同的体系结构发出代码,因为知道不同的子模块保证会产生将被克隆函数调用的必要函数。

有关模块序列化方式的其他一些元数据也存储在存档中,例如用于序列化模块的线程数和编译的函数数。

静态链接

Aot编译过程的最后一步是在由 jl_dump_native. 这将生成一个包含编译代码的共享库。 然后,Julia可以加载此共享库以恢复运行时的状态。 编译系统映像时,C编译器使用的本机链接器用于生成最终的共享库。 对于包映像,LLVM链接器LLD用于提供更一致的链接接口。

加载代码图像

加载共享库

加载代码映像的第一步是加载链接器生成的共享库。 这是通过调用 jl_dlopen 共享库的路径上。 此函数负责加载共享库并解析库中的所有符号。

加载本机代码

加载程序首先需要确定编译的本机代码对加载程序运行的体系结构是否有效。 这是避免执行旧Cpu无法识别的指令所必需的。 这是通过检查当前计算机上可用的CPU功能与编译代码的CPU功能来完成的。 启用多版本控制后,加载程序将根据当前计算机上可用的CPU功能选择要使用的每个功能的适当版本。 如果没有多版本化的功能集,则加载程序将引发错误。

多版本控制过程的一部分创建了模块中所有函数的多个全局数组。 当此过程是多线程时,将创建一个数组数组,加载程序将其重组为一个大数组,其中包含为此体系结构编译的所有函数。 模块中的全局变量也会发生类似的过程。

建立朱莉娅州

然后,加载器使用加载本机代码产生的全局变量和函数在当前进程中设置Julia运行时核心数据结构。 此设置涉及向Julia运行时添加类型和方法,并使缓存的本机代码可供其他Julia函数和解释器使用。 对于包映像,必须验证每个方法,因为全局方法表的状态必须与编译包映像的状态相匹配。 特别是,如果在加载时与包映像的编译时存在一组不同的方法,则必须在首次使用时使该方法无效并重新编译。 这是必要的,以确保执行语义保持不变,无论是预编译包还是直接执行代码。 系统映像不需要执行此验证,因为全局方法表在加载时为空。 因此,系统映像的加载时间比包映像快。