编译器是否利用多线程来加快编译时间?


16

如果我正确地记住了我的编译器课程,那么典型的编译器将具有以下简化的轮廓:

  • 词法分析器逐字符扫描(或调用某些扫描功能)源代码
  • 根据词素词典检查输入字符的字符串是否有效
  • 如果lexeme有效,则将其分类为与其对应的令牌
  • 解析器验证令牌组合的语法;逐个令牌

从理论上讲,将源代码分成四分之一(或其他分母)并在扫描和解析过程中使用多线程是否可行?是否存在利用多线程的编译器?




1
@RobertHarvey第一个链接的最高答案写道:“但是编译器本身仍然是单线程的。” 那不是吗?
8protons

我建议您阅读其余的答案,尤其是这个答案,以及我发布的第二个链接。
罗伯特·哈维

2
@RobertHarvey根据我的理解,您发布的第二个链接是关于生成生成已编译应用程序的多线程版本的编译器。这与编译器本身无关。感谢您分享资源,并抽出宝贵的时间回复。
8protons

Answers:


29

大型软件项目通常由许多编译单元组成,这些编译单元可以相对独立地进行编译,因此,通过并行多次调用编译器,通常可以以非常粗糙的粒度并行化编译。这发生在OS进程级别,并且由构建系统而不是适当的编译器进行协调。我意识到这不是您要的,但这是大多数编译器中最接近并行化的东西。

这是为什么?好吧,编译器所做的许多工作都不容易实现并行化:

  • 您不能仅将输入分成几个块并分别对其进行分类处理。为简单起见,您需要拆分lexme边界(这样就不会在lexme的中间开始任何线程),但是确定lexme边界可能需要大量上下文。例如,当您跳入文件中间时,必须确保没有跳入字符串文字。但是要检查这一点,您必须基本上查看之前出现的每个字符,这几乎与将其开始使用词法化功能差不多。此外,词法很少是现代语言编译器的瓶颈。
  • 解析更加难以并行化。拆分输入文本以进行词法分析的所有问题甚至更多地适用于拆分令牌以进行解析-例如,确定函数从何处开始与解析函数内容开始时一样困难。尽管可能也有解决的办法,但是对于小小的收益,它们可能会成比例地复杂。解析也不是最大的瓶颈。
  • 解析之后,通常需要执行名称解析,但这会导致庞大的交织关系网络。要在此处解析方法调用,您可能必须首先解析此模块中的导入,但这些导入需要解析另一个编译单元中的名称,依此类推。

在此之后,它变得稍微容易一些。类型检查和优化以及代码生成原则上可以在函数粒度上并行化。我仍然知道很少有编译器这样做,也许是因为同时执行如此大的任务非常具有挑战性。您还必须考虑到,大多数大型软件项目包含如此多的编译单元,以至于“并行运行一堆编译器”方法完全足以保持您所有的内核(在某些情况下甚至是整个服务器场)。另外,在大型编译任务中,磁盘I / O可能与实际编译一样成为瓶颈。

综上所述,我确实知道一个可以并行化代码生成和优化工作的编译器。Rust编译器可以将后端工作(LLVM,实际上包括传统上被认为是“中端”的代码优化)分配给多个线程。这称为“代码生成单元”。与上面讨论的其他并行化可能性相比,这是经济的,因为:

  1. 该语言具有相当大的编译单元(与之相比,例如C或Java),因此运行中的编译单元可能少于核心。
  2. 通常,要并行化的部分花费大量的编译时间。
  3. 后端工作在很大程度上令人尴尬地是并行的-仅优化并独立地将每个功能转换为机器代码。当然,存在过程间优化,而代码生成单元确实会阻碍这些过程,从而影响性能,但是没有任何语义问题。

2

编译是一个“令人尴尬的并行”问题。

没人在乎编译一个文件的时间。人们关心编译1000个文件的时间。对于1000个文件,处理器的每个内核可以一次愉快地编译一个文件,从而使所有内核完全处于繁忙状态。

提示:如果为make赋予了正确的命令行选项,则它会使用多个内核。否则,它将在16核心系统上编译另一个文件。这意味着您只需将构建选项更改一行就可以使其编译速度提高16倍。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.