为什么C++编译速度比Java慢得多?

编译一个cocos demo用了1分钟感觉好慢,为什么同样是编译型语言C++比Java编译速度慢
关注者
416
被浏览
22379

16 个回答

多种原因综合起来导致题主觉得C++编译起来比Java慢。
我平时工作的项目,用C++实现的JVM,编译一次要3分钟左右,我已经觉得挺快的了…

1. Java编译器只是个编译器前端

请参考我以前做的一个演示稿,第5到第12页:
Java Program in Action——Java程序的编译、加载与执行

其中,第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编译器里。

@陆明非 大大的回答说:
很多人一看到c++编译时间慢就高潮了,就开始从预处理,语法词法解析开始自以为是的分析,最后上升到抨击c++这门语言,正因为c++的语法复杂,所以才编译的这么慢!

我只想问你们一句话,决定编译时间的快慢的决定性因素是什么?

是优化!很多时候,优化占用的时间都在八九成,前端那些语法词法解析啥的,都是牛毛.那为什么优化要占用这么多时间?为了生成更高效的代码!如果你不想要高效代码,c++编译也可以很快.
我的回答的前半部分说的正是:“Java的编译速度”并没有算上优化的时间,而“C++的编译速度”通常是算上了优化时间的。

然后下面要说的就是即便不算优化,C++当前的语言特性仍然会让它比Java编译起来要更慢。

现在主流的Java源码编译器都是用Java写的,其实还算不上发挥出了“编译Java”的速度的极限。以前还有主流Java编译器是用C++实现的时候,编译得那个快的…
请跳传送门:微软当年的 J++ 究竟是什么?为什么 Sun 要告它? - RednaxelaFX 的回答,微软的jvc和IBM的Jikes都是用C++实现的Java编译器。

2. C++的语言特性导致前端编译速度慢

不过就算抛开优化不说,光拿C++编译器的前端跟Java源码级编译器来比速度,很可能还是Java编译器更快。这就是C++的语言特性所决定的了。Java再怎么说也只是(C++)--。
重点是:为了达到预期的用途,是否要选择某种设计并付出相应的代价;是否有别的设计可以更直接的、以更小开销解决问题,又或者说是否可以绕开不去解决那个问题。

C++98/03里最伤前端编译速度的语言特性大概大家都知道:
  • 预处理与条件编译
  • 模版
后续的C++11/14则有更多功能进一步消耗编译时间,例如:
  • 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可以引入任意文本,所以也有各种神奇的花样玩法…那些这里先不讨论)
  • 条件编译 - 本质上要写一个条件表达式解释器,而且重点是就算是应该跳过的文本块,编译器前端也还是要对它完整的扫描一次。
  • 宏 - 要一直展开到末端的词法元素

重点是:如果有了合适的模块实现,C++就再也不需要为模块的用途而使用#include,编译速度就可以大幅提升。目前消耗在#include上的编译时间纯属无谓的开销。

2.2 语法分析之后 - 模版

到可以做语法分析后,最耗时间的事情就是模版实例化了。
C++的模版是图灵完备的。写一大堆复杂的模版,想让编译器跑多久都可以。
跟模版相关的类型推导也是一编译时间大户。

传送门:
还有人蛋疼的用C++模版写了图灵机实现…

另外,在C++规范没有Concepts的现状下,如果模版参数是类型,那么该模版参数的实际参数与形式参数之间其实是构成structural typing(而不是C++其它地方所用的nominal typing)——但是却没有这个structural type自身的概念。
因而在实例化一个模版的时候,编译器可能会很傻的跑进去实例化的很大一部分之后才发现类型不匹配——例如传入参数T的实际类型缺少了个begin()函数——然后才报错。
配合上SFINAE这神奇的技巧,模版的编译速度就更慢了…

重点是:泛型(generics)是好的,编译时求值(compile-time evaluation)也是好的,但C++的模版目前在这两方面设计得都不够干净,达到同样的目的并不需要付出那么高的代价。
相比之下,D、Rust的泛型和编译时求值就都干净得多,直接了当的去达到目的。

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是很小一部分的语义分析,最后一个是个打酱油的代码生成。

对,这些作业都还没涉及到模版,只是在预处理和词法分析上就很费事了。
有兴趣的同学可以试试去做一下那些作业体验一下有多蛋疼 >_<
C++语法复杂,编译器负担很重,还有头文件和模板这种大山压顶,编译速度是主流语言里最慢的。

要提高编译速度,主要靠一些良好的习惯和技巧。一是组织好头文件,减少无谓的包含,尽量多用前向声明。二是开启预编译头,将一些基本不需要调整的头文件放到预编译头里。比如<windows.h>、<vector>、<string>。

刻意为了编译时间调整代码的实现方式,比如避免使用模板等就有些过了。不推荐这么做。