A Brief History of Just-In-Time 简读

A Brief History of Just-In-Time

最近一直在研究 Query compliation,作为顺带,也应该对 JIT 的发展有一个梳理,因此在网上搜罗一翻,找到了这篇 2000 左右的文章,这篇文章对 1960—2000 的 JIT发展历史有比较详尽的描述,值得一读。

http://eecs.ucf.edu/~dcm/Teaching/COT4810-Spring2011/Literature/JustInTimeCompilation.pdf

从1960年代起,软件系统一直在使用“即时”编译(JIT)技术。广义上说,JIT 编译包括程序开始执行后动态执行的任何转换。我们研究了JIT 编译背后的动机和强加在 JIT 编译系统上的约束,并为这类系统提出了一个分类方案。这个分类是在我们回顾四十年(1960 - 2000)JIT工作时出现的。

Introduction

想法总是被产生,被探索,被搁置——在数年后被重新发明。JIT 或者说动态编译技术也是一样,都是对在程序开始执行后的动态转换技术的概括。严格的说,JIT,动态编译技术是完全不必要的,他们只是用来提升程序执行的时间和空间效率的一种方法。毕竟,JIT系统解决的核心问题是一个已经解决的问题:将编程语言翻译成能在指定平台上运行的程序。

翻译是什么?需要翻译成可执行形式的编程语言的范围和性质涵盖了广泛的范围。包括 Ada、C 和 Java 等传统偏程语言,也包括正则表达式等小语言。

传统上,翻译有两种方法:编译解释。编译将―种语言翻译成另一种语言 —— 例如,汇编语言——这意味着翻译后的形式可能是在进一步的编译阶段之后,将更适合以后的执行。解释消除了这些中间步骤,执行与编译相同的分析,但立即执行。

JIT 编译用于同时获得(静态)编译和解释的好处。这些好处会在后面的章节中提到,所以我们只在这里总结一下:

  • 编译后的程序运行得更快,尤其是编译成在底层硬件上可以直按执行的形式时。静态编译还可以投入大量的时间来进行程序分析和优化。这就引出了JIT系统的主要约束:速度。JIT系统不能因为它的操作而导致正常程序执行中出现麻烦的停顿。
  • 被解释的程序通常更小,这仅仅是因为中间的表现形式比机器代码形式层次更高﹐并且可以隐含地携带更多的语义信息。
  • 被解释的程序往往具有更好的可移植性。假设有一个机器无关的表示形式。比如高级源代码或虚拟机代码,只需要提供解释器就可以在不同的机器上运行程序。(当然,程序仍然可能在做不可移植的操作,但那是另一回事了,因为这些问题会有解释器代码代为完成)。
  • 解释器可以访问运行时信息,例如输入参微、控制流和目标机器的具体信息。这些信息可能在不同的运行时发生变化,或者在运行时之前不可获取。此外,在程序运行之前收集有关它的某些类型的信息可能涉及使用静态分析无法确定的算法。

为了缩小我们的研究范围,我们只研究基于软件的 JIT 系统,它们具有重要的翻译方面。Keppel等人富有想象力地为运行时代码生成的更一般情况建立了一个论点,虽然再后来这个论点并不继续成立。

请注意,我们在广义上使用了执行这个术语 —— 如果一个程序表示可以被JIT系统以任何方式执行(无论是机器码的形式或者解释器的形式),我们称之为可执行的程序。

JIT COMPILATION TECHNIQUES

JIT 编译技术的工作通常集中于特定编程语言的实现。我们在本节中速循了同样的划分,尽可能从最早到最迟排序。

Genesis

自计算机诞生之初,自修改代码就已经存在,但我们将其排除在外,因为通常不涉及编译或翻译方面。相反,我们怀疑最早发表的关于JIT编译的工作是麦卡锡 [1960] 的LISP论文。他提到将函数编译成机器语言,这个过程足够快,以至于编译器的输出不需要保存。这可以看作是让程序和数据共享相同表示法的必然结果[McCarthy 1981]。另一个早期发表的关于 JIT 编译的文献可以追溯到1966年。IBM 7090的密歇根大学执行系统明确指出,可以使用汇编器 [University of Michigan 1966b, p. 1]和装载器 [University of Michigan 1966a, p. 6] 在执行期间进行翻译和加载。(手册的序言说,大部分章节都是在1965年8月之前写的,所以这可能要追溯到更早的时候。)
Thompson [1968] 的论文发表在 ACMcommun ications of the ACM 上,在现代出版板物中经常被引用为“早期工作”。他以一种特殊的方式将正则表达式编译到IBM7094代码中,然后执行这些代码来进行匹配。

LC 2 ^2 2

会话计算语言:简称 LC 2 ^2 2,是为交互式编程而设计的 [Mitchell et al. 1968],虽然在卡耐基梅隆大学被短暂用于教学,LC 2 ^2 2是主要是一种实验语言 [Mitchell 2000]。如果不是因为 Mitchell 在其实现中使用的技术 [Mitchell 1970],这些技术后来影响了 smalltalk 和 self 的 JIT 系统,否则这门语言可能会被扔进历史的垃圾箱。
Mitchell 观察到,编译后的代码可以在运行时从解释器派生出来,只需存储在解释过程中执行的操作。然而,这只适用于己经执行的代码,他举了一个 if-then-else 语句的例子,其中只有 else 部分被执行。为了处理这种情况,代码生成为未执行的部分,该部分在执行时重新调用解释器(上面示例中的then部分).

APL

关于高效实现APL 的开创性工作是 Abrams 的论文 [Abrams 1970] 。Abrams 炮制了两个关键的 APL 优化策略。他用内涵术语 drag-alongbeat 来描述。Drag-along 尽可能长时间地推迟表达评估,并收集上下文信息,希望更有效的评估方法可能出现:现在这被称为惰性求值。beat 是代码的转换,以减少表达式求值过程中涉及的数据操作量。Drag-along 和 beating 与JIT编译有关,因为APL是一种非常动态的语言;数据对象的类型和属性,要到运行时才知道。为了充分实现这些优化的潜力,它们的应用程序必须推迟到运行时信息可用时。Abrams 的“APL机器”采用了两个单独的JIT编译器。第一个翻译得到后缀代码的APL程序被应用于能够缓存延迟指令的 D-machine。

  1. D-machine:大概D代表延期(defer)或拖(Drag-Along)
  2. 最终,利顿工业公司(施罗德和沃恩的雇主)没有制造出这台机器[Mauriello 2000].

D-machine 充当了一个“代数简化编译器” [Abrams 1970,p.84],它会在运行时执行 Drag-along 和 beating。在必要时调用 E-machine来执行缓冲指令。

Abrams 的工作方向是一种高效支持 APL 的架构,硬件支持高级语言是当时的流行追求。然而,Abrams 始终没有制造出这台机器。几年后尝试实现了[ Schroeder and vaughn 1973],这些技术后来被其他人扩展 [Miller 1977],尽管 JIT 的基本性质从未改变,并被用于Hewlett-Packard 的 APL\3000 的实现[Johnston 1977;van Dyke 1977]。

Mixed Code, Throw-Away Code, and BASIC

执行时间和空间之间的权衡通常是 JIT 编译的基础。这种权衡在图1中进行了总结。另一个考虑是,根据经验研究的数据[Knuth 1971],大多教程序花费大部分时间执行少教代码。调和这些观察结果的方式出现了两种:混合代码一次性编译

混合代码是指将程序实现为原生代码和翻译代码的混合:由Dakin和Poole [1973]以及Dawson [1973]独立提出。程序中经常执行的部分将是在本地代码中,不经常执行的部分会被解释,希望能在对速度影响很小或没有影响的情沉下产生更小的内存占用。这就意味着一个细粒度的混合:用解释代码实现程序,用原生代码实现库﹐就不会构成混合代码。

混合代码方法的进一步转变涉及定制解释器[Pittman 1987],而不是在程序中混合本地代码,而是将本地代码表现为特殊的虚拟机指令;然后程序会被完全编译成虚拟机代码。

混合代码的基本思想,即在不同类型的可执行代码之间进行切换,仍然适用于JIT系统,尽管当时很少有研究人员主张在运行时生成机器码。在运行时将编译器和解释器同时放在内存中可能被认为在当时的机器上代价太大,从而抵消了任何关于程序大小的权衡。

反对混合代码的案例来自于软件工程[Brown 1976]。即使假设大多数代码将在解释器和编译器之间共享,仍然有两段完全不同的代码(解释器本身和编译器的代码生成器)必须维护并表现出相同的行为。

(部分求值或程序特化的支持者会注意到,这在某种意义上是一个似是而非的论点。因为编译器可以被认为是一个专门的解释器[Jones et al 1993]。然而,部分求值技术的使用目前并不普遍。)

这就引出了第二种调解方式:一次性编译(throw-away compilation,Brown 1976)。这纯粹是作为一种空间优化提出的:与静态编译不同,程序的一部分可以根据需要进行动态编译。在耗尽内存后,部分或全部编译后的代码可能会被丢弃;如果有必要,这些代码会在以后重新生成。

BASIC是一次性编译的测试平台,Brown[1976]基本上把这种技术描述为—种解决时空权衡的好方法;Hammond[1977]在某种程度上更坚定。他声称除了内存紧张的情况外,一次性编译是优越的。关于混合代码和一次性编译的讨论可以在Brown[1990]中找到。

FORIRAN

公式翻译程式语言

JIT系统中程序在运行时自动优化其“热点”的第一个工作是由Hansen[1974]完成的。他解决了三个重要的问题:

  1. **哪些代码应该优化?**Hansen 选择了一个简单、低成本的频率模型,为每个代码块维护一个执行频率计数器(我们使用通用术语块来描述一个代码单元块的确切性质对于我们的目的来说是无关紧要的)。
  2. 何时应该优化代码?频率计数器表演了第二个角色:跨越阀值使相关的代码块成为下一个“级别”优化的候选者,如下所述。“Su- pervisor”代码在块之间被调用,它将评估计数器,在必要时执行优化,并将控制转移到下一个代码块。后一种操作可以是直接调用,也可以是解释器调用——混合代码得到了Hansen设计的支持。
  3. **代码应该如何优化?**选择并排序了一组常规的机器无关和机器相关的优化,因此一个块可能首先通过常量折叠进行优化,其次通过对公共子表达式估值消除进行优化,当代码执行是执行第三次优化时间优化发生,由代码运动第三次,以此类推。Hansen[1974]观察到这个方案限制了在任何给定的优化点上所花费的时间(如果频率模型被证明是不正确的,这一点尤其重要),并且允许优化逐步添加到编译器中。

据报道,使用由此产生的自适应FORTRAN系统的程序并不总是比静态编译和优化的对应程序更快,但总体性能更好。

再次回到混合代码,Ng和Cantoni[1976]使用这种技术实现了FORTRAN的一个变体,他们的系统可以在运行时将函数编译为“伪指令”,可能是源代码的标记化形式,而不是较低层次的虚拟机代码,然后,这些伪指令将被解释。他们声称,运行时编译对于某些应用程序是有用的:并避兔了缓慢的编译—链接过程。他们在运行时不产生混合代码;他们对这个术语的使用指的是,静态编译的FORTRAN程序能够在需要时通过链按器欺骗自动调用它们的伪指令解释器。

Smalltalk

当向类中添加新方法时,Smalltalk 源代码会编译成虚拟机代码 [Goldberg and Robson 1985]。然而,na¨ıve Smalltalk 实现的性能还有待提高。

Deutsch 和 Schiffman [1984] 没有用硬件来解决性能问题,而是在软件上进行了关键的优化。这背后的观点是,他们可以选择最有效的信息表示,只要表示之间的转换是自动发生的,对用户是透明的。

虚拟机代码到本地代码的JIT转换是他们使用的优化技术之一,是比作宏展开的过程。程序被惰性地编译为本地代码,当执行进入程序时;本地代码被缓存起来以备以后使用。他们的系统与内存管理相关联,因为原生代码永远不会被分页出来,只是被丢弃并在以后必要时重新生成。反过来,Deutsch和Schiffman[1984]将动态翻译的想法归功于Rau[1978]。Rau关注的是能够很好地执行各种高级语言的“通用宿主机”(比如与专门的APL机器相比)。他提出在单个虚拟机指令的粒度上将之动态翻译为微码。一个硬件缓存,即动态翻译缓冲区,将存储完成的翻译;缓存未命中将表示翻译缺失,以及动态翻译例程中的故障。

Self

Self 编程语言 [Ungar and Smith1987;Smith and Ungar 1995],与本节提到的许多其他语言相比,它主要是一个研究工具。Self 在很多方面都受到 Smalltalk 的影响,因为两者都是纯面向对象的语言——一切都是对象。但是 Self 为了原型而回避类的概念,并且试图统一一些概念。每一个操作都是动态和可变的,甚至基本的操作,比如访间局部变量,都需要调用一个方法。更复杂的是,Self 是一种动态类型的语言,这意味着标识符的类型直到运行时才知道。Self 不寻常的设计使得高效的实现变得困难。这导致了当时最激进、最雄心勃勃的JIT 编译和优化的发展。Self 小组注意到三个不同的世代編译器 [H"olzle1994],下面将按照这个顺序进行介绍;在所有情况下,编译器都是在方法调用时动态调用的,就像Deutsch和Schiffman 的[1984]Smalltalk系统一样。

  • First Generation Self 编译器所采用的几乎所有优化技术都处理类型信息,并以某种确定标识符类型的方式转换程序。然而,只有少数技术与JIT编译有直接关系。

    在第一代 Self 编译器中,最主要的是定制[Chambers et al.1989;Chambers and Ungar 1989;Chambers 1992]。编译器没有动态地将方法编译成适用于任何方法调用的本机代码,而是生成了针对该特定上下文定制的方法版本。与静态编译相比,JIT编译器提供了更多的类型信息,通过利用这一事实,生成的代码更加高效。虽然来自相似上下文的方法调用可以共享自定义代码,但“过度自定义”仍然可能在运行时消耗大量内存:后来研究了解决这个问题的方法[Dieckmann and H"olzle 1997]。

  • Second Generation 第二代 Self 编译器扩展了其前身所使用的程序转换技术之一,并为循环计算了更好的类型信息[Chambers and Ungar 1990;chambers 1992]。这种 Self 编译器的输出确实比第一 代快,但也付出了代价。编译器在基准测试上的运行速度慢了15到35倍[Chambersand Ungar 1990. 1991], 以至于许多用户拒绝使用新的编译器[H"olzle 1994]。

    [Chambers and Ungar 1991]对相关的算法进行了修改,以加快编译速度.。其中一项修改被称为“不常见情况的延迟编译”。编译器被某些事件,例如篡术溢出,是不太可能发生。在这种情况下,不会为这些不常见的情况生成代码;取而代之的是在代码中留下一个存根(stub),如果有必要,存根将再次调用编译器。这样做的实际结果是,在初始编译时不需要对不常见情况下涉及的代码进行分析,从而节省了大量的时间。Ungar等人[1992]很好地介绍了Self 中使用的优化技术以及在第一代和第二代编译器中产生的性能。

  • Third Generation 第三代 Self 编译器从更基本的层面上解决了慢编译的问题。Self 编译器是交互式图形化编程环境的一部分:在运行中执行编译器会导致明显的执行停顿。H"olzle认为,通过计时编译器运行的时间量来测量 JIT 编译执行中的暂停是具有欺骗性的,并且不能代表用户的体验[H"olzle 1994;H"olzle and Ungar 1994b]。编译器的两次调用可以被一个简短的程序执行分开,但用户会认为是一个长时间的暂停。H"olzle通过考虑时间相关的暂停组,或“暂停集群”,而不是单独的编译暂停来进行补偿。

    至于编译器本身, 通过使用自适应优化减少了编译时间一一或者至少是分散了编译时间,类似于 Hansen [1974] 的 FORTRAN 工作。最初的方法编译是由一个快速的、非优化的编译器执行的:为每个方法保留了调用频率计数器,以确定何时应该发生重新编译[H"olzle1994;H"olzle and Ungar 1994a,1994b]。H"olzle 对这种机制做了一个有趣的评论:

    … 在我们的实验过程中,我们发现触发机制(“何时”)是对于良好的重新编评结果来说,比选择机制(“什么”)的重要性要小得多。[H"olzle 1994,p.38]

    这可能来自于一种有点违反直觉的观念,即重新编译的最佳候选方法不一定是其计数器触发重新编译的方法。面向对象的编程风格倾向于鼓励简短的方法;更好的选择可能是(重新)优化方法的调用者,并在内联[H"olzle and Ungar 1994b]中合并频繁调用的方法。

    自适应优化增加了复杂性,即修改后的方法可能已经在执行,并且具有依赖于修改后方法[H"olzle 1994]以前版本的信息(如栈上的激活记录);这一点必须被考虑进去。

    Self 编译器的 JIT 优化借助于“类型反馈”的引入[H"olzle 1994;H"olzle and Ungar 1994a]。当程序执行时,类型信息由运行时系统收集。当重新编译发生时,这种类型信息将是可用的,允许更积极的优化。使用类型反馈收集的信息后来被证明可以与静态类型推断的信息相媲美,甚至是互补的[Agesen and H"olzle 1995;Agesen 1996]。

Slim Binaries and Oberon

软件分发和维护的一个问题是软件运行的异构的计算环境:不同的计算机体系结构需要不同的二进制可执行文件。即使在同一系列向后兼容的处理器中,也可能存在许多可行性上的变化;一个程序静态为最不常用的处理器编译的可能不能充分利用它最终执行的处理器的优势。

在他的博士论文中,Franz用“精简的二进制”解决了这些问题 [Franz 1994; Franz and Kistler 1997]。一个精简的二进制文件包含一个高级的、与机器无关的程序模块表达。当加载一个模块时,为它动态生成可执行代码,它可以根据运行时环境调整自己。Franz和后来的Kistler声称,就产生的代码性能而言,一次性为整个模块生成代码通常优于 Smalltalk 和 Self 使用的每次生产的方法策略[Franz 1994;Kistler 1999]。

快速代码生成对于精简二进制方法至关重要。数据结构被精心安排以促进这一点;生成的代码可以被重用,并在以后需要时复制,而不是重新生成 [Franz 1994]。

Franz 为 Oberon 系统实现了允许动态加载模块[Wirth and Gutknecht1989]实现了细长的二进制文件。装载和生成精简二进制代码并不比装载传统二进制代码快 [Franz 1994;Franz and Kistler 1997],但Franz 认为这最终会成为在处理器以及 I/O 设备增长的情况下的速度差异。

以使用精简二进制作为起点,Kistler [1999] 的工作研究了“连续”运行时优化,其中执行程序的部分可以无限优化。他将这与Self中使用的自适应优化进行了对比,在Self中,方法的优化最终会停止。

当然,重新优化只有在新的、更好的解决方案能够得到的情况下才有用;这意味着连续优化最适合于输入随程序的时间变化而变化的优化执行。因此,Kistler研究了缓存优化——动态地重新排列结构中的字段,以优化程序的数据访问模式[Kistler 1999; Kistler and Franz 1999]——以及跟踪调度的动态版本,它基于执行过程中关于程序控制流的信息进行优化[Kistler 1999]。

连续优化器本身在后台执行,作为一个独立的低优先级线程,只在程序空闲时间执行[Kistler 1997,1999]。Kistler使用了比直接的计数器更复杂的指标来决定何时进行优化,并观察到决定优化什么是与优化高度相关的[Kistler 1999]。

同样的持续优化的思想对Scheme也实现了。Burger[1997]利用配置文件信息动态地对代码块进行重排序,以改进代码的局部性和硬件分支预测。他的方案依赖于(复制)垃圾收集器来定位指向函数旧版本的指针,并将它们更新为指向新版本的指针。这种动态的重新编译过程可以重复任何次数[Burger 1997,page 70]。

Templates, ML, and C

ML 语言和 C 语言并不相似,但两者在动态编译中都采用了相同的方法。这种方法称为阶段性编译,其中单个程序的编译分为两个阶段:静态编译和动态编译。在运行时之前,静态编译器编译“模板”,本质上是构建数据块,由动态编译器在运行时拼接在一起的,动态编译器还可以将运行时值放入模板中留下的空洞中。尽管在自动派生它们方面已经做了一些工作[Mock et al. 1999],通常情况下,这些模板是由用户注释指定的。

正如刚才所描述的,基于模板的系统可以说不符合我们对 JIT 编译器的描述,因为似乎没有重要的翻译方面。然而,模板可能以一种需要在执行前进行运行时转换的形式编码,或者动态编译器可能在连接模板后执行运行时优化。

模板已应用于 ML 语言的(子集)[Leone and Lee 1994;Lee and Leone 1996;Wickline et al.1998]。它们还被用于 C 语言的运行时特化[Consel and Noel 1996;Marlet et al. 1999],以及C 语言的动态扩展[Auslander et al.1996;Engler et al. 1996;Poletto et al. 1997]。其中一个系统,Dynamo,被提议为 Scheme 和 Java 以及 ML 执行分阶段编译和动态优化[Leone and Dybvig 1997]。

撇开模板不谈,ML 语言可以被动态编译。在Cardelli对他的 ML 语言编译器的描述中,他指出:

[编译]对于用户输入的每一个定义或表达式都是重复的…或者从外部文件获取。由于编译器的交互使用,小短语的编译实际上必须是即时的。.[Cardelli 1984, p. 209]

Erlang

Erlang是一种函数式语言,设计用于大型软实时系统,如电信设备[Armstrong 1997]。Johansson et al.[2000]描述了 Erlang 的 JIT 编译器 HiPE 的实现,旨在解决性能问题。

作为一个最近才设计,没有历史包袱的系统,HiPE 的突出之处在于用户必须显式调用 JIT 编译器。这样做的基本原理是,它让用户在混合代码提供的性能和代码空间权衡方面有很好的控制程度 [Johansson et al. 2000]。

Hipe 在执行原生代码和翻译代码的“模式切换”时非常小心。模式切换可能需要在明显的位置——调用和返回——以及被抛出的异常。它们的调用使用调用方的模式,而不是被调用代码的模式;这与Lisp中混合代码使用的技术形成了对比(Gabriel 和 Masinter [1985] 讨论了Lisp中的混合代码调用及其性能影响)。

Specialization and O’Caml

O’Caml是另一种函数式语言,可以认为是 ML 语言的一种方言[Remy et al. 1999]。O 'Caml 翻译器一直是运行时专门化工作的重点。

Piumarta 和 Riccardi [1998]以一种受限的方式将解释器的指令专门用于正在运行的程序,他们首先动态地将解释的字节码翻译成直接的线程代码[Bell 1973],然后动态地将指令块组合成新的“宏操作码”,修改代码以使用新的指令。这减少了指令分派的开销,并产生了宏操作码的优化机会,如果指令是分开的就不可能实现这种优化(尽管它们不执行这种优化)。正如所介绍的,他们的技术没有考虑动态执行路径,并且他们指出,它最适合于低级指令集,在这种情况下,调度时间对性能的影响相对较大。

Thibault 等人采用了一种更通用的运行时特化方法[2000]。他们将他们的程序专家"Tempo" [Consel et al. 1998]应用于运行时的 Java 虚拟机和 O 'Caml 解释器。他们指出:

虽然特化所获得的加速是显著的,但它无法与手工编写的离线或运行时编译器所获得的结果竞争。[Thibault et al.,2000,p.170]

但在论文的后面,他们说:

…程序特化正在进入相对成熟阶段。[Thibault et al. 2000, p. 175]

这可能意味着,至少在目前,程序特化可能不像其他动态编译和优化方法那样富有成效。

Prolog

Prolog 系统也可以动态编译,尽管 Prolog 的执行模型需要使用专门的技术。Van Roy [1994] 对这一领域进行了出色而详细的调查。SICStus Prolog 的本机代码编译器之一在 Haygood [1994] 被描述可以被调用并动态加载其输出。

Simulation, Binary Translation, and Machine Code

模拟是在另一个体系结构上运行一个体系结构的本机可执行机器代码的过程。这与 JIT 编译有什么关系?其中一种模拟技术是二进制转换;我们特别关注动态二进制翻译,它涉及在运行时从一个机器代码转换到另一个机器代码。通常,二进制转换器在源代码和目标代码方面高度特化:对可重定向和“可参考的”二进制转换器的研究仍处于初级阶段[Ung and Cifuentes 2000]。Altman等人[2000b]很好地讨论了二进制转换所涉及的挑战,Cmelik 和 Keppel [1994] 详细比较了1995年以前的模拟系统。我们不会复制他们的工作,而是采取更高层次的观点。

May [1987] 提出模拟器根据他们的实现技术可分为三代。在此基础上,我们增加了第四代来实现更新的工作成果:

  1. 第一代模拟器是解释器,它可以根据需要简单地解释每个源指令。正如预期的那样,由于解释开销,它们往往表现出较差的性能。
  2. 第二代模拟器每次动态地将源指令翻译成目标指令,并将翻译缓存以备以后使用。
  3. 第三代模拟器通过一次性地动态地翻译整个源指令块来改进第二代模拟器的性能。这就提出了新的问题,即应该翻译什么?大多数这样的系统要么翻译基本代码块,要么扩展基本代码块[Cmelik and Keppel 1994],反映了源程序的静态控制流。还可以使用其他静态翻译单元:一个异常系统 DAISY 执行从 PowerPC 指令到 VLIW 指令的逐页翻译 [Ebcioglu and Altman 1996,1997]。
  4. 第四代模拟器通过动态转换路径在第三代模拟器的基础上进行扩展。路径反映源程序在运行时显示的控制流,它是动态的而不是静态的转换单元。最近关于二进制翻译的工作集中在这类系统上。

第四代模拟器在最近的文献中占主导地位 [Bala et al. 1999; Chen et al. 2000; Deaver et al. 1999; Gschwind et al. 2000; Klaiber 2000; Zheng and Thompson 2000]。它们的结构非常相似:

  1. 异形执行 模拟器的工作应该集中在频繁执行的代码的“热点”区域。例如,只执行一次的初始化代码不应该翻译或优化。为了确定哪些执行路径是热点区域,将以某种方式执行源程序并收集概要信息,在这方面投入的时间被认为最终会得到回报。

    当源体系结构和目标体系结构不相似,或者源体系结构不复杂时(例如简化的指令集计算机(RISC)处理器),然后通常使用源程序的解释来执行源程序 [Bala et al. 1999; Gschwind et al. 2000; Transmeta Corporation 2001; Zheng and Thompson 2000]。Rosenblum等人总结了另一种方法,即直接执行。[1995, p. 36]:

    目前为止最快的CPU模拟器,MMU,和SGI多处理器的内存系统是一个SGI多处理器。

    换句话说,当源体系结构和目标体系结构相同时,比如目标是源程序的动态优化时,源程序可以直接由中央处理器执行(CPU)。通过适当地修改源程序,模拟器周期性地恢复控制[Chen et al. 2000]或通过不那么直接的方式,如中断[Gorton 2001]。

  2. 热点路径检测 为了代替硬件支持,可以通过保持计数器来记录执行频率来检测热路径 [Zheng and Thompson 2000],或者通过观察结构上可能很热门的代码,比如向后分支的目标 [Zheng and Thompson 2000],在硬件支持下,程序的程序计数器可以间隔采样,以检测热点 [Deaver et al. 1999]。

    其他一些考虑因素是,如果路径太昂贵或难以翻译,可能会在战略上排除它们 [Zheng and Thompson 2000],为路径选择好的停止点可能与选择好的起点一样重要,以保持可管理的轨迹数量 [Gschwind et al. 2000]。

  3. 代码生成和优化 一旦注意到热点路径,模拟器将把它转换为目标体系结构的代码,或者优化代码。翻译的正确性一直是一个问题,郑和汤普森讨论了一些实证验证技术[2000]。

  4. “Bail-out”机制 在动态优化系统中(源和目标体系结构相同),可能会对源程序的性能产生负面影响。救援机制[Bala et al. 1999]启发式地尝试检测这样的问题,并返回到源程序的直接执行;例如,可以通过监视路径工作集的稳定性来发现这一点。这种机制也可以用来避免处理复杂的情况。

最近二进制翻译工作中另一个反复出现的主题是二进制翻译的硬件支持问题,特别是将遗留体系结构的代码翻译成 VLIW 代码。这引起了人们的兴趣,因为 VLIW 体系结构承诺遗留体系结构实现具有更高的性能,更大的指令级并行性[Ebcioglu and Altman 1996,1997],˘更高的时钟率[Altman et al. 2000a; Gschwind et al. 2000],以及更低的功率要求[Klaiber 2000]。这些处理器中的二进制翻译工作仍然是由软件在运行时完成的,因此仍然是动态二进制翻译,尽管偶尔会用更花哨的名称来吸引风险投资家 [Geppert and Perry 2000]。这些系统的关键思想是,为了提高效率,目标 VLIW 应该提供源体系结构的超集 [Ebcioglu˘ and Altman 1997];这些源程序看不到的额外资源可以被二进制转换器用于积极的优化或模拟源体系结构中麻烦的方面。

Java

Java 是通过对Java虚拟机(JVM)字节码指令的静态编译实现的。早期的 jvm 只是解释器,导致性能不佳:

*解释字节码很慢。 [Cramer et al. 1997, p. 37]*

*Java 不只是慢,它真的很慢,慢得惊人。 [Tyma 1998, p. 41]*

不管这个表达多么尖刻,它传递的信息是 Java 程序必须运行得更快,而实现这一目标的主要方法是 Java 字节码的JIT编译。的确,Java使“just-in-time”一词在计算文献中得到了广泛的使用毫无疑问,对快速 Java 实现的压力刺激了 JIT 研究;历史上从未有过如此集中的时间和金钱投入。

Java JIT 编译的早期观点是由Cramer等人[1997]提出的,他们是Sun Microsystems (Java的前身)的工程师。他们观察到 JIT 编译可以实现的加速有一个上限,注意到在他们运行的概要文件中,适当的解释只占执行时间的68%。他们还主张直接使用 JVM字节码(一种基于堆栈的指令集)作为 JIT 编译和优化的中间表示。回想起来,这只是少数人的观点;大多数后来的工作,包括Sun自己的[Sun Microsystems 2001],都是从将JVM代码转换为基于寄存器的中间表示开始的。

Java JIT工作的有趣趋势 [Adl-Tabatabai et al. 1998; Bik et al. 1999; Burke et al. 1999; Cierniak and Li 1997; Ishizaki et al. 1999; Krall and Grafl 1997; Krall 1998; Yang et al. 1999]隐含的假设是,仅仅从字节码到本机代码的翻译是不够的:代码优化也是必要的。同时,这项工作认识到传统的优化技术是昂贵的,并寻找优化算法的修改,以达到算法执行速度和结果代码速度之间的平衡。

除了通常的先解释、后优化的方法之外,还有其他 Java JIT 编译方法。Burke 等人[1999]采用了只编译的策略,没有任何解释器,他们也用 Java 实现了他们的系统;JIT的改进直接使他们的系统受益。Agesen[ 1997] 将 JVM 字节码翻译成 Self 代码,以利用Self编译器中已经存在的优化。Azevedo等人 [1999] 尝试将代码优化的工作转移到运行时之前:高效 JIT 优化所需的信息被预先计算出来,并作为注释标记到字节码上,然后被 JIT 系统用于辅助其工作。最后,Plezbert 和 Cytron [1997]提出并评估了 Java 的“连续编译”思想,即解释器和编译器可以并发执行,最好是在独立的处理器上。

CLASSIFICATION OF JIT SYSTEMS

在考察 JIT 工作的过程中,出现了一些共同的属性。我们提出 JIT 系统可以根据三个属性进行分类:

  1. 调用 如果用户必须在运行时采取某些操作来进行编译,则显式调用 JIT 编译器。隐式调用的 JIT 编译器对用户是透明的。
  2. 可执行性 JIT 系统通常涉及两种语言:要翻译的源语言和要翻译的目标语言(如果JIT系统只动态地执行优化,那么这些语言可能是相同的)。如果 JIT 系统只能执行这些语言中的一种,我们就称它为单可执行的,如果可以执行多种语言,我们就称它为多可执行的。多可执行 JIT 系统可以决定何时需要编译器调用,因为这两种程序表示都可以使用。
  3. 并发性 这个属性描述了 JIT 编译器相对于程序本身的执行方式。如果程序执行自动暂停以允许编译,它就不是并发的:在这种情况下,JIT 编译器可以通过子例程调用、消息传输或将控制转移到协程来调用。相反,并发的 JIT 编译器可以在程序并发执行时进行操作:在单独的线程或进程中,甚至在不同的处理器上。

JIT 系统的硬实时功能可能构成第四个分类属性,但目前在该领域的研究似乎很少;目前还不清楚硬性实时约束是否会给 JIT 系统带来任何独特的问题。

有些趋势是显而易见的。例如,隐式调用的 JIT 编译器在最近的工作中绝对占主导地位。可执行性因系统而异,但这更多是一个设计问题,而不是 JIT 技术的问题。关于并发 JIT 编译器的工作目前才刚刚开始,随着处理器技术的发展,它的重要性可能会增加。

TOOLS FOR JIT COMPILATION

总的来说,帮助动态生成二进制代码的通用的、可移植的 JIT 编译工具直到最近才出现。这些工具包在不同程度上解决了三个问题:

  1. 二进制代码生成 正如 Ramsey 和 Fernandez [1995]所论证的那样,“像机器语言那样发出二进制代码是一种充满错误机会的情况。还有相关的簿记任务:在初始代码生成时,信息可能还不可用,比如转发分支目标的位置。一旦发现,信息必须回补丁到适当的位置。
  2. 高速缓存一致性 近年来,CPU速度的进步远远超过了内存速度的进步 [Hennessy and Patterson 1996]。为了弥补这一点,现代的cpu 包含了一个小的、快速的缓存内存,它的内容可能会暂时与主存不同步。在动态生成代码时,必须注意确保缓存内容反映了在执行之前写入主存的代码。当多个cpu共享一个内存时,情况就更加复杂了。Keppel[1991]对此进行了详细的讨论。
  3. 执行 硬件或操作系统可能会施加限制,限制可执行代码可以驻留的位置。例如,指定给数据的内存在默认情况下可能不允许执行(即,指令获取),这意味着代码可以生成到数据内存中,但如果没有特定于平台的争论就不能执行,参考 Keppel [1991]。

只有第一个问题与 JIT 编译解释的虚拟机代码有关——解释器并不直接执行它们解释的代码——但是 JIT 编译工具没有理由不能用于生成非本机代码。

表1给出了工具包的比较。除了说明工具包对上述三个领域的支持程度之外,我们还添加了两个额外的类别。首先,抽象接口是与体系结构无关的。使用工具包的抽象接口意味着为了使用新平台,用户的代码需要修改的很少(如果有的话)。缺点是依赖于体系结构的操作(如寄存器分配)可能很困难,从抽象到实际机器的映射可能是次优的,例如从RISC抽象到复杂指令集计算机(CISC)机制的映射。

其次,输入是指工具包所期望的输入的结构(如果有的话)。就JIT编译而言,更复杂的输入结构需要更多的时间和空间供用户生产和工具包使用[Engler 1996]。

使用工具可以解决一些问题,但也会引入其他问题。与手动生成二进制代码相比,二进制代码生成工具有助于避免许多错误。然而,这些工具需要对二进制指令格式有详细的了解,其规范本身可能容易出错。Engler 和 Hsieh[2000]提出了一种“元工具”,它可以通过重复查询具有不同输入的现有系统汇编程序来自动派生这些指令编码。

CONCLUSION

动态编译(即时编译)是一种古老的实现技术,具有碎片化的历史。通过收集这些历史信息,我们希望缩短重新发现的航程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
4S店客户管理小程序-毕业设计,基于微信小程序+SSM+MySql开发,源码+数据库+论文答辩+毕业论文+视频演示 社会的发展和科学技术的进步,互联网技术越来越受欢迎。手机也逐渐受到广大人民群众的喜爱,也逐渐进入了每个用户的使用。手机具有便利性,速度快,效率高,成本低等优点。 因此,构建符合自己要求的操作系统是非常有意义的。 本文从管理员、用户的功能要求出发,4S店客户管理系统中的功能模块主要是实现管理员服务端;首页、个人中心、用户管理、门店管理、车展管理、汽车品牌管理、新闻头条管理、预约试驾管理、我的收藏管理、系统管理,用户客户端:首页、车展、新闻头条、我的。门店客户端:首页、车展、新闻头条、我的经过认真细致的研究,精心准备和规划,最后测试成功,系统可以正常使用。分析功能调整与4S店客户管理系统实现的实际需求相结合,讨论了微信开发者技术与后台结合java语言和MySQL数据库开发4S店客户管理系统的使用。 关键字:4S店客户管理系统小程序 微信开发者 Java技术 MySQL数据库 软件的功能: 1、开发实现4S店客户管理系统的整个系统程序; 2、管理员服务端;首页、个人中心、用户管理、门店管理、车展管理、汽车品牌管理、新闻头条管理、预约试驾管理、我的收藏管理、系统管理等。 3、用户客户端:首页、车展、新闻头条、我的 4、门店客户端:首页、车展、新闻头条、我的等相应操作; 5、基础数据管理:实现系统基本信息的添加、修改及删除等操作,并且根据需求进行交流信息的查看及回复相应操作。
现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本微信小程序医院挂号预约系统就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息,使用这种软件工具可以帮助管理人员提高事务处理效率,达到事半功倍的效果。此微信小程序医院挂号预约系统利用当下成熟完善的SSM框架,使用跨平台的可开发大型商业网站的Java语言,以及最受欢迎的RDBMS应用软件之一的MySQL数据库进行程序开发。微信小程序医院挂号预约系统有管理员,用户两个角色。管理员功能有个人中心,用户管理,医生信息管理,医院信息管理,科室信息管理,预约信息管理,预约取消管理,留言板,系统管理。微信小程序用户可以注册登录,查看医院信息,查看医生信息,查看公告资讯,在科室信息里面进行预约,也可以取消预约。微信小程序医院挂号预约系统的开发根据操作人员需要设计的界面简洁美观,在功能模块布局上跟同类型网站保持一致,程序在实现基本要求功能时,也为数据信息面临的安全问题提供了一些实用的解决方案。可以说该程序在帮助管理者高效率地处理工作事务的同时,也实现了数据信息的整体化,规范化与自动化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值