使用LLVM
Julia和LLVM之间的接口概述
默认情况下,Julia中的动态链接是使用LLVM执行的。 对于静态布局,使用参数`USE_LLVM_SHLIB=0`运行构建。
将AST Julia表示降级为中间表示(IR)或其直接解释的代码位于目录`src/`中。
档案 | 资料描述 |
---|---|
'aotcompile。cpp` |
进入编译器的C接口并发出目标文件 |
"建造。c` |
内置函数 |
"ccall。cpp` |
降级 'ccall' |
"古提斯。cpp` |
降低辅助函数,主要用于访问数组和元组 |
"科德根。cpp` |
顶级代码生成,传递列表,降低内置函数 |
'debuginfo。cpp` |
跟踪JIT代码的调试信息 |
""不高兴。cpp` |
处理机器目标文件并执行JIT代码反汇编 |
"gf。c` |
通用功能 |
"内在。cpp` |
降低内部功能 |
'jitlayers。cpp` |
与JIT相关的代码、级别和ORC编译辅助工具 |
'llvm-alloc-helpers。cpp` |
与Julia相关的逃逸分析 |
'llvm-alloc-opt。cpp` |
使用堆栈传输降级堆分配的LLVM自定义通行证 |
'llvm-cpufeatures。cpp` |
LLVM自定义传递降级基于CPU的函数(例如haveFMA) |
'llvm-demote-float16.cpp` |
LLVM自定义传递将16位浮点运算降级为32位浮点运算 |
'llvm-final-gc-lowering。cpp` |
LLVM自定义传递将垃圾收集器调用降级为最终表单 |
'llvm-gc-invariant-verifier。cpp` |
用于验证Julia垃圾回收不变量的LLVM自定义传递 |
'llvm-朱莉娅-licm。cpp` |
LLVM自定义传递提升或降低Julia的内部函数 |
'llvm-late-gc-lowering。cpp` |
垃圾收集器跟踪的LLVM自定义传递到"根"值 |
'llvm-低级处理程序。cpp` |
用于降低try-catch块的LLVM自定义传递 |
'llvm-muladd。cpp` |
LLVM自定义通行证用于快速FMA匹配 |
'llvm-多版本控制。cpp` |
LLVM自定义通行证,用于为多个体系结构生成系统映像代码 |
'llvm-propagate-addrspaces。cpp` |
地址空间规范化的LLVM用户通行证 |
'llvm-ptls。cpp` |
LLVM自定义传递降级TLS操作 |
'llvm-remove-addrspaces。cpp` |
LLVM自定义传递删除Julia地址空间 |
'llvm-remove-ni。cpp` |
LLVM自定义传递,用于删除Julia的非整数地址空间 |
'llvm-simdloop。cpp` |
LLVM用户通行证 '@simd' |
"管道。cpp` |
新通道管理器管道,通道管道分析 |
"系统。c` |
操作系统的I/O和辅助功能 |
一些`。cpp’文件形成被编译成单个对象的组。
内部函数和嵌入函数之间的区别在于嵌入函数功能齐全,可以像任何其他Julia函数一样使用。 内置函数只能处理解压缩的数据,因此其参数必须是静态类型的。
使用不同版本的LLVM构建Julia
默认LLVM版本在`deps/llvm’文件中指定。版`。 它可以通过创建一个名为"Make"的文件来重新定义。用户’在顶级目录中,并向其添加以下行:
LLVM_VER = 13.0.0
除了LLVM版本号之外,您还可以将参数`DEPS_GIT=llvm`与’USE_BINARYBUILDER_LLVM=0’组合设置为使用最新的LLVM开发版本构建。
您还可以通过在"Make"中指定参数`LLVM_DEBUG=1`或`LLVM_DEBUG=Release`来使用LLVM的调试版本进行构建。用户的文件。 在第一种情况下,LLVM构建将完全未优化,而在第二种情况下,它将被优化。 根据需要,第二种选择可能就足够了,而且速度要快得多。 使用’LLVM_DEBUG=Release’参数时,可能还需要设置’LLVM_ASSERTIONS=1’以启用不同传递的诊断。 默认情况下,此参数仅在`LLVM_DEBUG=1’时启用。
向LLVM传递参数
您可以使用环境变量将参数传递给LLVM。 'JULIA_LLVM_ARGS'。 下面是使用`bash`语法的参数示例:
-
'export JULIA_LLVM_ARGS=-print-after-all’每次通过后输出IR;
-
'export JULIA_LLVM_ARGS=-debug-only=loop-vectorize’outputs diagnostics'DEBUG(...)'LLVM用于循环矢量化器。 如果收到有关未知命令行参数的警告,请使用参数`LLVM_ASSERTIONS=1’重新构建LLVM。
-
'export JULIA_LLVM_ARGS=-help’显示可用参数列表。 'export JULIA_LLVM_ARGS=-help-hidden’显示其他参数。
-
'export JULIA_LLVM_ARGS="-fatal-warnings-print-options"`是使用多个参数的示例。
`JULIA_LLVM_ARGS’的有用参数
-
'-print-after=PASS':在执行`PASS’后输出IR;用于检查pass所做的更改。
-
'-print-before=PASS':在执行`PASS’之前输出IR;用于验证输入数据的pass。
-
'-print-changed':每当通道更改IR时输出IR;用于识别导致问题的通道。
-
`-print-(before|after)=MARKER-PASS':Julia管道包括几个标记通道,可用于识别出现问题或发生优化的地方。 标记传递定义为在管道中出现一次且在IR中不执行任何转换的传递。 它只对执行前或执行后的结论有用。 目前,管道中存在以下标记传递: 优化前 简化前 事后简化 优化前 后优化 优化前 之前 AfterLICM 简化前 后简化 后优化 优化前 AfterScalarOptimization 实施前 AfterVectorization 在购买前 后处理 清理前 清理后 优化后
-
'-time-passes':显示每个通行证所花费的时间;用于识别耗时的通行证。
-
'-print-module-scope`:与`-print-(before|after)'结合使用,获取整个模块,而不是传递期间接收的IR单元。
-
'-debug':在LLVM上输出大量调试信息。
-
'-debug-only=NAME`:从将`DEBUG_TYPE`定义为`NAME’的文件输出调试消息;对于获取有关问题的其他上下文很有用。
LLVM转换的独立调试
有时,将LLVM转换与Julia系统的其他部分分开调试可能会很有用,例如,因为在julia中重现问题会花费太多时间,或者因为您需要使用LLVM工具(例如,bugpoint)。
首先,您可以安装用于使用LLVM的开发人员工具,如下所示。
make -C deps install-llvm-tools
要获得整个系统图像的未优化IR表示,请传递参数'--output-unpt-bc unpt。bc’在构建系统映像的过程中。 结果,未优化的IR表示将被输出到文件’unpt。bc`。 然后可以按照通常的方式将此文件传输到LLVM工具。 Libjulia库可以充当LLVM通道的插件,并加载到LLVM工具中,以便在适当的环境中提供与Julia相关的通道。 此外,它还提供了`-julia’元传递,它执行应用于IR的整个Julia传递管道。 例如,要使用旧的过道管理器创建系统映像,可以执行以下操作。
llc -o sys.o opt.bc cc -shared -o sys.so sys.o
要使用新的通道管理器创建系统的映像,您可以执行以下操作。
opt -load-pass-plugin=libjulia-codegen.so --passes='julia' -o opt.bc unopt.bc llc -o sys.o opt.bc cc -shared -o sys.so sys.o
然后,julia可以以通常的方式加载此系统映像。
此外,您可以只为一个Julia函数输出LLVM IR模块的转储,如下所示:
fun, T = +, Tuple{Int,Int} # Подставьте здесь интересующую вас функцию
optimize = false
open("plus.ll", "w") do file
println(file, InteractiveUtils._dump_function(fun, T, false, false, false, true, :att, optimize, :default, false))
end
这些文件可以以与IR系统图像的上述未优化表示完全相同的方式处理。
改进Julia的LLVM优化
为了改进LLVM代码生成,通常需要使Julia代码降级与LLVM传递更兼容,或者优化传递。
如果您要优化通道,请务必查看https://llvm.org/docs/DeveloperPolicy.html [针对开发人员的LLVM策略]。 最好的策略是在一个表单中创建一个示例代码,允许您使用LLVM`opt`工具单独研究它和您感兴趣的段落。
-
创建您需要的Julia代码的示例。
-
使用参数’JULIA_LLVM_ARGS=-print-after-all’获取IR转储。
-
在执行您感兴趣的通行证之前立即选择位置中的IR。
-
删除调试元数据并手动修复TBAA元数据。
后者将需要努力。 如果您能提出更方便的方法,我们将不胜感激。
jlcall呼叫协议
Julia对非优化代码有一个通用的调用约定,看起来像这样:
jl_value_t *any_unoptimized_call(jl_value_t *, jl_value_t **, int);
这里,第一个参数是一个打包的函数对象,第二个是放置在堆栈上的参数数组,第三个是参数的数量。 现在我们可以直接执行降级,并为参数数组调用alloca函数。 但是,这将违反在调用位置使用SSA的原则,并且会使优化(包括放置垃圾收集根)显着复杂化。 相反,我们将调用它如下:
call %jl_value_t *@julia.call(jl_value_t *(*)(...) @any_unoptimized_call, %jl_value_t *%arg1, %jl_value_t *%arg2)
这允许您遵循在优化器的所有操作中使用SSA的原则。 通过放置垃圾回收根,此调用稍后将降级为原始ABI C。
放置垃圾收集根
垃圾收集根作为传递管道中后期LLVM传递之一的一部分放置。 通过将垃圾收集根作为后期传递的一部分,LLVM可以对需要垃圾收集根的代码执行更积极的优化,并且还可以减少所需的垃圾收集根和垃圾收集根保存操作的数量(因为LLVM平台不支持我们的垃圾收集器,否则将禁止使用存储在垃圾收集框架中的值,因此,出于安全原因,它的操作将受到限制)。 例如,考虑错误调用路径:
if some_condition()
#= Возможно, здесь используются какие-либо переменные =#
error("An error occurred")
end
在常量崩溃期间,LLVM可以检测到条件始终为false并删除基块。 但是,如果垃圾收集根提前降低,则远程块中使用的垃圾收集根槽以及由于在错误路径中使用而存储在这些槽中的任何值都将由LLVM平台保存。 随着垃圾收集根的后期降级,我们允许LLVM执行通常的优化(常量卷积,消除无用代码等)。),而不用担心(太多)哪些值可能会或可能不会被垃圾收集器跟踪。
但是,为了使垃圾收集根的后续放置成为可能,我们需要能够确定以下内容:a)垃圾收集器跟踪的指针;b)此类指针的所有用例。 所以放置垃圾回收根的目的很简单。:
最大限度地减少必要的垃圾收集根的数量并在其中保存操作,同时考虑到在每个安全点,垃圾收集器跟踪的任何活动指针(即在该点之后有一条路径)位于垃圾收集器的某个插槽中的限制。
工作表现
因此,主要的困难在于选择一个IR表示,即使在通过优化器运行程序之后,也可以识别垃圾收集器跟踪的指针及其用例。 为此,我们的方法涉及使用三个LLVM函数:
-
用户定义的地址空间;
-
操作数包;
-
非整数指针。
用户定义的地址空间允许我们用整数标记每个位置,该整数应该在优化过程中保存。 编译器不能在原始程序中不存在的地址空间之间添加转换,并且在加载,保存等过程中不应该更改指针的地址空间。 这允许您注释垃圾回收器跟踪的指针,以便它不会受到优化器的影响。 请注意,使用元数据实现相同的事情是不可能的。 假定可以在不改变程序含义的情况下删除任何元数据。 但是,无法识别垃圾收集器跟踪的指针从根本上改变了程序的行为-它可能会崩溃或返回不正确的结果。 我们目前正在使用三个不同的地址空间(它们的数字在文件`src/codegen_shared中定义。cpp`):
-
垃圾回收器跟踪的指针(当前为10个):这些是指向可放置在垃圾回收帧中的打包值的指针。 它们大约类似于c中的指针’jl_value_t*'。注意:这个地址空间中不应该有任何指针不能存储在垃圾收集器槽中。
-
派生指针(当前为11个):这些是从垃圾回收器跟踪的任何指针派生的指针。 使用此类指针需要使用原始指针。 但是,它们本身不一定必须为垃圾收集器所知。 垃圾回收根放置传递必须找到由派生此指针的垃圾回收器跟踪的指针,并使用它来创建根。
-
被叫方的根指针(目前为12个):这是一个辅助地址空间,用于表达被叫方根值的概念。 此地址空间中的所有值都必须能够存储在垃圾回收的根目录(尽管将来此条件可能会变得不那么严格),但与其他指针不同,它们在传递给调用时不必是根目录(但是,如果它们在定义之间的另一个安全点处于活动状态,它们仍然必须是根目录)。 和挑战)。
-
从受监视对象加载的指针(当前为13个):由本身包含指向托管数据的指针的数组使用。 此数据区属于数组,但它本身不是垃圾收集器监视的对象。 编译器保证只要此指针处于活动状态,从中加载它的对象将保持活动状态。
不变量
垃圾回收根放置传递使用几个必须由接口部分遵守并由优化器保留的不变量。
首先,只允许以下地址空间转换:
-
0->{Tracked,Derived,CalleeRooted} (跟踪,派生,被调用方的根):一个无法跟踪的指针可以退化为任何其他指针。 但是,请注意,优化器有权不将这样的值作为根值。 如果该值需要垃圾回收根(或从这样的值派生),则在程序的任何部分的地址空间中具有0值是不安全的。
-
跟踪(tracked)->派生(derived):这是内部值的标准简并路径。 放置通道查找此类值以确定任何用例的基本指针。
-
Tracked(跟踪)->CalleeRooted(被叫方的根):CalleeRooted地址空间只是表示不需要垃圾回收根。 但是,请注意,派生(跟踪)->CalleeRooted(被调用方的根)的退化是被禁止的,因为指针通常应该能够存储在垃圾收集槽中,即使在这个地址空间中也是如此。
现在让我们来看看什么适用于用例。:
-
用于加载位于其中一个地址空间中的值的操作;
-
用于保存位于特定位置的地址空间之一中的值的操作;
-
将操作保存在其中一个地址空间中的指针中;
-
其中一个地址空间中的操作数为值的调用;
-
对参数数组包含值的jlcall ABI的调用;
-
返回指示。
我们明确允许在跟踪和派生地址空间中加载和保存操作以及简单调用。 Jlcall参数数组的元素必须始终位于跟踪的地址空间中(根据ABI,它们必须是有效的`jl_value_t*`指针)。 对于返回指令也是如此(但是,请注意,结构形式的返回参数可以在任何地址空间中)。 在CalleeRooted地址空间中使用指针的唯一有效方法是将其传递给调用(该调用必须具有适当类型的操作数)。
此外,禁止在跟踪的地址空间中查找’getelementptr'。 原因是,如果操作不是空闲的,指针最终将无法存储在垃圾回收槽中,因此它将无法驻留在此地址空间中。 如果需要这样的指针,则必须首先将其带到派生地址空间。
最后,在这些地址空间中禁止`inttoptr`和`ptrtoint`指令。 有这样的指令意味着一些`i64’值实际上正在被垃圾收集器跟踪。 这会产生一个问题,因为它会违反能够定义与垃圾收集相关的指针的要求。 此固定条件由LLVM的"非整数指针"功能提供,该功能出现在LLVM5.0中。 它禁止优化器执行会导致此类操作的优化。 请注意:我们仍然可以在地址空间0中使用`inttoptr`在JIT期间引入静态常量,然后将它们强制转换到适当的地址空间。
支援服务 'ccall'
A = randn(1024)
ccall(:foo, Cvoid, (Ptr{Float64},), A)
降级时,它会将数组转换为指针,结果删除对数组值的引用。 但是,肯定有必要确保阵列在运行时保持活动状态。 'ccall'。 为了理解这是如何实现的,让我们看一下上述代码可能降级的假设示例。:
return $(Expr(:foreigncall, :(:foo), Cvoid, svec(Ptr{Float64}), 0, :(:ccall), Expr(:foreigncall, :(:jl_array_ptr), Ptr{Float64}, svec(Any), 0, :(:ccall), :(A)), :(A)))
最后一个元素`:(A)'是在降级期间添加的额外参数列表,并告诉代码生成器在降级期间Julia级别的哪些值应该保持活动状态。 'ccall'。 然后我们获取这些信息并将其作为IR级别的"操作数包"呈现。 操作数包本质上是一个虚构的用例,与调用位置相关联。 在IR级别,它看起来像这样:
call void inttoptr (i64 ... to void (double*)*)(double* %5) [ "jl_roots"(%jl_value_t addrspace(10)* %A) ]
在垃圾回收根放置过程中,操作数包’jl_roots’被视为常规操作数。 但是,在最后一步中,在添加垃圾收集根之后,操作数包被删除,以免混淆指令的选择。
支援服务 'pointer_from_objref'
一个特征 'pointer_from_objref'是用户必须显式控制垃圾收集根。 根据上述不变量,该函数是无效的,因为它执行从地址空间10到0的转换。 但是,在某些情况下它可能是有用的,因此我们提供了一个特殊的内部函数。:
declared %jl_value_t *julia.pointer_from_objref(%jl_value_t addrspace(10)*)
在降低垃圾收集根之后,它被降低到适当的地址空间减少。 但是,请注意,通过使用此内部函数,调用方承担确保值为根的全部责任。 此外,此内部函数不被视为用例,因此在垃圾收集根放置传递期间,不会为其提供垃圾收集根。 因此,有必要在系统监控值的同时提供根的外部控制。 换句话说,尝试使用此操作的结果来创建全局根是不可接受的-优化器可能已经删除了该值。
在没有用例的情况下保持值活动
在某些情况下,即使编译器不知道其用例,对象也必须保持活动状态。 对于直接与内存中对象的表示进行操作的低级代码,或者必须与C代码交互的代码,这可能是正确的。 为此,我们在LLVM级别提供以下内部函数:
token @llvm.julia.gc_preserve_begin(...) void @llvm.julia.gc_preserve_end(token)
('Llvm’元素。"是使用"令牌"类型所必需的。)这些内部函数具有以下含义:在由`gc_preserve_begin`调用控制但不受相应`gc_preserve_end`调用控制的任何安全点(即,其参数是`gc_preserve_begin`调用返回的令牌的调用),作为参数传递给 请记住,'gc_preserve_begin’仍然被认为是这些值的常见用例,因此标准生存期语义将确保值在进入保存区域之前处于活动状态。