【译】Ruby2.6的JIT功能,编译和解释型语言的相关说明

825 阅读10分钟

一篇阐述编译以及解释相关概念的文章,对于理解计算机语言的编译以及解释的过程有一定的好处,同时还会了解到JIT到底是个什么东西。原文链接: medium.com/@mich_berr/…


Ruby2.6

Ruby2.6在几个星期之前发布了,新特性是一个崭新的Just-In-Time(JIT)编译器. 你可以点击这里读到关于关于新特性的更多细节,但如果你在寻找更多的背景知识,那么我想一个阐述编译型语言,解释型语言,以及JIT是如何工作的相关指引会对你有所帮助。

首先,什么是编程语言?所有的编程语言都是机器代码的抽象。机器代码是由位(许多的0和1)组成的,可以通过你机器的硬件进行存储和执行。让人类去阅读和编写二进制是非常低效的事情,这也是我们发明编程语言的理由。

解释和编译的不同之处主要在于人类编写的程序是怎么转换成机器可以执行的指令集的。有的时候编程语言并不能明确地划分为是到底是编译型的还是解释型的;编译和解释是把你写的代码转换成机器可读代码的两种不同的策略。我们会在讨论JIT的时候了解到编译和解释之间的界线变得越发模糊。

编译

简单来说,编译就是把高级的编程语言转换成机器能够识别的语言。

让我们拿C语言来做个例子,它是一门有代表性的编译型语言。为了运行C语言写的程序,必须使用像gcc或者clang这样的编译器,把C语言的源代码编译成能适应你计算机的机器码。需要重点指出,不同的电脑可能会有不同的CPU架构,意味着一台电脑处理0和1序列的方式跟另一台电脑是不一样。一个编译器会把源代码转换成特定架构的机器代码。一旦你的代码被编译完,它可以按你希望的那样运行在任何具有相同架构的系统上。不过如果你更新了源代码,或者想要把你的程序运行在具有不同架构的机器上的时候,你就需要对代码进行重新编译了。

编译器的一个很好的类比就是人类的翻译人员。跟翻译人员把西班牙语的书翻译成英文书籍那样,一个编译器就是把人类编写的源代码翻译成机器代码。一旦一本书被翻译完,任何懂英语的读者都能够阅读它。然而如果原来西班牙语版本的书籍有所改动,英文版将需要重新翻译相关的部分并再次发布。

解释

不像编译器可以在程序运行之前预先把源代码翻译成机器码,解释器是一行接着一行,一边翻译一边执行。继续之前的类比,计算机解释器就像一个口译人员。他们充当西班牙语以及英语谈话者交流的桥梁,一句句实时地翻译出来。

我们用Ruby作为“解释型”语言的一个例子。Ruby1.8以及更早的版本,那个时候的Ruby解释器(MRI), 它的行为就像上面描述的那样。它读取每一行Ruby代码,解析并token化,然后使用一个树型的数据结构来执行它。而从版本1.9开始,Ruby切换到一个包含YARV(Yet Another Ruby Virtual Machine)的实现。在这个实现里,Ruby会被预编译成字节码,这样命名是因为它们会占用一个字节的内存空间。一个非常简单的例子,2+3 将会把加法运算转换成字节码的形式,并且接收23作为参数,一旦Ruby代码被转换成字节码,这些字节码将会由虚拟机一行一行地执行。把源代码转换成字节码对提升运行速度有重大意义。

Python当然也会利用字节码,你可以直接在Python程序产生的.pyc文件里面看到它。这些文件的作用就像是一个缓存;如果Python代码再次运行,却没有作任何修改,可以跳过编译的步骤直接执行相关的字节码文件。当Ruby编程成字节码之后,它的字节码只是存储在内存中而不是持久化到文件。

Just-In-Time编译

我刚刚描述了一些当代的一些解释型语言如今是怎么包含字节码编译这一步骤的,但还有另一种方式,它会使编译与解释之间的界线开始模糊起来,被称为Just-In-Time编译。

最流行的Just-In-Time编译器就是Java虚拟机(JVM)了。Java是一门静态类型的编程语言,它可以直接被编译成机器代码,但是这通常都会通过JVM来完成转换。在近期的案例中,Java代码会被Javac编译器编译成Java字节码,这些字节码会由JVM来翻译并执行。然而JVM并不会一句句地去翻译字节码。相反,在这个时候它会尝试去组合像函数那样的有意义的代码块。而要决定怎么去组合这些代码块将会稍微有点耗时,不过最终会使执行更加高效。这个过程被称之为Just-In-Time编译,因为它的行为就像是一个编译器,区别就是它会在运行时进行编译。使用JVM的好处是它既保留了编译型语言的性能特征又让Java可以像解释型语言那样移植到不同的机器上。Java是世界上最流行的编程语言,很大一部分原因要归功于它的JVM。

除了JVM之外,已经有其他的VM项目采用了JIT编译。PyPy是一个包含JIT功能的Python解释器,当然现在的Ruby也有可选的JIT功能了。

折衷

现在你已经对编译型以及解释型语言有个大概的了解了,那么他们各自是如何权衡的呢?

速度

编译型语言通常会比解释型语言快许多。一个编译好的C程序可能要比Python,Ruby这样的解释型语言快上好几个量级。然而Java的JIT解决方案也是非常高效的,它的运行速度可以几乎可以媲美C语言所写的程序。

可移植性

为了在不同架构的机器上运行你编译好的程序,你需要重新编译它。当一门语言被解释之后,它的指令集可以在具有不同架构的机器上运行(当然要有相关的虚拟机)。JVM就是很好的例子,它结合了解释以及编译两门技术,最大程度地兼顾了速度以及可移植性。

动态类型 VS 静态类型

编译器必须要把应用程序转换并组合成机器指令,这是非常死板的。当声明一个变量的时候,编译器需要明确地知道是什么类型的变量以及需要为它分配多少内存空间。这就是为什么编译通常都要求静态类型。作为对比,解释器一行行地执行相关的程序,因此他们的行为更灵活。

在Ruby里面,我们可以写出像2 + 3 或者"a" + "b"这样的代码,解释器会在运行时确定对象的类型,不管是整数的相加,还是字符串的拼接,都会为它们调用正确的方法。

Bugs/调试

解释型语言通常更容易调试,因为程序在遇上错误之前会一直执行。解释器将会告知用户具体是哪一行引发的运行时错误,反之,在编译好的程序里面bug比较难以发现。

FAQS

为什么把一门语言看成是“编译型语言”或者“解释型语言”是用词不当的?

一门语言是根据他们的语法以及数据结构来确定的。编译及解释是把语言的语法转换成可以在硬件上运行的形式的两种不同的实现方式。“编译”或者“解释”并不是语言的天性;同一门语言可能会同时包含这两种实现方式。

为什么人们把Python看作是解释型语言把C看作是编译型语言?

他们都是参考了该语言最通用的实现或者是发行版本。然而Python也能够被编译,C语言当然也可以被解释。

什么是虚拟机?

一个虚拟机就是一切行为像计算机那样的抽象,意味着它能够接收一系列的指令,与硬件的实现方式不同,它是通过软件来实现的。虚拟机通常用于在计算机的一个操作系统上运行其他的操作系统。举个例子,你有一个windows的笔记本,但是想要仿真一个Linux的操作系统。你会利用一个软件,它隐蔽在你物理机的操作系统跟你想要运行的操作系统之间。这被称为一个“系统虚拟机”。

不要跟之前的例子混淆,那些是“程序虚拟机”。比如,Java的虚拟机(JVM)或者Ruby的虚拟机(YARV)。这些都会被看作是虚拟机,因为它们可以接收指令集并运行相关的字节码。虚拟机的好处是对硬件进行抽象并提供了一个平台独立的编程环境。

为什么人们通常认为Python和Ruby有解释器,而Java有虚拟机?解释器是否也符合虚拟机的定义?

解释器和虚拟机的更多是语义上的区别,或者像这个人所阐述的是“社会构建”上的区别。我觉得解释器是符合虚拟机的定义的。严格意义上,Ruby和Python的解释器都包含能够处理相关字节码的虚拟机。而另一方面,JVM本质上跟Ruby或者Python的解释器有所不同。我已经在这篇文章中触及了一小部分的原因,不过剩下的知识已超出这篇文章的范围。

为什么解释一门语言之前先把它编译成字节码能使速度加快?

字节码会比源码耗费更少的内存空间,并且更容易被解释器执行。当一门语言要被编译成字节码,解释器必须要解析所有的语句,把他们转换成字节码,然后解析字节码并执行它们。对一个简单的代码片段而言,这个中间的步骤会小幅度地增加了执行的时间。然而,对那些需要重复执行的代码,比如,循环或者是可复用的方法等,字节码的步骤能够大幅度提升速度。

更多资源:

BaseCS,一个关于编程基础概念的很不错的博客https://medium.com/basecs/a-deeper-inspection-into-compilation-and-interpretation-d98952ebc842

Ruby原理剖析,深度谈论Ruby内部机制的书籍,书中有一些C语言代码,程序员应该会感到亲切:patshaughnessy.net/ruby-under-…

Bradfield Academy上的计算机组成方面的课程,关于计算机的组成概念的优秀课程:bradfieldcs.com/

Tyler Elliot Bettilyon的youtube视频,Tyler是我在Bradfield上的指导员,是星球上把CS概念阐述得最好的人类之一https://www.youtube.com/watch?v=KsZLPTRSleI