compiler-construction - 设计选择的理由会导致JVM/CLR语言长时间启动?

标签 compiler-construction jvm clr language-design

我正在考虑设计一种编程语言,我希望它以与CPython或Perl相同的速度启动。为了用我的语言做出正确的设计选择来满足这一要求,我正在研究现有的动态语言,以了解它们的设计选择如何影响其启动时间。许多基于JVM或CLR的语言实现比CPython或Perl具有更长的启动时间。这表明在导致这种情况的JVM和/或CLR的设计中进行了设计选择。那是什么选择,为什么要那样做?

这是一个三部分的问题:

  • 慢速启动动态JVM / CLR语言实现是否是所有的基本设计问题,还是只是一个较小的问题,可以通过改进语言实现来解决?
  • 如果是设计问题,那么会导致JVM的哪些设计选择以及这些语言的哪些设计选择导致这些语言的启动延迟比CPython和Perl更长?
  • 通过缓慢启动获得了什么? 就是说,由于(2)所述的设计选择,JVM / CLR动态语言具有CPython和Perl所缺乏的哪些优势?

  • 注意,其他SO问题已经处理了“Why is the JVM slow to start?”以及为什么各种JVM语言启动缓慢。这个问题不同于那个问题,因为这个问题是关于设计权衡的。这么长的启动时间可以换来什么?

    其他的SO问题则询问用户如何加快各种JVM语言的速度(答案通常是使用某种可以预加载JVM的守护程序),但这不是我要问的。我在问您如何设计允许快速启动(无需预加载)的语言(和/或虚拟机),以及您为此付出了什么损失。

    背景研究

    各种语言实现的速度

    我在我的GNU / Linux机器上的非正式Hello World测试中对 CPython和Perl 进行了基准测试,发现它们的启动时间不到0.05秒。在本文的其余部分中,我将说“快速”是指“启动时间不会明显长于CPython或Perl的启动时间”,而“慢”则表示其他意思。

    很容易发现意见,即JVM本身和/或Java 的启动缓慢(3456),以及大约1秒或更长时间的具体数字(727)和基准测试(8)。但是,两个Hello World JVM基准测试仅在0.04秒(在GNU / Linux上)(910)上启动。

    Clojure 的启动时间约为0.6-1秒(12);这比我的目标0.05秒慢了约20倍。 ClojureCLR甚至更慢(1)。 Clojure启动时间基准和讨论可以在博客文章Clojure bootstrapping (Kariniemi)Why is Clojure slow (Trojer)中找到。

    一位启动时间基准测试人员表示,Clojure和 JRuby “比其他所有程序都慢得多”(25);这些也是经过测试的仅有的两种基于JVM的动态语言。另一个(很旧的)基准测试表明 Jython 的启动也很慢(26)。在这个问题中,我们专注于动态语言,但是 Scala 也不是那么快(1)可能与之有关。有一个名为的JVM方案Kawa (23)。据报道,Kawa的启动时间约为0.4(24),虽然比Clojure快,但仍然比我的目标高出一个数量级。

    在启动过程中执行什么操作?

    两者(12)都得出结论,Clojure花费了启动时间来加载类并初始化clojure.core命名空间。 SO问题“Clojure应用程序启动性能”的一个answer似乎在说Java启动时间和Clojure启动时间之间的区别是因为Java懒加载了其标准库,而Clojure却急切地加载了它。 SO问题“Can any Clojure implementation start fast?”的答案包括“这只是可以纠正的实现问题,而不是基本的设计选择”(paraphrased)和"One limitation of the JVM is that objects must be copied on initialization, "You can't embed any composite constants in byte code. Not even arrays.""

    一个blog post指出ClojureCLR的启动时间主要花费在JIT上,而JIT之前的启动时间大大缩短了时间(尽管与CPython和Perl相比,它可能仍然很慢)。

    关于为什么某些JVM或Java程序启动缓慢的一种解释是从标准库(11)
    中加载许多类文件的 I / O。基准测试支持了该假设,该基准测试表明JVM启动时间大大缩短了“热启动”时间,在此期间,标准库类文件的内容可能已经加载到操作系统的高速缓存中。有人说,很多启动时间是由于在类文件中读取I / O所致,而不是由于数据量庞大,而是由于该数据在磁盘上的组织不理想(1516)。

    JVM的字节码验证程序可能不是启动时间的重要因素,因为验证程序的40%加速仅转换为大型程序启动时间(14)的5%。

    哪些设计选择(不会)导致启动缓慢?

    在(22)中,Kariniemi得出的结论是,由于包含动态功能的设计选择,Clojure启动固有地启动缓慢。但是,我对这个结论提出了质疑,因为CPython和Perl可以实现更快的启动速度,同时仍然提供了动态性。

    不能使用字节码,因为CPython也使用字节码。

    由于加载类文件的I / O似乎有问题,因此人们可能会怀疑底层的设计选择是提供大型标准库。但是,这不是原因,因为CPython还提供了一个大型标准库,并且启动速度并不慢。另外,尽管Java的运行速度存在争议,但值得注意的是Java必须在启动时加载rt.jar,但根据某些基准,Hello World在Java中的运行速度很快。

    最佳答案

    启动时间取决于运行时实际开始执行任何“用户代码”之前所需的工作量。让我比较一些选择的确切结果。

    本机二进制(C++左右)

    操作系统maps将主要的可执行文件存入内存。即使此文件非常大(几个GB),映射仍然非常快。对于10-50MB的典型文件大小,它非常快。然后读取一些可执行 header ,该 header 提供列表动态模块。这些模块由OS搜索并以相同方式映射。然后,可能会发生一些搬迁。此后,您的代码就可以执行了(尽管此时控制权可能交给了您的语言运行时,而不是代码本身)。

    脚本语言

    解释器可执行文件在上一节中进行了介绍之后,它将开始读取和执行提供的脚本。假设没有解析/编译为字节码(我们已经拥有.pyc或类似字节码格式的所有内容)。每次需要加载模块解释器时,它只会分配足够长的内存块并将模块内容复制到其中。然后,它将控制权转移到该字节代码块。在此阶段确实需要完成一些工作,但通常不是很多。例如,thats是将在dis上执行的python import dis模块的bytecode

    进入JVM

    对于JVM,并不是那么容易。首先,运行时不能仅将.class文件映射到内存,也不能将其内容读入内存并告诉解释器:“嘿!这是您的字节码”。它需要进行验证和解决。

    验证的目的是确保解释器可以执行而无需任何进一步的运行时检查(功能异常分支,堆栈上溢或下溢,类型检查)。即使我们假设O(指令数)验证的时限,它仍然很大,因为必须检查模块中的每条指令。请记住,对于脚本语言,我们需要进行少量的加载工作,通常只是使用新功能和类填充“导出”字典。

    解决是一种优化(和语言设计的选择)。考虑一下Java代码:

    System.out.println("Hello, world!");
    

    对于此代码,Java编译器将有关println的信息放入.class文件中:println是静态方法,具有来自(ILJAVA/LANG/STRING;)V类的签名java.lang.System
    加载包含上述行的类时,JVM必须寻找java.lang.System(可能也在进程中加载​​它),找到具有此签名的方法println,并将指向该方法的指针放在某个地方,以便稍后在此行发生时可以找到它。执行。

    必须对每个已加载类中的每个唯一方法调用执行此过程。每个引用的类,字段,接口(interface)等都相同。因此,加载大的.class文件与“将其内容复制到内存”和“执行一些环境调整”无关。

    有了足够大的标准库,仅这些操作就可能导致较长的启动时间。

    编译(至少优化编译)很慢。记住编译一个像样的C++项目需要花费多长时间。因此,各种技巧使该操作更快。在JVM中(至少在某些实现中),解释和JIT编译可以并行执行,因此默认情况下解释代码,如果确定“热”(经常执行),则对JIT进行编辑。但是解释也很慢。因此,这不是灵丹妙药,只是在“慢做”或“根本不做,希望JIT尽快完成工作”之间进行权衡。没有jit支持的Python只是“做得慢”。但是标准库的某些性能关键部分(如字典)是用C(或Java或C#,而不是Python本身)编写的。 Java标准库是用Java编写的。这就是为什么它也必须通过JIT编译,否则运行会很慢的原因。

    概要
  • 这些缓慢的启动时间是一个设计问题。那是价格
    几乎与C一样快,并且同时具有很高的动态性。
  • 导致此速度下降的设计选择是:字节码验证
    加载时间而不是执行时间,加载时间链接和JIT
    汇编。
  • 正如我所说,这允许JVM使用JIT生成代码,这是
    与使用本地语言的代码几乎一样快(在某些测试中甚至更快)。

  • 结论

    如果希望缩短启动时间,请以无需运行时加载模块的大量工作来设计语言。充其量,仅需复制+环境更新即可完成更多工作。

    JIT开销可以通过JIT缓存或Ahead-of-Time compilation来克服。如果您的语言是完全动态的,则不是这种情况(例如,您可以在某些模块中覆盖'array.length'属性,并且标准库也必须遵守此更改)。

    关于compiler-construction - 设计选择的理由会导致JVM/CLR语言长时间启动?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28118218/

    相关文章:

    java - 编译器 + 类间引用 : how does javac do quickly what C++ compilers do slowly?

    java - JVM启动时如何在Java中运行初始化方法?

    .net - MethodImplOptions.InternalCall 的意义何在?

    math - 在计算机科学中,什么不是正式语言?

    c++ - 为什么VC++ 2010编译器在编译简单代码时会崩溃?

    java - 我能否保证我的 Java 应用程序将使用具有限制性策略的安全管理器执行?

    JAVA 8 : how to pass -xms and -xmx JVM values in the command

    c# - 一个字符串在 x64 中占用多少字节?

    .net - 当 CLR 在 .net 中调用垃圾收集器时?

    c - 我如何在 (GNU) C 中编写一个代理函数来连接两个不同的调用约定?