因为工作原因,最近要做包管理工具方面的开发,需要对 Compiler 有一些最基本的理解,写这篇文章的目的有两个:

  • 为了记录和整理自己的近期的学习内容,方便日后查阅
  • 抛开大段代码和抽象概念,通过通俗易懂的写作方式来加深自己对这些概念的理解

废话不多说,我们一起看看内容吧!

需要了解的概念

在看了不少关于编译相关的文章之后,我发现下面的词汇是大量出现的。

知道这些词汇代表的意思,以及对应的层次,能够更好地看懂别人所要表达的意思。

高级语言代码 High-Level Code

高级语言代码,自然是指由高级编程语言编写代码,对计算机的细节有更高层次的抽象。

相对于低级编程语言(low-level programming language)更接近自然语言(人类的语言),集成一系列的自动工具(垃圾回收,内存管理等),会让程序员更快乐的编写出更简洁,更易读的程序代码。

低级语言代码 Low-Level Code

低级语言代码,指由低级编程语言编写的代码,相对高级语言,少了更多的抽象概念,更加接近于汇编或者机器指令。但是这也意味着代码的可移植性很差。

在我看来,高与低,只是一组相对词而已。越高级的语言,性能、自由度越不及低级语言。但是在抽象、可读可写性、可移植性越比低级语言优秀。 在以前的年代,C/C++语言相对汇编语言,机器指令来说,肯定是高级语言。

而到了今天,我们更多人对C语言偏向认知为「低级语言」。 或许未来世界的开发者,看我们现在所熟悉的Java、PHP、Python、ECMAScript等等,都是「low」到爆的语言。

汇编语言 Assembly Language

汇编语言作为一门低级语言,对应于计算机或者其他可编程的硬件。它和计算机的体系结构以及机器指令是强关联的。换句话说,就是不同的汇编语言代码对应特定的硬件,所以不用谈可移植性了。

相对于需要编译和解释的高级语言代码来说,汇编代码只需要翻译成机器码就可以执行了。所以汇编语言也往往被称作象征性机器码(symbolic machine code)

字节码 Byte Code

字节码严格来说不算是编程语言,而是高级编程语言为了种种需求(可移植性、可传输性、预编译等)而产生的中间码(Intermediate Code)。它是由一堆指令集组成的代码,例如在 javac 编译过后的 java 源码产生的就是字节码。

源码在编译的过程中,是需要进行「词法分析 → 语法分析 → 生成目标代码」等过程的,在预编译的过程中,就完成这部分工作,生成字节码。 然后在后面交由解释器(这里通常指编程语言的虚拟机)解释执行,省去前面预编译的开销。

机器码 Machine Code

机器码是一组可以直接被 CPU 执行的指令集,每一条指令都代表一个特定的任务,或者是加载,或者是跳转,亦或是计算操作等等。所有可以直接被 CPU 执行的程序,都是由这么一系列的指令组成的。

机器码可是看作是编译过程中,最低级的代码,因外再往下就是交由硬件来执行了。 当然机器码也是可以被编辑的,但是以人类难以看懂的姿势存在,可读性非常差。

建立模糊的印象

如果要用一种现实生活中的职业来形容编译器的作用,我想翻译官是一个不错的选择。不论是同声传译,还是各个节目或者动漫的专业字幕组,反正只要能够把 A 语言流畅的翻译成 B 语言的都算。

但翻译的工作并不是那么简单,需要理解某种语言的文字,语法才能进行,当然更专业的人还能使用精简的句子传达意境。总之,这里的 ”翻译“ 其实不仅仅是翻译,还要再经过编辑,这也就就是 “compile“ ,编译的意思。

编译器 Compiler

在有了一个模糊的印象后,我们在聚焦到 compiler 上,compiler 就是计算机编程语言里的翻译官,不同的 compiler 会编译成不同的语言,有可能是转换成机器语言(machine code), byte code, 甚至是另外一种语言,如图:

image.png

最终产出的 target program 是能够被直接执行的,所以程序的编译到执行应该是这样的:

image.png

这种方式也叫做提前编译,Ahead-Of-Time Compilation(AOT),wiki 传送门:点我

直译器 Interpreter

还有另外一种语言处理的工具:直译器(Interpreter),相较于上图,compiler 是编译 source code 后产出可执行的代码,由使用者输入 input 后,再得到 output。而直译器是 source code 与 input 一起给出,直接编译并执行,产出 output,而使用直译器的语言有耳熟能详的 Python,它的架构如下:

image.png

另外 compiler 与 interpreter 在速度上也有一定的差异,compiler 产生的 target program 执行的比 interpreter 快。但 interpreter 的纠错能力又比较好,因为它是一行行的检查与执行程序中的代码。

关于 Interpreter,有的翻译叫做直译器,有的叫做解释器,wiki 传送门:点我)

编译器与直译器的异同

表现 Behavior

  • 编译器把源代码转换成其他的更低级的代码(例如二进制码、机器码),但是不会执行它。
  • 直译器会读取源代码,并且直接生成指令让计算机硬件执行,不会输出另外一种代码。

性能 Performance

  • 编译器会事先用比较多的时间把整个程序的源代码编译成另外一种代码,后者往往较前者更加接近机器码,所以执行的效率会更加高。时间是消耗在预编译的过程中。
  • 直译器会一行一行的读取源代码,解释,然后立即执行。这中间往往使用相对简单的词法分析、语法分析,压缩解释的时间,最后生成机器码,交由硬件执行。直译器适合比较低级的语言。但是相对于预编译好的代码,效率往往会更低。如何减少解释的次数和复杂性,是提高直译器效率的难题。

Compilation + Interpretation

再来,就势必要提及赫赫有名的 Java,为什么呢?Java 是一个结合 Compilation 和 Interpretation 的程序语言,这是什么意思呢?

就是 Java 会先编译成 byte code,接着再直译成机器码,这样的好吃是,Java 经历过一次编译,就可以通过虚拟机(Virtual machine)在不同的机器上直接执行。

沿用文章开始的翻译人员例子,byte code 就像是目前的通用国际语言 - 英语。只要将 A 语言翻译成英文,且 B 国人人能直接把英语翻译成自己的语言(当然前提是大家都会英文),此时,大家的交流就没有任何障碍了,整体的架构如下:

image.png

这种方式也叫做即时编译,Just-In-Time Compilation(JIT),wiki 传送门:点我

结合到实际

image.png

从左往右看,

  • 以 Java 为例,我们在文本编译器写好了 Java 代码,交由编译器编译成 Java Bytecode。然后 Bytecode 交由 JVM 来执行,这时候 JVM 充当了直译器的角色,在解释 Bytecode 成 Machine Code 的同时执行它,返回结果。
  • 以 BASIC 语言(早期的可以由计算机直译的语言) 为例,通过文本编译器编写好,不用经历编译的过程,就可以直接交由操作系统内部来进行解释然后执行。
  • 以 C 语言为例,我们在文本编译器编写好源代码,然后运行 gcc hello.c 编译出 hello.out 文件,该文件由一系列的机器指令组成的机器码,可以直接交由硬件来执行。

从抽象里看本质

无论是编译 (Compiler),还是直译 (Interpreter),甚至是即时编译。 本质还是人与计算机的交流形式,人的语言最终转换成机器语言。

一句 Hello World,经过一些列的编译和直译,最终转换成一系列包含机器指令的那些 0 和 1,机器傻傻执行完之后,告诉你结果。

就这么一个过程,我们就需要很多的翻译官。 有些翻译官可以做到同声传译(直译),有些翻译官却只能把我们的意图记下来再全部翻译(编译)给计算机。

而往往一个翻译官能力有限,也只能把你的语言,翻译成另外一种低级点的语言,再由另外懂这个语言的翻译官来翻译更接近计算机能读得懂的语言。

总结

这篇文章从一些与编译相关的常见概念说起,通俗的描述了编译原理范畴内的编译器与直译器:

  • 编译 Compile:把整个程序源代码翻译成另外一种代码,然后等待被执行,发生在运行之前,产物是另一份代码。
  • 直译 Interpret:把程序源代码一行一行的读懂然后执行,发生在运行时,产物是运行结果。

同时我们还用一些常见的计算机编程语言作为例子,浅显的解释了它们的编译过程。

希望通过这篇文章,你能对编译在计算机领域里扮演的角色和功能形成一个清晰的认知。