JIT的开发和实施
导言
JIT管理编译资源,搜索以前编译的代码,并编译新代码。 它主要建立在技术上。 https://llvm.org/docs/ORCv2.html LLVM中的[compilations on demand](ORCv2),它实现了对许多有用功能的支持,例如并行编译,延迟编译以及在单独进程中编译代码的能力。 虽然LLVM以LLJIT的形式提供了基本的JIT编译器,但Julia直接使用许多ORCv2Api来创建自己的JIT编译器。
检讨
生成代码时,将从类型推断获得的原始SSA Julia IR(在上面的编译器图中标记为translate)中创建一个LLVM模块,其中包含一个或多个Julia函数的IR。 代码的实例也映射到LLVM函数名。 但是,即使基于Julia的编译器对Julia的IR应用了一些优化,在代码生成期间获得的LLVM IR表示仍然包含许多优化可能性。 因此,JIT的第一步是启动一个独立于目标的优化管道[这不是一个完全独立于目标的管道,因为像矢量化这样的转换依赖于关于目标的信息,如矢量寄存器的宽度和成本建模。 此外,代码生成本身做出了几个依赖于目标对象的假设,这些假设随后将在优化管道中使用。]在LLVM模块中。 然后,JIT运行依赖于目标的优化管道以进行优化和代码生成,并输出目标文件。 最后,JIT将生成的目标文件编译到当前进程中,并使代码可供执行。 所有这些操作都由文件`src/jitlayers中的代码控制。cpp'。
目前,一次只能有一个线程进入优化-编译-链接管道。 这是由于我们的一个链接器(RuntimeDyld)施加的限制。 但是,JIT旨在支持同时优化和编译,并且预计将来在所有平台上完全替换RuntimeDyld时,将解除链接器限制。
优化管道
优化管道基于新的LLVM pass管理器,但适应了Julia的需求。 管道在`src/pipeline中定义。cpp’和一般地,它经历几个阶段,这在下面详细描述。
-
早期阶段的简化
-
这些段落主要用于简化IR和规范模式,以便后续段落可以更容易地识别这些模式。 此外,诸如分支预测提示和注释的各种内部调用被降级到其他元数据或其他IR函数。 这里的关键作用是由https://llvm.org/docs/Passes.html#simplifycfg-simplify-the-cfg ['SimplifyCFG'](控制顺序图的简化),https://llvm.org/docs/Passes.html#dce-dead-code-elimination ['DCE'](不包括非工作代码)和https://llvm.org/docs/Passes.html#sroa-scalar-replacement-of-aggregates [`SROA'](聚集体的标量替换)。
-
-
前期优化
-
这些通行证通常很便宜,主要旨在减少IR中的指令数量并将知识传播到其他指令。 例如,https://en.wikipedia.org/wiki/Common_subexpression_elimination ['EarlyCSE']用于消除常见的子表达式,并且https://llvm.org/docs/Passes.html#instcombine-combine-redundant-instructions ['InstCombine']和https://llvm.org/doxygen/classllvm_1_1InstSimplifyPass.html#details ['InstSimplify']执行一些小的局部优化以降低操作成本。
-
-
循环优化
-
这些段落规范和简化了循环. 循环通常是热编码的,这使得优化循环对性能非常重要。 这里的关键作用是由https://llvm.org/docs/Passes.html#loop-rotate-rotate-loops ['LoopRotate'],https://llvm.org/docs/Passes.html#licm-loop-invariant-code-motion [`LICM']和https://llvm.org/docs/Passes.html#loop-unroll-unroll-loops ['LoopFullUnroll']。 作为通道的结果https://llvm.org/doxygen/InductiveRangeCheckElimination_8cpp_source.html [`IRCE']边界检查部分也正在被消除,这证实了从未超过某些边界。
-
-
标量优化
-
标量优化管道包含许多更昂贵但更强大的传递,例如https://llvm.org/docs/Passes.html#gvn-global-value-numbering [`GVN'](全局值的编号),https://llvm.org/docs/Passes.html#sccp-sparse-conditional-constant-propagation [`SCCP'](稀疏条件常数的传播)和边界检查消除的另一个循环。 这些传递很昂贵,但它们通常允许您删除大量代码并使矢量化更加成功和高效。 其他几个简化和优化过程与更昂贵的过程交替进行,以减少他们必须做的工作量。
-
-
矢量化
-
https://en.wikipedia.org/wiki/Automatic_vectorization [自动矢量化]是一个非常强大的转换CPU密集型代码。 简而言之,矢量化允许您执行https://en.wikipedia.org/wiki/Single_instruction …_multiple_data[single instruction with multiple data](SIMD)为例,同时执行8次加法运算。 然而,要证明代码支持矢量化并且在同一时间对矢量化有益是相当困难的,并且它在很大程度上取决于初步的优化传递来使IR达到矢量化是合理的状态。
-
-
降级内部功能
-
Julia为对象分配、垃圾回收和异常处理等目的插入了许多自定义的本机内部函数。 最初,这些内部函数是为了使优化可能性更加明显而放置的,但现在它们已降级为LLVM IR,以便IR可以作为机器代码输出。
-
-
清除。. 这些传递是最后机会优化,并执行小的优化,例如扩展组合乘法-加法和简化除法-求余数。 此外,对于不支持半精度浮点数的目标,半精度指令将降级为单精度指令,并且将添加传递以支持消毒程序。
布局
Julia目前正处于从旧的RuntimeDyld链接器到新链接器的过渡阶段。 https://llvm.org/docs/JITLink.html [JITLink]。 JITLink包含许多RuntimeDyld所不具备的功能,例如并行和可重用链接,但它目前对性能分析集成没有足够的支持,并且还不支持RuntimeDyld支持的所有平台。 预计随着时间的推移,JITLink将完全取代RuntimeDyld。 有关JITLink的更多信息,请参阅LLVM文档。
成就;成就
编译到当前进程中的代码可供执行。 生成代码实例通过相应更新字段"invoke","specsigflags"和"specptr"来了解这一点。 代码实例支持更新’invoke`,`specsigflags`和’specptr’字段,前提是它们的每个组合都可以在任何给定时间调用。 在这种情况下,JIT可以在不使现有代码实例无效的情况下更新这些字段,从而支持将来可能并行的JIT。 特别是,以下状态可能是有效的:
-
'invoke’具有NULL值,'specsigflags’具有0b00值,'specptr’具有NULL值
-
这是代码实例的初始状态,表示代码实例尚未编译。
-
-
'invoke’具有非零值,'specsigflags’具有值0b00,'specptr’具有值NULL
-
这表明代码实例尚未使用任何特化进行编译,应该直接调用它。 请注意,在这种情况下,'invoke’不会读取’specsigflags’字段或’specptr’字段,因此可以在不使`invoke`指针无效的情况下更改它们。
-
-
'invoke’具有非零值,'specsigflags’具有0b10的值,'specptr’具有非零值
-
这表明代码实例已被编译,但专用函数签名被认为对于代码生成是不必要的。
-
-
'invoke’具有非零值,'specsigflags’具有值0b11,'specptr’具有非零值
-
这表明代码实例已被编译,并且专用函数签名被识别为代码生成所必需的。 'Specptr’字段包含指向专用函数签名的指针。 'Invoke’指针可以读取’specsigflags`和’specptr’字段。
-
此外,在更新过程中发生几种不同的过渡状态。 要考虑这些可能的情况,在处理代码实例的这些字段时应使用以下写入和读取模式。
-
当写入’invoke`,`specsigflags`和’specptr’字段时:1。 假设旧值为NULL,对specptr字段执行原子比较交换操作。 此比较-交换操作必须至少具有捕获-释放顺序,以确保在写入时对剩余的内存操作进行排序。 2. 如果’specptr’字段具有非零值,则停止写操作并等待位0b10写入’specsigflags’字段。 3. 写入’specsigflags’字段的新低阶位作为其最终值。 这可能是弱化的写操作。 4. 为`invoke`写一个新指针作为其最终值。 要与"调用"读取操作同步,它必须至少具有内存释放顺序。 5. 将`specsigflags’的第二位设置为1。 与`specsigflags`读取同步至少需要内存释放顺序。 此步骤完成写操作,并通知所有其他线程已设置所有字段。
-
当读取所有字段’invoke'’specsigflags’和’specptr’时:
-
考虑’invoke’字段,至少与内存捕获顺序有关。 此操作将被指定为’initial_invoke'。
-
如果’initial_invoke’有一个值,代码实例仍然是可执行的。 'invoke’具有空值,'specsigflags’可以被认为具有0b00值,`specptr’可以被认为具有空值。
-
考虑’specptr’字段,至少与内存捕获顺序有关。
-
如果’specptr’为NULL,那么’initial_invoke’指针不应该依赖’specptr’来正确执行。 因此,'invoke’具有非零值,'specsigflags’可被视为具有值0b00,`specptr’可被视为具有值NULL。
-
如果’specptr’具有非零值,那么`initial_invoke`可能不是使用`specptr`的最终`invoke’字段。 如果`specptr’已被记录,但`invoke`尚未被记录,则可能发生这种情况。 因此,迭代通过’specsigflags’字段的第二个位,直到值1被设置,至少与捕获内存排序。
-
重新读取`invoke`字段,至少使用内存捕获顺序。 此操作将被指定为’final_invoke'。
-
使用任何内存顺序读取`specsigflags`字段。
-
'invoke’是’final_invoke`,`specsigflags`是在步骤7中读取的值,`specptr’是在步骤3中读取的值。
-
-
当将`specptr’更新为不同但相似的函数指针时:
-
释放’specptr’中新函数指针的内存。 这里应该是安全的,因为旧的函数指针应该仍然有效,就像任何新的一样。 写入`specptr’的指针必须始终可用于调用,无论它是否随后被复盖。
-
尽管很复杂,但这些写入、读取和更新步骤确保JIT可以更新代码实例而不会使现有实例无效,并且JIT可以更新代码实例而不会使现有的"invoke"指针无效。 这使得JIT能够在未来更高的优化级别重新优化函数,以及支持函数的并行编译。