从 JIT 编译看 Runtime 的过去与未来

本文介绍了编程语言的运行时(Runtime)和 JIT(Just-In-Time)编译的历史、分类及挑战,探讨了 JIT 编译在 LISP、APL、BASIC 等语言中的应用,并分析了现代 JIT 系统面临的二进制代码生成、缓存和执行等问题。随着技术的发展,JIT 编译在提高程序性能、适应多语言需求等方面展现出巨大潜力,尤其是在深度学习和云原生环境中的应用。
摘要由CSDN通过智能技术生成

作者简介

常开颜

中国科学院计算技术研究所直博生,研究方向为硬件编程语言、编译技术。


如果读者想了解更多有关 Runtime 相关的技术内容,欢迎加入编程语言社区 SIG-Runtime

加入方式:文末有小助手微信,添加并备注加入 SIG-Runtime。


# 编译器是什么 #

编程语言处理器可以分为三类,它们之间的关系用一句著名的话说就是:编译器是特化的解释器(a compiler is a specialized interpreter)[1]。

  • 编译器 Compiler 能够给定一种语言的程序,输出另一种语言里的等价程序(编译过程会生成新的程序)

  • 解释器 Interpreter 能够给定一个程序和程序的输入,计算程序的结果(解释过程不生成新的程序)

  • 特化器 Specializer 能够给定一个程序一些提前知道的输入,创建一个仅需要剩下输入的等价(但更高效)的程序

\begin{aligned}Compiler&:program(L_0)\rightarrow program(L_1)\\Interpreter&: input\rightarrow program(L_0)\rightarrow result\\Specializer&:input_{partial}\rightarrow program(L_0)\rightarrow program(L_1) \\\end{aligned}

注:L1:即计算机实际执行的语言

作为一篇科普性质的综述类文章,本文不打算挖掘深度,而是注重广度,聚焦于提供一个 Runtime 的领域蓝图。就像 J.R.R.Tolkien 所说:"你必须有一张地图,不管多么粗糙。否则你会四处游荡。(You must have a map, no matter how rough. Otherwise you wander all over the place.)"

# Runtime 是什么 #

Runtime 包括了 动态程序的相关理论 和 程序运行时的支持系统

  • 动态程序的相关理论 指程序动态特性相关的编程语言理论,比如动态程序验证,运行时并发程序的动态验证等;

  • 运行时支持系统 runtime system 指保障程序在运行时正确执行的支持系统,比如垃圾回收系统、即时编译系统、处理器系统等。

由于编程语言的运行不仅需要软件系统的支撑,也需要硬件系统的支撑,因此 Runtime 涉及了软件和硬件层次的许多交叉方面。此外,Runtime 不仅与通用编程语言有关,也与领域特定语言有关。在近来兴起的深度学习编译器中,许多框架都用到了即时编译系统,比如 PyTorch。在云计算兴起的时代,在可编程网络领域也广泛用到了编程语言作为接口对设备进行配置,比如阿里云太玄 OS 的跨平台编程语言和编译器 Lyra [2]。

考虑到 Runtime 是一个涉及编程语言理论、体系结构、程序分析等的综合领域,本文按照应用领域把相关的研究大致分为以下六个领域(如下图所示),下面对每一个领域做说明。

图片

Runtime 应用领域概览

编译器运行时

编译器运行时属于传统的程序运行时支持系统,包括了通用语言虚拟机及其 JIT 编译,比如:JVM、V8 等。

在传统的编译器运行时中,一个很大的部分是 垃圾回收技术(Garbage Collection),由此衍生出了 三色标记法标记清扫法引用计数法 等不同的垃圾回收方法。

其他和编程语言特性相关的特性还包括了反射、虚函数等。

涉及到的其他程序运行时的行为还有 并发 和 事务

同时编译器运行时也涵盖了领域特定语言(Domain Specific Language)及其 JIT 编译。领域特定语言是专用于某一个领域的编程语言,分为嵌入式领域特定语言和外部领域特定语言,嵌入式领域特定语言包括 Halide、TVM、Tensorflow、JAX 等,外部领域特定语言包括:云原生编程语言 Ballerina,硬件描述语言 Verilog、VHDL。

动态程序验证

动态程序验证与静态验证基于不同的系统。静态程序的验证一般是从作用域、类型等抽象特性上验证程序的语义是否符合规范,而动态验证则更为灵活。

从理论上讲,可以归结为 动态语义的验证,比如采用 霍尔逻辑(Hoare Logic) 验证数据结构的正确性,验证循环的正确性;还可以使用 混合符号执行(Concolic Testing) 来测试程序,缩减符号执行造成的搜索空间爆炸的问题,比如使用混合符号执行 fuzzing。

从实践上讲,在 JVM 中,类的加载阶段需要进行验证,比如采用变量类型执行的方式,把变量的类型压入虚拟机栈,在类型执行时判断栈顶类型是否匹配。

动态程序分析

动态程序分析是对程序的动态行为进行分析,从而定位程序故障、进行调试或是进行更为激进的优化。在调试模式中,LLVM 采用 程序插桩 的方法,在局部变量的前后插桩方便调试时查看它们的值。LLVM 也采用了其他方法来加快程序的运行速度,比如收集性能制导优化的 profile,在每个分支指令后标注类似 !prof !0 的标识插入分支相对可能执行次数的元信息来辅助提高程序执行速度。还可以在程序运行时分析存储的读写序列发现访存冲突问题。

动态程序合成

动态程序的合成可以分为两类,一种是 交互式程序合成,比如微软在 Excel 中的自动填充技术,可以根据已经存在的单元格数据提取相关信息,与用户输入的数据进行对比,填充空白的单元格。第二种是收集运行时信息的 程序合成 技术,比如 TVM 的自动调度方案,采用运行时的性能模型合成高效率的代码调度 [3]。

体系结构优化

编程语言与体系结构的交互伴随运行过程始终,在动态编译、并行编程和存储系统方面都有体现。动态二进制指令翻译是一种与此相关的研究,把一种形式的二进制指令翻译为另一种形式的二进制指令,比如使用动态二进制翻译的 CPU 模拟器 Qemu。还有用于并行编程的推测调度和多线程技术,比如软件流水技术、超标量技术。近来兴起的 非易失性存储器(Non-Volatile Memory) 成为数据存储领域新的研究载体,有研究在 Java 语言中加入与 NVM 相关的关键字,从编程语言的设计上方便操作新型存储器。

集成电路设计

硬件是编程语言运行时的核心部分,向上提供指令集作为接口。领域特定语言(DSL)需要领域特定硬件(DSA)来作为实际载体加速。DSL 加速软件开发,DSA 加速领域特定语言的运行速度是深度学习出现以来的新的趋势。电子设计自动化工具作为硬件设计的重要组成部分一直是重要的组成部分。无论是传统上的集成电路设计用的 Verilog、VHDL,还是后来为了方便编程演变出的 HLS 和 Pynq [4],都是为了加速芯片设计而产生的。但是近来兴起的 Circt 和 XLS 沿袭了 LLVM 的思路,采用了复杂系统模块化的思路,希望能够对传统集成电路设计工具解耦。

# Runtime 的过去 #

笔者在 Charles N. Fischer 所著的 Crafting a Compiler [5] 中了解到 Runtime 的发展历史。

编程语言设计的演变导致越来越复杂的运行时存储组织方法的产生。举例来说,数组可以被分配一个单一的固定大小的内存块,而现在有一些新的编程语言,允许数组的大小在程序执行时指定,甚至能够根据程序的需要动态扩展。

最初,所有的数据都是全局的,其生命周期跨越了整个程序。相应地,所有的存储分配也是静态的。在翻译过程中,一个数据对象或指令序列在程序的整个执行过程中被简单地放在一个固定的内存地址上。

在 1960 年代,Lisp 和 Algol 60 语言引入了局部变量,这些局部变量(local variables)仅仅能够在子程序(subprogram)的执行的时候被访问。这种特性导致了 栈式分配(stack allocation) 的产生。当过程或者方法被调用时,新的 栈帧(frame) 被压入运行时栈。栈帧由为所有特定过程中的局部变量分配的空间组成。随着过程的返回,它的栈帧被弹出,同时被局部变量占据的空间被取回。因此,仅仅是正在执行的过程被分配了空间,而不活跃的过程不需要数据空间。这使得相比早期使用静态分配翻译的空间效率更高。而且,递归的过程会由于同一个过程不相关嵌套的活动而请求多个栈帧。

Lisp 和随后的像 C、C++、C# 和 Java 一类的语言,使得动态分配数据能够在运行期间的任何时候被创建或者释放。动态的数据请求堆分配(heap allocation),这使得内存块在程序执行的任何时间以任何顺序被分配和释放。使用动态分配,数据对象的数目和大小不需要提前固定。每个程序的执行能够定制它所需要的存储分配

所有的存储分配技术利用数据区域(data area)这个符号。一个数据区域是一块被编译器知道的存储,这个区域具有统一的存储分配的必要条件。也就是说,所有在这个数据区域上的对象共享相同的数据分配策略。一个程序的全局变量能够组成一个数据区域。当程序开始运行时,所有变量的空间被分配,并且变量仍然再分配直到执行结束。

这里给一个关于内存布局的问题,C++ 语言里给定一个 char * 的变量指针,如何判断这个变量是 new 动态分配的还是在栈中呢?如下面的代码所示,我们只需要根据栈是在程序虚拟空间的顶端自顶向下生长,而堆是在栈的下方自底向下生长这一规律就可以判断。ch 变量是局部变量,分配在栈中,故其地址一定大于堆中的变量地址。

图片

C 语言运行时空间分配

bool if_malloc(char *p) {
  char ch{};
  char *ch_p{&ch};
  if (ch_p> p) {
    return true;
  }
  else
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值