为什么C++编译速度比Java慢得多?
多种原因综合起来导致题主觉得C++编译起来比Java慢。
我平时工作的项目,用C++实现的JVM,编译一次要3分钟左右,我已经觉得挺快的了…
1. Java编译器只是个编译器前端
请参考我以前做的一个演示稿,第5到第12页:
Java Program in ActionJava程序的编译、加载与执行
其中,第7页我介绍了一个编译器的基本结构。流程图有两行,上面那行是“编译器前端”的组成部分,而下面那行则是“编译器后端”的组成部分。
(注1:现在编译器流行分为 [语言前端] -> [优化器] -> [后端] 的三部分,这里为了省事我把[优化器]与[后端]笼统叫做编译器后端。
注2:主要关注编译器前端技术的人可能会把我这里说的前端拆分为:
[前端:词法/语法分析] -> [中端:语义分析] -> [后端:中间代码生成] 的三部分,也就是说他们会认为语义分析与中间代码生成并不属于“前端”这只是视角的差异,并不是什么本质问题,什么东西都有输入/处理/输出三个部分,总是可以细分出自己领域中的“前端”“中端”“后端”。)
第12页我介绍了Oracle/Sun JDK对Java的实现。很明显,Java的源码级编译器(例如javac)只覆盖了传统编译流程中的前端部分,而其编译生成的Class文件里的字节码其实对应于传统编译器的“中间代码”(Intermediate Code,或者叫中间表示 Intermediate Representation)。
JVM实现可以选择把字节码进一步编译为机器码(也就是实现对应编译器后端的部分),或者解释执行字节码(也就是不实现编译器后端,而用解释器替代之),或者混合两者。
这方面讨论可以进一步跳传送门:Java字节码,ISA OR 中间表示? - RednaxelaFX 的回答
所以当题主比较C++与Java的编译速度时,如果Java一侧比较的是javac(或者ECJ之类的同级别编译器),那么比较的双方其实是
- 一个完整的C++编译器的速度
- 一个Java执行系统中的前端编译器的速度
C#与C++的编译速度相比的话也是同理,C#的源码级编译器只负责从源码编译到MSIL,只对应传统编译器的前端;而对应编译器后端的部分则在CLR的JIT编译器里,或者是例如.NET Native方案中的AOT编译器里。
2. C++的语言特性导致前端编译速度慢
不过就算拿C++编译器的前端跟Java源码级编译器来比速度,很可能还是Java编译器更快。这就是C++的语言特性所决定的了。
C++98/03里最伤前端编译速度的语言特性大概大家都知道:
- 预处理与条件编译
- 模版
- constexpr - 本质上要在C++编译器前端里自带一个C++子集的解释器
- 语法的二义性导致语法分析过程中要做一些语义分析来判定某个语法元素是类型名还是变量名
- 运算符重载以及用户自定义隐式类型转换使得name binding / resolution开销高
- auto与decltype - C++编译器前端本来就需要做类型检查,需要对每个声明和表达式做类型计算。auto和decltype只是让编译器把原本就需要计算出来的类型信息用作声明的一部分而已。
2.1 语法分析之前 - 预处理与条件编译
根据C++语言规范所提到的Phases of translation,从C++源码开始到真正能作语法分析总给得经过7个phase。当然实际实现可以尝试把这些phase尽可能混合在一起做,但这复杂度总归是在那里。C++ Compilation Speed - Walter Bright大大如是说。
其中有些功能是几乎没人用的,不小心用到还可能出问题的,例如trigraph sequence。
在到能做语法分析之前,大家平时用的最多而又耗时的功能大概是:
- #include - 继承自C的缺憾。到C++17,“模块”也还没有进入正式标准,而大家平时用#include最多的场景就是原本应该由“模块”解决的问题引入所依赖的库的声明。每个编译单元在#include一个文件时,都要让C++编译器前端把被#include的文件整个走一遍phase 1~4,不管里面的内容到底有没有用。这就非常蛋疼。(当然#include可以引入任意文本,所以也有各种神奇的花样玩法…那些这里先不讨论)
- 条件编译 - 本质上要写一个条件表达式解释器,而且重点是就算是应该跳过的文本块,编译器前端也还是要对它完整的扫描一次。
- 宏 - 要一直展开到末端的词法元素
2.2 语法分析之后 - 模版
到可以做语法分析后,最耗时间的事情就是模版实例化了。
C++的模版是图灵完备的。写一大堆复杂的模版,想让编译器跑多久都可以。
跟模版相关的类型推导也是一编译时间大户。
传送门:
还有人蛋疼的用C++模版写了图灵机实现…
另外,在C++规范没有Concepts的现状下,如果模版参数是类型,那么该模版参数的实际参数与形式参数之间其实是构成structural typing(而不是C++其它地方所用的nominal typing)但是却没有这个structural type自身的概念。
因而在实例化一个模版的时候,编译器可能会很傻的跑进去实例化的很大一部分之后才发现类型不匹配例如传入参数T的实际类型缺少了个begin()函数然后才报错。
配合上SFINAE这神奇的技巧,模版的编译速度就更慢了…
3. 感受一下C++编译器前端要做的事
对编译器不熟悉的同学可能会好奇:一个C++编译器的前端要做什么事呢?可以参考C++ Grandmaster Certification [CPPGM]给出的课程内容:
Assignment | Codename
Programming Assignment 1 | pptoken
Programming Assignment 2 | posttoken
Programming Assignment 3 | ctrlexpr
Programming Assignment 4 | macro
Programming Assignment 5 | preproc
Programming Assignment 6 | recog
Programming Assignment 7 | nsdecl
Programming Assignment 8 | nsinit
Programming Assignment 9 | cy86
CPPGM的课程I其实大部分都是关于C++编译器前端(的很小一部分…)。
上面的9个作业里,头5个其实都是预处理与词法分析相关的;第6个是很小一部分的语法分析,第7-8是很小一部分的语义分析,最后一个是个打酱油的代码生成。
对,这些作业都还没涉及到模版,只是在预处理和词法分析上就很费事了。
有兴趣的同学可以试试去做一下那些作业体验一下有多蛋疼 >_<
查看评论 回复