传统编译器发展#
编译技术是计算机科学皇冠上的一颗明珠,作为基础软件中的核心技术,程序员的终极追求是能够掌握编译器相关的技术。
简单的说,编译器其实只是一段程序,它用来将编程语言 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% 保证开发者编写的程序代码从语法上是正确,因为哪怕有一点小小的错误,编译器会反馈错误的地方,便于开发者对自己编写的代码进行修改。
最后是编译器的几个重要的特点:
编译器读取源程序代码,输出可执行机器码,即把开发者编写的代码转换成 CPU 等硬件能理解的格式
将输入源程序转换为机器语言或低级语言,并在执行前并报告程序中出现的错误
编译的过程比较复杂,会消耗比较多的时间分析和处理开发者编写的程序代码
可执行结果,属于某种形式的特定于机器的二进制代码
历史发展#
编译器从计算机架构和驱动计算机架构刚开始发展到现在,历经了 60 余年,这段时间内编译器发展了很多代。先让目光回到 1957 年的第一个编译器 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)。
Frontend:主要负责词法和语法分析,将源代码转化为抽象语法树,即将程序划分为基本的组成部分,检查代码的语法、语义和语法,然后生成中间代码
Optimizer:优化器则是在前端的基础上,对得到的中间代码进行优化(如去掉冗余代码、子表达式消除等工作),使代码更加高效
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#
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。
但是操作系统是包括很多软件的,除了操作系统内核之外,还要有编辑器,编译器,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 原本使用 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。
正像名字所写的那样,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)