传统编译器发展#

编译技术是计算机科学皇冠上的一颗明珠,作为基础软件中的核心技术,程序员的终极追求是能够掌握编译器相关的技术。

简单的说,编译器其实只是一段程序,它用来将编程语言 A 翻译成另外-种编程语言 B,上面将源代码翻译为目标代码的过程是叫作编译(compile)。完整的编译过程通常包含词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成等步骤。我们很难想象,在没有出现编译器的时候,程序员编程是有多么的困难。

在本节内容里面,由于 AI 系统中大量地使用了传统编译器中的概念和内容,本节我们将会去了解传统编译器的发展。想要深入了解编译器的内容也参考以下经典材料。

编译器经典教材

基础介绍#

出现原因#

我们平时所说的程序,是指双击后或者执行命令行后,就可以直接运行的程序,这样的程序被称为可执行程序(Executable Program)。在 Windows 下,可执行程序的后缀有 .exe.com;在类 UNIX 系统(Linux、Mac OS 等)下,可执行程序没有特定的后缀,系统根据文件的头部信息来判断是否是可执行程序。

可执行程序的内部是一系列计算机指令和数据的集合,它们都是二进制形式的,CPU 可以直接识别,毫无障碍;但是对于程序员,它们非常晦涩,难以记忆和使用。

例如,在屏幕上输出一句话,Python 语言的写法为:

def print_id(comment):
    print(comment)

print_id("我是 ZOMI")

二进制的写法就变得非常复杂,看到都想撞墙:

二进制示例

在 1950 年代开始,计算机发展的初期,程序员就是使用二进制指令来编写程序,当时候除了缺乏编译器也缺乏良好的编程语言。当程序比较大的时候,不但编写麻烦,需要频繁查询指令手册,而且 Debug 会异常苦恼,要直接面对二进制数据,让人眼花缭乱。另外,用二进制指令编程步骤繁琐,要考虑各种边界情况和底层问题,开发效率十分低下。

这就迫使程序员开发出了编程语言,提高程序开发的效率,例如汇编、C 语言、C++、Java、Python、Go 语言等,都是在逐步提高程序的开发效率。至此,编程终于不再是只有极客能做的事情了,不了解计算机的读者经过一定的训练也可以编写出有模有样的程序。

因此,编译器跟编程语言的发展是相辅相成的,有了高级编程语言,通过编译器能够翻译成低级的指令或者二进制机器码。

什么是编译器#

典型的编译型程序语言有 C 和 C++,以 C 语言为例:C 语言代码由固定的词汇按照固定的格式组织起来,简单直观,程序员容易识别和理解,但是对于 CPU,只认识有限的二进制形式的指令。这时候就需要一个程序工具,将 C 语言代码转换成 CPU 能够识别的二进制指令。这个工具是一个特殊的软件程序,叫做编译器(Compiler)。

编译器可以将整个程序转换为目标代码(object code),这些目标代码通常存储在文件中。目标代码也被称为二进制代码,在进行链接后可以被机器直接执行。

编译器示例

编译器能够识别高级语言程序代码中的词汇、句子以及各种特定的格式和数据结构,并将其转换成机器能够识别的二进制码,这个过程称为编译(Compile)

编译的过程,类似于将中文翻译成英文、将英文翻译成象形文字。它是一个复杂的软件执行过程,大致包括词法分析、语法分析、语义分析、性能优化、生成可执行文件等多个步骤,期间涉及到复杂的算法和硬件架构。

在 C 语言的编译器有很多种,不同的平台下有不同的编译器,例如:

  • Windows:常用的是微软编译器(cl.exr),被集成在 Visual Studio 或 Visual C++ 中,一般不单独使用;

  • Linux:常用 GUN 组织开发的 GCC,很多 Linux 发行版都自带 GCC;

  • Mac:常用的是 LLVM/Clang,被集成在 Xcode 中

代码语法正确与否是由编译器来检查,即编译器可以 100% 保证开发者编写的程序代码从语法上是正确,因为哪怕有一点小小的错误,编译器会反馈错误的地方,便于开发者对自己编写的代码进行修改。

最后是编译器的几个重要的特点

  1. 编译器读取源程序代码,输出可执行机器码,即把开发者编写的代码转换成 CPU 等硬件能理解的格式

  2. 将输入源程序转换为机器语言或低级语言,并在执行前并报告程序中出现的错误

  3. 编译的过程比较复杂,会消耗比较多的时间分析和处理开发者编写的程序代码

  4. 可执行结果,属于某种形式的特定于机器的二进制代码

历史发展#

编译器从计算机架构和驱动计算机架构刚开始发展到现在,历经了 60 余年,这段时间内编译器发展了很多代。先让目光回到 1957 年的第一个编译器 IBM Fortran,其早期在计算机领域属于一项了不起的技术。它的起源和取得的成果,付出的巨大努力是今天很多开发者都无法想象的。

IBM Fortran 发明者合照

1955 年 IBM 想要销售让更多的人能够进行编程的计算机。但是当时编程是用汇编语言完成的,IBM 的工程师很清楚编程对于大多开发者来说太难了。于是蓝色巨人希望在不牺牲性能的前提下,给开发者提供更快、更方便地编写程序的方式。发明 Fortran 计算机语言的科学家,希望利用高级编程语言编写的程序,提供尽可能接近手动调整的机器代码的性能。

对于编译器来说,需要重点思考一下三个点:性能、生产力和可移植性。

性能上,Fortran 的发明者拿机器代码的性能作了比较;生产力方面的好处是,开发者再也不必编写机器代码。在 1957 年无法为软件申请专利权,IBM 也没有禁止其他企业和组织实现 Fortran。因此过了一段时间后,市面上出现了来自其他企业和组织面向其他机器的 Fortran 编译器。这立即为 Fortran 程序创造了机器代码无法想象的可移植性生态。

从这时开始,编译器开始迅猛发展起来。下面一起回顾编译器发展情况,编译器与编程语言几乎是同步发展起来的,发展过程可以分为几个阶段:

  • 第一阶段:20 世纪 50 年代,出现了第一个编译程序,将算术公式翻译成机器代码,为高级语言的发展奠定了基础。

  • 第二阶段:20 世纪 60 年代,出现多种高级语言和相应的编译器,如 Fortran、COBOL、LISP、ALGOL 等,编译技术也逐渐成熟和规范化

  • 第三阶段:20 世纪 70 年代,出现了结构化程序设计方法和模块化编程思想,以及面向对象的语言和编译器,如 Pascal、C、Simula 等,编译技术也开始注重工程代码的可读性和可维护性。

  • 第四阶段是 20 世纪 80 年代,出现了并行计算机和分布式系统,以及支持并行和分布式的语言和编译器,如 Ada、Prolog、ML 等,编译技术也开始考虑程序的并行和分布能力。

  • 第五阶段:20 世纪 90 年代,出现了互联网和移动设备等新兴平台,以及支持跨平台和动态特性的语言和编译器,如 Java、C#、Python 等,编译技术也开始关注程序的安全性和效率。

  • 第六阶段:21 世纪第一个 10 年,出现了以 Lua 为首的 Torch 框架,用于解决爆炸式涌现的 AI 应用和 AI 算法研究,之后又推出 TensorFlow、PyTorch、MindSpore、Paddle 等 AI 框架,随着 AI 框架和 AI 产业的发展,出现了如 AKG、MLIR 等 AI 编译器。

历史涌现的编译器

在发展过程中也伴随着编译理论体系的逐步成熟,一些关键也成为了实现编译器必不可少的部分,如:有限状态自动机、上下文无关文法、属性文法等。

基本构成#

目前主流如 LLVM 和 GCC 等经典的开源编译器,通常分为三个部分,前端(Frontend),优化器(Optimizer)和后端(Backend)。

  1. Frontend:主要负责词法和语法分析,将源代码转化为抽象语法树,即将程序划分为基本的组成部分,检查代码的语法、语义和语法,然后生成中间代码

  2. Optimizer:优化器则是在前端的基础上,对得到的中间代码进行优化(如去掉冗余代码、子表达式消除等工作),使代码更加高效

  3. Backend:后端则是将已经优化的中间代码,针对具体的硬件生成目标代码,转换成为包括代码优化器和代码生成器

编译器

编译体系#

在 21 世纪后仍然有许多新兴的编程语言,如函数式编程语言 Haskell,优秀的内存安全性与性能的 Rust,用于分布式微服务的 Go,基于 JVM 平台的 Android 官方开发语言 Kotlin,Apple 平台的新编程语言 Swift 等。

这些新的编程语言设计之初都有着不同的侧重点,并且相比传统的编程语言拥有着更加优秀的专业领域表达能力,伴随着也拥有着更完善的编译体系,比如有强大的 lint 静态分析工具,以及更出色的编译期优化。优秀的语言与编译套件,能大大提升在专业领域的软件开发效率。

基础设施#

新兴编程语言的快速发展少不了基础设施的逐步完善。如 LLVM (Low Level Virtual Machine) 的出现,可以让任意编程语言前端编译到一个 LLVM 的中间表示(IR),再由 LLVM 中的后端编译至具体硬件平台,并且可以分不同阶段实现优化。

LLVM 极大地简化了编程语言编译器的开发过程,不同语言只需要实现语言到 LLVM IR 的前端编译程序,再调用 LLVM 后端编译器,就可以得到编译至任意平台的能力,而无需为不同的平台实现不同的编译器。如 Rust 和 Swift 语言的编译器就使用了 LLVM 作为后端。

不过随着时代发展,其他语言也能通过 GCC(GNU Compiler Collection)的 IR 来实现最终编译阶段,目前 Rust 社区也在积极尝试使用 GCC 作为后端的编译器,以使用 GCC 的优化和平台支持。

值得一提的是,正如如上文所述,LLVM 和 GCC 如今已不再是某个具体的编译工具,而已然成为了一套编译基础设施。LLVM 和 GCC 不仅提供了一系列编译器,也主要提供了一些 C/C++ 语言相关配套工具,如 LLVM 的 Clang 工具链(包含 Clang-tidy、Clang-format)。

此外配套的一些语言的分析工具:

  • 代码格式化工具:自动格式化代码,使代码符合固定格式,提高代码可读性。

  • 静态代码分析工具:编译期间运行,来检测出代码中的问题和漏洞。如 Clang-tidy、rust-clippy、Clangd(LSP)、rust-analyzer(LSP)

  • 动态代码分析工具:运行时分析,比静态分析更能发现一些潜在的漏洞,诸如 C/C++ 的内存检测工具,用于检查内存泄露以及异常内存使用并能返回问题代码位置。

虚拟机与优化#

一些高级编程语言(如 Java、Python、JavaScript)的运行,依赖于运行时(Runtime),并常常带有虚拟机(VM)和解释器。

这些语言有的作为脚本语言不需要编译,或者是可编译为跨平台的字节码。语言性能通常较静态且直接编译为机器码的语言低许多,原因也是很明显的,因为其需要在运行时先解释代码再执行。不过如今也有许多技术手段能够提升这些语言的性能。下文将主要以 JVM 平台调优举例说明:

  • JIT(Just In Time):即时编译,在程序运行时将源代码或字节码编译成机器码,提高执行效率。使用 JIT 可以避免热点代码的重复解释,虚拟机可以在运行时实施动态优化,检测并将热点代码编译成机器码。缺点是会增加启动时间和内存占用。

  • AOT(Ahead Of Time):预编译,运行前将代码编译成机器码,获得更少的启动时间和内存占用以及更接近原生的性能,甚至不再需要依赖于虚拟机。但是随之而来缺陷是,失去了一次编译,处处使用的跨平台特性,以及一些语言的动态特性。

  • 虚拟机优化:OpenJDK 有许多不同的实现,不同的实现也有着不同的性能,其中比如 Oracle 的 HotSpot JVM(with GraalVM’s advanced JIT optimizing compiler) 对 JIT 有额外的优化编译器。

集成环境#

实际开发中,除了编译器是必须的工具,往往还需要很多其他辅助软件,例如:

  • 编辑器:用来编写代码,并且给代码着色,以方便阅读;

  • 代码提示器:输入部分代码,即可提示全部代码,加速代码的编写过程;

  • 调试器:观察程序的每一个运行步骤,发现程序的逻辑错误;

  • 管理工具:对程序涉及到的所有资源进行管理,包括源文件、图片、视频、第三方库等;

  • 开发的界面:各种按钮、面板、菜单、窗口等控件整齐排布,操作更方便。

这些工具通常被打包在一起,统一发布和安装,例如 Visual Studio、Dev C++、Xcode、Visual C++ 6.0、C-Free、Code::Blocks 等,统称为集成开发环境(IDE,Integrated Development Environment)

集成开发环境就是一系列开发工具的组合套装。这就好比台式机,一个台式机的核心部件是主机,有了主机就能独立工作了,但是在购买台式机时,往往还要附带上显示器、键盘、鼠标、U 盘、摄像头等外围设备。集成开发环境也是相同道理,只有编译器不方便,所以还要增加其他的辅助工具。

传统编译器之争#

GCC、LLVM 和 Clang 都是常见的编译器,用于将高级语言代码转换为机器语言代码。它们在编译速度、优化能力和支持的语言等方面有所不同,曾经很长一段时间,开发者都会在争论到底哪个开源编译器更好。

GCC 与 LLVM

GCC#

GCC(GNU Compiler Collection,GNU 编译器套装),是一套由 GNU 开发的编程语言编译器。它是一套以 GPL 及 LGPL 许可证所发布的自由软件,也是 GNU 课程的关键部分,亦是自由的类 Unix 及苹果电脑 Mac OS X 操作系统的标准编译器。GCC(特别是其中的 C 语言编译器)也常被认为是跨平台编译器的事实标准。

上面提到的 GNU 名称来自 Gnu's Not Unix"的缩写,一个类 UNIX 的操作系统,由 GNU 计划推动,目标在于创建一个完全兼容于 UNIX 的自由软件环境。由于 UNIX 系统是商业收费软件,而且有一部分源码是没有开放的,所以在 1983 年,理查德·斯托曼提出 GN 计划,希望发展出一套完整的开放源代码操作系统来取代 Unix,计划中的操作系统,名为 GNU。

GUN

但是操作系统是包括很多软件的,除了操作系统内核之外,还要有编辑器,编译器,shell 等等一些软件来支持。

GNU 工程十几年以来已经成为一个对软件开发主要的影响力量,创造了无数的重要的工具,例如:GCC 编译器,甚至一个全功能的 Linux 操作系统。GNU 计划采用了部分当时已经可自由使用的软件,例如 TeX 排版系统和 X Window 视窗系统等。不过 GNU 计划也开发了大批其他的自由软件,这些软件也被移植到其他操作系统平台上,例如 Microsoft Windows、 BSD 家族、 Solaris 及 Mac OS。

GCC 作为 GNU 工程的其中一个课程,原名为 GNU C 语言编译器(GNU C Compiler),因为它原本只能处理 C 语言。GCC 很快地扩展,变得可处理 C++。之后也变得可处理 Fortran、Pascal、Objective-C、Java、Ada,以及 Go 与其他语言。

GCC

GCC 原本使用 C 开发,后来因为 LLVM、 Clang 的崛起,令 GCC 更快将开发语言转换为 C++。许多 C 的爱好者在对 C++ 一知半解的情况下主观认定 C++ 的性能一定会输给 C,但是 Taylor 给出了不同的意见,并表明 C++ 不但性能不输给 C,而且能设计出更好,更容易维护的程序。

由于 GCC 已成为 GNU 系统的官方编译器(包括 GNU/Linux 家族),它也成为编译与创建其他操作系统的主要编译器,包括 BSD 家族、Mac OS X、NeXTSTEP 与 BeOS。

GCC 通常是跨平台软件的编译器首选。有别于一般局限于特定系统与运行环境的编译器,GCC 在所有平台上都使用同一个前端处理程序,产生一样的中介码,因此此中介码在各个其他平台上使用 GCC 编译,有很大的机会可得到正确无误的输出程序。

  • 总结

GNU 计划本来是为了开发一个自由系统来取代 UNIX 的,但是由于开发的内核 hurd 一直不怎么样,这个系统至今都没出稳定版本,然而 GNU 计划中开发的其他一些自由软件,比如 GCC 编译器,却非常的好,在移植到各大操作系统上一直广泛使用至今。

Clang#

Clang 是一个 C、 C++、 Objective-C 和 Objective-C++ 编程语言的编译器前端。它采用了底层虚拟机(LLVM)作为其后端。

Clang 课程在 2005 年由苹果电脑发起,是 LLVM 编译器工具集的前端(front-end),目的是输出代码对应的抽象语法树(Abstract Syntax Tree, AST),并将代码编译成 LLVM Bitcode。接着在后端(back-end)使用 LLVM 编译成平台相关的机器语言。它的目标是提供一个 GNU 编译器套装(GCC)的替代品。Clang 课程包括 Clang 前端和 Clang 静态分析器等。

Clang 本身性能优异,其生成的 AST 所耗用掉的内存仅仅是 GCC 的 20% 左右。FreeBSD 10 将 Clang/LLVM 作为默认编译器。测试证明 Clang 编译 Objective-C 代码时速度为 GCC 的 3 倍,还能针对用户发生的编译错误准确地给出建议。

  • Clang 历史

Apple 吸收 Chris Lattner 的目的要比改进 GCC 代码优化宏大得多,GCC 系统庞大而笨重,而 Apple 在 MAC 系统大量使用的 Objective-C 在 GCC 的课程支持优先级中比较低。此外 GCC 作为一个纯粹的编译系统,与 IDE 配合得很差。

加之许可证方面的要求,Apple 无法使用 LLVM 继续改进 GCC 的代码质量。于是,Apple 决定从零开始写 C、C++、Objective-C 语言的前端 Clang,完全替代掉 GCC。

苹果开发 LLVM 与 Clang

正像名字所写的那样,Clang 只支持 C,C++ 和 Objective-C 三种 C 家族语言。2007 年开始开发,C 编译器最早完成,而由于 Objective-C 相对简单,只是 C 语言的一个简单扩展,很多情况下甚至可以等价地改写为 C 语言对 Objective-C 运行库的函数调用,因此在 2009 年时,已经完全可以用于生产环境。C++ 的支持也热火朝天地进行着。

  • 总结

GCC 目前作为跨平台编译器来说它的兼容性无异是最强的,兼容最强肯定是以牺牲一定的性能为基础,苹果为了提高性能,因此专门针对 mac 系统开发了专用的编译器 Clang 与 LLVM,Clang 用于编译器前段,LLVM 用于后端。

LLVM#

LLVM (Low Level Virtual Machine,底层虚拟机) 提供了与编译器相关的支持,能够进行程序语言的编译期优化、链接优化、在线编译优化、代码生成。简而言之,可以作为多种编译器的后台来使用。

LLVM 作为一个编译器的基础建设,它是为了任意一种编程语言写成的程序,利用虚拟技术,创造出编译时期,链结时期,运行时期以及“闲置时期”的优化。

  • LLVM 历史

Apple 一直使用 GCC 作为官方的编译器。GCC 作为开源世界的编译器标准一直做得不错,但 Apple 对编译工具会提出更高的要求:

一方面,Apple 对 Objective-C 语言(甚至后来对 C 语言)新增很多特性,但 GCC 开发者对 Apple 使用 Objective-C 的支持度较低。因此 Apple 基于 GCC 某个版本开始,分成两条分支分别开发,这也造成 Apple 的编译器版本远落后于 GCC 的官方版本。

另一方面,GCC 的代码耦合度太高,不好独立,而且越是后期的版本,代码质量越差,但 Apple 想做的很多功能(比如更好的 IDE 支持)需要模块化的方式来调用 GCC,但 GCC 一直没有实现,从根本上限制了 LLVM-GCC 的开发。

所以,这种不和让 Apple 一直在寻找一个高效的、模块化的、协议更放松的开源替代品,于是 Apple 请来了编译器高材生 Chris Lattner,主持实现 LLVM 课程。

编译器

  • 总结

因为 GCC 的编译器已经慢慢无法满足苹果的需求,因此苹果开发了 Clang 与 LLVM 来完全取代 GCC。Xcode4 之后,苹果的默认编译器采用 Clang 作为编译器前端,LLVM 作为编译器后端。

差异对比#

下面来通过不同的维度来比较一下两大编译器巨头 GCC 和 Clang:

  • 开源软件:众所周知,GCC 和 Clang 都是免费的开源软件。但是他们的许可授权很不一样。GCC 是参照 GPL(GNU 公共许可证)授权的,而 Clang/LLVM 是 Apache 许可授权的。比较 GCC 和 Clang 的许可授权,最专业的是律师。

  • 支持平台:GCC 和 Clang 都支持几乎所有的平台。Clang/LLVM 可在 Windows 本机上进行编译,而 GCC 则需要 MinGW 这样的子系统,才能与 Windows 兼容。这样比较 Clang 和 GCC 是不公平的,因为 GCC 没有在本地支持 Windows 的计划。

  • 代码复杂度:GCC 是一个非常复杂的软件,有超过 1500 万行代码。尽管其前/后端定义清晰明了,但软件在本质上更为单一。对比 GCC,Clang 更多的是模块化架构,具有定义良好的扩展点。

  • 标准支持:对 C++ 20,即最新推出的 C++ 版本,GCC 已通过测试。另外,它也完全符合 C++ 17 以及最新的 C 语言标准,C17。Clang 完全符合 C++ 17 标准,也将很快跟进 C++ 20 标准。

  • 高效代码生成:Clang 和 GCC 的代码生成,在空间和时间的复杂度旗鼓相当。因此这种比较毫无意义,因为这两个编译优化工具都基于一种严密的静态分配形式。 语言独立的类型系统——在这个标题下对比 Clang 与 GCC 很有意义。由于 Clang/LLVM 对所有兼容语言都使用语言独立的类型系统,因此可以确定指令的确切语义。GCC 则没有语言独立类型系统的设计目标。

  • 前端解析器:GCC 以前有基于 Bison 的 LR 解析器,后来转向了手写递归下降解析器。Clang 一直使用手写的确定性递归下降解析器,且可回溯。

  • 后端链接器:GCC 与 Clang 的差异在这个层面最为明显。GCC 使用 ld 作为链接器,支持 ld-gold。Clang 使用 lld 作为链接器。通过一些基准测试,可以看出 lld 比 ld 甚至最新的 ld-gold 都快。

  • 构建工具:Clang 与 GCC 的另一个大的区别。GCC 使用 Autotools 和 Make 作为构建工具,而 Clang/LLVM 使用 CMake。

  • 调试支持:GCC 有一个优秀的 GDB 调试器。GDB 历经时间考验,性能优异。Clang 则将 LLDB 调试器构建为 LLVM 上的一组可重用组件。

GCC 是一个功能强大的编译器集合,支持多种编程语言,广泛应用于各种开源课程和商业软件。LLVM 是一个灵活的编译器基础设施,提供了通用的编译器工具和库,被用于构建自定义编译器。

Clang 是基于 LLVM 的主要支持 C、C++、Objective-C 和 Objective-C++ 编译器,具有快速的编译速度和低内存占用,Clang 的底层框架 LLVM 具有足够的可扩展性,可以支持 Julia 和 Swift 等较新的语言。

以下的表格简单梳理了两个工具的差异:

类型

GCC

Clang/LLVM

许可证

GNU GPL

Apache 2.0

代码模块化

一体化架构

模块化

支持平台

Uinx, Windows、MAC

Uinx、MAC

代码生成

高效,有很多编译器选项可以使用

高效,LLVM 后端使用了 SSA 表单

语言独立类型系统

没有

构建工具

Make Base

CMake

解析器

最早采用 Bison LR,现在改为递归下解析器

手写的递归下降解析器

链接器

LD

lld

调试器

GDB

LLDB

未来发展趋势#

根据现代语言发展的情况来看,跨越不同编程语言之间的鸿沟变得越来越小,语言也逐渐在一些方面趋同。

比如如今在 JVM 平台上可以运行多种语言,甚至可以通过使用 GraalVM Compiler 对接 LLVM,使得 C/C++、Rust 代码得以在 JVM 上运行,实现多语言之间无缝调用。

反之,在底层平台逐步扩大的同时,上层的语言也变得能够扩张到任何平台,比如 Java、Kotlin 最初在 JVM 上运行,到可以编译为原生机器码或者 WASM 在 WebAssembly 平台上运行。

小结与思考#

  • 编译技术是计算机科学皇冠上的一颗明珠,作为基础软件中的核心技术

  • 编译器能够识别高级语言程序代码中的词汇、句子以及各种特定的格式和数据结构

  • 编译过程,是将源代码程序转换成机器能够识别的二进制码

  • 传统编译器通常分为三个部分,前端(frontEnd),优化器(Optimizer)和后端(backEnd)

本节视频#