3.构建JIT:按函数惰性编译
- 懒惰编译
当我们从第2章向KaleidoscopeJIT类添加一个模块时,它会立即由IRTransformLayer,IRCompileLayer和RTDyldObjectLinkingLayer为我们进行优化,编译和链接。这个方案使得模块可执行文件的所有工作都是预先完成的,很容易理解,其性能特征很容易理解。但是,如果要编译的代码量很大,它将导致非常高的启动时间,如果在运行时只调用少量编译函数,也可能会进行大量不必要的编译。真正的“即时”编译器应该允许我们推迟任何给定函数的编译,直到首次调用该函数为止,从而缩短启动时间并消除冗余工作。事实上,ORC API为我们提供了一个懒惰地编译LLVM IR的层: CompileOnDemandLayer。
CompileOnDemandLayer类符合第2章中描述的图层接口,但其addModule方法的行为与我们目前看到的图层完全不同:它不是预先做任何工作,而是扫描正在添加的模块并安排每个函数。它们在第一次被调用时被编译。为此,CompileOnDemandLayer为它扫描的每个函数创建两个小实用程序:存根和编译回调。存根是一对函数指针(一旦编译了函数将指向函数的实现)和间接跳过指针。通过在程序的生命周期内修复间接跳转的地址,我们可以为函数提供一个永久的“有效地址”,即使函数的实现从未编译,也可以安全地用于间接和函数指针比较。编译不止一次(例如,由于在更高的优化级别重新编译函数)并更改地址。第二个实用程序,即编译回调,表示从程序到编译器的重新进入点,它将触发编译然后执行函数。通过初始化函数的存根以指向函数的编译回调,我们启用延迟编译:第一次尝试调用函数将跟随函数指针并触发编译回调。编译回调将编译该函数,更新存根的函数指针,然后执行该函数。在对函数的所有后续调用中,函数指针将指向已编译的函数,因此编译器不会产生进一步的开销。我们将在本教程的下一章中更详细地介绍这个过程,但是现在我们将相信CompileOnDemandLayer为我们设置所有存根和回调。我们需要做的就是将CompileOnDemandLayer添加到堆栈的顶部,我们将获得延迟编译的好处。我们只需要对源进行一些更改:编译回调将编译该函数,更新存根的函数指针,然后执行该函数。在对函数的所有后续调用中,函数指针将指向已编译的函数,因此编译器不会产生进一步的开销。我们将在本教程的下一章中更详细地介绍这个过程,但是现在我们将相信CompileOnDemandLayer为我们设置所有存根和回调。我们需要做的就是将CompileOnDemandLayer添加到堆栈的顶部,我们将获得延迟编译的好处。我们只需要对源进行一些更改:编译回调将编译该函数,更新存根的函数指针,然后执行该函数。在对函数的所有后续调用中,函数指针将指向已编译的函数,因此编译器不会产生进一步的开销。我们将在本教程的下一章中更详细地介绍这个过程,但是现在我们将相信CompileOnDemandLayer为我们设置所有存根和回调。我们需要做的就是将CompileOnDemandLayer添加到堆栈的顶部,我们将获得延迟编译的好处。我们只需要对源进行一些更改:所以编译器没有进一步的开销。我们将在本教程的下一章中更详细地介绍这个过程,但是现在我们将相信CompileOnDemandLayer为我们设置所有存根和回调。我们需要做的就是将CompileOnDemandLayer添加到堆栈的顶部,我们将获得延迟编译的好处。我们只需要对源进行一些更改:所以编译器没有进一步的开销。我们将在本教程的下一章中更详细地介绍这个过程,但是现在我们将相信CompileOnDemandLayer为我们设置所有存根和回调。我们需要做的就是将CompileOnDemandLayer添加到堆栈的顶部,我们将获得延迟编译的好处。我们只需要对源进行一些更改:
最后,我们需要在addModule,findSymbol和removeModule方法中替换对OptimizeLayer的引用。有了它,我们就开始运转了。
这是代码:toy.cpp