使用LLVM
这不是LLVM文档的替代品,而是为Julia处理LLVM的提示集合。
Julia到LLVM接口概述
Julia默认动态链接到LLVM。 用 USE_LLVM_SHLIB=0 以静态链接。
将Julia AST降低到LLVM IR或直接解释它的代码在目录中 src公司/.
| 档案 | 资料描述 |
|---|---|
|
编译器C-接口入口和目标文件发射 |
|
内建函数 |
|
降低;降低 |
|
降低实用程序,特别是数组和元组访问 |
|
顶级代码生成,传递列表,降低内建 |
|
跟踪JIT代码的调试信息 |
|
处理本机对象文件和JIT代码反汇编 |
|
泛型函数 |
|
降低内部函数 |
|
特定于JIT的代码,ORC编译层/实用程序 |
|
Julia特有的逃逸分析 |
|
将堆分配降级到堆栈的自定义LLVM传递 |
|
自定义LLVM传递到较低的基于CPU的函数(例如haveFMA) |
|
自定义LLVM传递到较低的16b浮点运算到32b浮点运算 |
|
自定义LLVM传递到较低的GC调用它们的最终形式 |
|
自定义LLVM传递以验证Julia GC不变量 |
|
自定义LLVM传递到提升/接收器Julia特定的内部函数 |
|
自定义LLVM传递到根GC跟踪值 |
|
自定义LLVM传递到较低的try-catch块 |
|
在多个体系结构上生成sysimg代码的自定义LLVM通行证 |
|
自定义LLVM传递到规范化addrspaces |
|
自定义LLVM传递到较低的TLS操作 |
|
自定义LLVM传递以删除Julia addrspaces |
|
自定义LLVM传递以删除Julia非积分addrspaces |
|
自定义LLVM通行证 |
|
新建pass manager pipeline,pass pipeline解析 |
|
I/O和操作系统实用程序功能 |
一些 .cpp 文件形成编译为单个对象的组。
Intrinsic和builtin之间的区别在于builtin是第一类函数,可以像任何其他Julia函数一样使用。 内部只能对未装箱的数据进行操作,因此其参数必须是静态类型的。
使用不同版本的LLVM构建Julia
LLVM的默认版本在 deps/llvm。版本. 您可以通过创建一个名为 做吧。用户 在顶级目录中,并在其中添加一行,例如:
LLVM_VER = 13.0.0
除了LLVM发行版本,您还可以使用 DEPS_GIT=llvm 与 USE_BINARYBUILDER_LLVM=0 以针对LLVM的最新开发版本进行构建。
您还可以指定构建LLVM的调试版本,方法是设置 LLVM_DEBUG=1 或 LLVM_DEBUG=释放 在你的 做吧。用户 档案。 前者将是LLVM的完全未优化构建,后者将产生LLVM的优化构建。 根据您的需要,后者就足够了,而且速度更快。 如果您使用 LLVM_DEBUG=释放 您还需要设置 LLVM_ASSERTIONS=1 为不同的通行证启用诊断。 只有 LLVM_DEBUG=1 默认情况下暗示该选项。
将选项传递给LLVM
您可以通过环境变量将选项传递给LLVM JULIA_LLVM_ARGS. 以下是使用的示例设置 巴什 语法:
* 导出JULIA_LLVM_ARGS=-print-after-all 每次通过后转储IR。
* 导出JULIA_LLVM_ARGS=-debug-only=loop-vectorize 转储LLVM 调试(。..) 环路矢量化器的诊断。 如果您收到有关"未知命令行参数"的警告,请使用 LLVM_ASSERTIONS=1.
* 导出JULIA_LLVM_ARGS=-帮助 显示可用选项的列表。 导出JULIA_LLVM_ARGS=-help-hidden 显示更多。
* export JULIA_LLVM_ARGS="-fatal-warnings-print-options" 是如何使用多个选项的示例。
有用的 JULIA_LLVM_ARGS 参数
* -打印后=通过:在执行 通行证,用于检查通过传递所做的更改。
* -打印-之前=通过:在执行 通行证,用于检查输入传递。
* -打印-更改:每当一个通行证改变IR时,打印IR,这对于缩小哪些通行证会导致问题很有用。
* -打印-(前|后)=标记-通过:Julia管道在管道中附带许多标记通道,可用于识别问题或优化发生的位置。 标记传递被定义为在管道中出现一次并且对IR不执行转换的传递,并且仅对定位打印之前/打印之后有用。 目前,管道中存在以下标记传递:
**优化前
**简化前
**事后简化
**优化前
**后优化
**优化前
**之前
**AfterLICM
**简化前
**后简化
**后优化
**优化前
**AfterScalarOptimization
**实施前
**AfterVectorization
**在购买前
**后处理
**清理前
**清理后
**优化后
* -时间流逝:打印每个通行证所花费的时间,用于识别哪些通行证需要很长时间。
* -打印-模块-范围:配合使用 -打印-(前/后),获取整个模块而不是通过传递接收的IR单元
* -调试:在整个LLVM中打印出大量的调试信息
* -仅调试=名称,从文件中打印出调试语句 DEBUG_TYPE 定义为 姓名,用于获取有关问题的其他上下文
单独调试LLVM转换
有时,将LLVM的转换与Julia系统的其他部分隔离开来调试会很有用,例如,因为在里面重现了问题 朱莉娅 会花费太长时间,或者因为想要利用LLVM的工具(例如bugpoint)。
首先,您可以通过以下方式安装开发人员工具来使用LLVM:
make -C deps install-llvm-tools
要获得整个系统图像的未优化IR,请通过 --输出-unopt-bc unopt。公元前 系统映像构建过程的选项,该过程将未优化的IR输出到 unopt。公元前 档案。 然后可以像往常一样将此文件传递给LLVM工具。 [医]利朱利亚 可以作为一个LLVM传递插件,可以加载到LLVM工具中,使julia特定的传递在这个环境中可用。 此外,它暴露了 -朱莉娅 meta-pass,它在IR上运行整个Julia pass-pipeline。 例如,要使用旧的通行证管理器生成系统映像,可以这样做:
llc -o sys.o opt.bc cc -shared -o sys.so sys.o
要使用新的通行证管理器生成系统映像,可以这样做:
./usr/tools/opt -load-pass-plugin=libjulia-codegen.so --passes='julia' -o opt.bc unopt.bc ./usr/tools/llc -o sys.o opt.bc ./usr/tools/cc -shared -o sys.so sys.o
然后可以通过以下方式加载此系统映像 朱莉娅 像往常一样。
也可以只为一个Julia函数转储LLVM IR模块,使用:
fun, T = +, Tuple{Int,Int} # Substitute your function of interest here
optimize = false
open("plus.ll", "w") do file
code_llvm(file, fun, T; raw=true, dump_module=true, optimize)
end
这些文件可以像上面显示的未优化的sysimg IR一样处理,或者如果您想自己查看LLVM IR并获得额外的验证运行,则可以使用
./usr/tools/opt -load-pass-plugin=libjulia-codegen.so --passes='julia' -S -verify-each plus.ll
(注意在MacOS上这将是 libjulia-codegen。迪利布 和Windows上 libjulia-codegen。dll)
运行LLVM测试套件
要在本地运行llvm测试,您需要首先安装工具,构建julia,然后才能运行测试:
make -C deps install-llvm-tools make -j julia-src-release make -C test/llvmpasses
如果您想直接运行单个测试文件,通过每个测试文件顶部的命令,这里的第一步将把工具安装到 ./usr/工具/选择. 然后你会想要手动替换 %s 与测试文件的名称。
改进Julia的LLVM优化
改进LLVM代码生成通常涉及改变Julia降低以对LLVM的传递更友好,或者改进传递。
如果您计划提高通行证,请务必阅读https://llvm.org/docs/DeveloperPolicy.html[LLVM开发者政策]。 最好的策略是在一个表单中创建一个代码示例,您可以在其中使用LLVM的 选择 工具来研究它和孤立的兴趣传递。
-
创建一个感兴趣的Julia代码示例。
-
使用方法
JULIA_LLVM_ARGS=-打印-毕竟转储IR。 -
在感兴趣的传递运行之前的点处挑选IR。
-
剥离调试元数据并手动修复TBAA元数据。
最后一步是劳动密集型。 建议更好的方式将不胜感激。
Jlcall调用约定
Julia对未优化的代码有一个通用的调用约定,看起来如下所示:
jl_value_t *any_unoptimized_call(jl_value_t *, jl_value_t **, int);
其中第一个参数是盒装函数对象,第二个参数是参数的堆栈数组,第三个参数是参数的数量。 现在,我们可以执行一个简单的降低,并为参数数组发出一个alloca。 但是,这将背叛调用站点使用的SSA性质,使优化(包括GC根放置)变得更加困难。 相反,我们发出它如下:
call %jl_value_t *@julia.call(jl_value_t *(*)(...) @any_unoptimized_call, %jl_value_t *%arg1, %jl_value_t *%arg2)
这使我们能够在整个优化器中保留使用的SSA-ness。 GC根放置稍后会将此调用降低到原始C ABI。
GC根放置
GC根放置由传递管道后期的LLVM传递完成。 这样晚做GC根放置使LLVM能够对需要GC根的代码进行更积极的优化,并允许我们减少所需的GC根和GC根存储操作的数量(因为LLVM不理解我们的GC,否则它不会知道它是什么,也不允许对存储到GC帧的值做,所以它保守地做得很少)。 例如,考虑一个错误路径
if some_condition()
#= Use some variables maybe =#
error("An error occurred")
end
在恒定折叠期间,LLVM可能会发现条件总是假,并且可以移除基本块。 但是,如果早期进行GC根降低,则已删除块中使用的GC根槽以及仅因为在错误路径中使用而在这些槽中保持活动的任何值将由LLVM保持活动。 通过晚做GC根降低,我们给LLVM做任何通常的优化(常量折叠,死代码消除等)的许可。),而不必担心(太多)哪些值可能会或可能不会被GC跟踪。
但是,为了能够进行后期GC根放置,我们需要能够识别A)GC跟踪哪些指针以及b)此类指针的所有用途。 GC放置通行证的目标因此很简单:
最大限度地减少所需的GC根/存储的数量,这取决于约束,即在每个安全点,任何实时GC跟踪的指针(即在该点之后有一条包含使用此指针的路径)都在某个GC槽中。
代表权
因此,主要的困难是选择一个IR表示,使我们能够识别GC跟踪的指针及其用途,即使在程序通过优化器运行之后也是如此。 我们的设计利用了三个LLVM特性来实现这一目标:
*自定义地址空间 *操作数束 *非积分指针
自定义地址空间允许我们使用需要通过优化保留的整数来标记每个点。 编译器不得在原始程序中不存在的地址空间之间插入强制转换,并且绝不能在load/store/etc操作中更改指针的地址空间。 这允许我们以优化器抵抗的方式注释哪些指针被GC跟踪。 请注意,元数据将无法实现相同的目的。 元数据应该在不改变程序语义的情况下始终是可丢弃的。 但是,未能识别GC跟踪的指针会显着改变生成的程序行为-它可能会崩溃或返回错误的结果。 我们目前使用三种不同的地址空间(它们的数字定义在 src/codegen_shared。cpp):
*GC跟踪指针(目前为10个):这些是指向可能放入GC帧的盒装值的指针。 它大致相当于一个 jl_value_t* c端的指针。 注意:在这个地址空间中有一个指针可能不会存储到GC槽是违法的。
*派生指针(目前为11个):这些是从一些GC跟踪指针派生的指针。 这些指针的使用会生成原始指针的使用。 但是,GC不必知道它们本身。 GC根放置传递必须始终找到从中派生此指针的GC跟踪指针,并将其用作指向根的指针。
*被调用者根指针(目前为12个):这是一个实用程序地址空间,用于表达被调用者根值的概念。 这个地址空间的所有值都必须存储到GC根目录(尽管将来可以放松这个条件),但是与其他指针不同的是,如果传递给一个调用,它们不需要根目录(如果它们在定义和调用之间的另一个安全点上存在,它们仍然需要根目录)。
*从跟踪对象加载的指针(当前为13):数组使用此指针,数组本身包含指向托管数据的指针。 此数据区域由数组拥有,但本身不是GC跟踪的对象。 编译器保证,只要这个指针是活的,这个指针从加载的对象就会保持活的。
不变量
GC根放置传递使用了几个不变量,这些不变量需要由前端观察并由优化器保留。
首先,只允许以下地址空间转换:
* 0->{Tracked,Derived,CalleeRooted}:允许将未跟踪的指针衰减到任何其他指针。 但是,请注意,优化器具有不根这样的值的广泛许可。 如果程序的任何部分地址空间0中的值是(或派生自)需要GC根的值,则永远不会安全。 *跟踪->派生:这是内部值的标准衰减路径。 放置通道将查找这些以标识任何用途的基本指针。 *Tracked->CalleeRooted:Addrspace CalleeRooted只是作为不需要GC根的提示。 但是,请注意,派生的->CalleeRooted decay是禁止的,因为指针通常应该存储到GC槽中,即使在这个地址空间中也是如此。
现在让我们考虑什么构成使用:
*加载的值在其中一个地址空间中 *将值存储在其中一个位置的地址空间中 *存储到一个地址空间中的指针 *其中一个地址空间中的值是操作数的调用 *在jlcall ABI中调用,参数数组包含一个值 *返回说明。
我们明确允许在跟踪/派生的地址空间中加载/存储和简单调用。 Jlcall参数数组的元素必须始终位于跟踪的地址空间中(ABI要求它们是有效的 jl_value_t* 指针)。 返回指令也是如此(但请注意,允许struct返回参数具有任何地址空间)。 地址空间CalleeRooted指针的唯一允许使用是将其传递给调用(该调用必须具有适当类型的操作数)。
此外,我们不允许 getelementptr 在addrspace跟踪。 这是因为除非操作是noop,否则生成的指针将无法有效地存储到GC插槽,因此可能不在该地址空间中。 如果需要这样的指针,应该先将其衰减到addrspace派生。
最后,我们不允许 inttoptr/ptrtoint的 这些地址空间中的指令。 有这些指示将意味着一些 i64 值是真正的GC跟踪。 这是有问题的,因为它打破了我们能够识别GC相关指针的规定。 此不变性是使用LLVM"非积分指针"功能完成的,该功能在LLVM5.0中是新的。 它禁止优化器进行会引入这些操作的优化。 注意我们仍然可以在JIT时插入静态常量 inttoptr 在地址空间0,然后衰减到适当的地址空间之后。
支援服务 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)))
最后一个 :(一),是在降低期间插入的额外参数列表,它通知代码生成器在此期间需要保持哪些Julia级别值处于活动状态 ccall. 然后,我们获取这些信息并将其表示在IR级别的"操作数束"中。 操作数束本质上是附加到调用站点的假用法。 在IR级别,这看起来像这样:
call void inttoptr (i64 ... to void (double*)*)(double* %5) [ "jl_roots"(%jl_value_t addrspace(10)* %A) ]
GC根放置通行证将处理 [医]根 操作数捆绑,就好像它是一个常规操作数。 但是,作为最后一步,在插入GC根之后,它将删除操作数束,以避免混淆指令选择。
支援服务 pointer_from_objref
pointer_from_objref是特殊的,因为它要求用户对GC生根进行显式控制。 通过我们上面的不变量,这个函数是非法的,因为它执行从10到0的地址空间转换。 然而,它可以是有用的,在某些情况下,所以我们提供了一个特殊的内在:
declared %jl_value_t *julia.pointer_from_objref(%jl_value_t addrspace(10)*)
其降到GC降根后cast对应的地址空间。 但是请注意,通过使用此intrinsic,调用方承担确保有问题的值是根的所有责任。 此外,这个内在不被认为是一个用途,所以GC根放置传递不会为函数提供GC根。 因此,必须在系统仍跟踪值的同时安排外部生根。 即尝试使用此操作的结果来建立全局根是无效的-优化器可能已经删除了该值。
在没有使用的情况下保持价值的活力
在某些情况下,有必要保持一个对象的活动,即使没有编译器可见的使用所述对象。 对于直接对对象的内存表示进行操作的低级代码或需要与C代码接口的代码,可能会出现这种情况。 为了允许这样做,我们在LLVM级别提供以下内部函数:
token @llvm.julia.gc_preserve_begin(...) void @llvm.julia.gc_preserve_end(token)
(该 llvm。 在名称是必需的,以便能够使用 令牌 型)。 这些内部函数的语义如下:在任何由a支配的安全点 gc_preserve_begin 呼叫,但这不是由一个相应的主导 gc_preserve_end 调用(即一个调用,其参数是由 gc_preserve_begin 调用),作为参数传递给它的值 gc_preserve_begin 将被活下来。 请注意, gc_preserve_begin 仍然算作这些值的常规使用,因此标准生存期语义将确保这些值在进入preserve区域之前保持活动状态。