如何最小化编程语言编译时间?

时间:2009-02-23 06:29:36

标签: performance compiler-construction language-design

我正在考虑更多关于我正在设计的编程语言。我想知道,有什么方法可以最大限度地缩短编译时间?

17 个答案:

答案 0 :(得分:10)

今天你的主要问题是I / O.您的CPU比主内存快许多倍,内存比访问硬盘快1000倍。

因此,除非对源代码进行大量优化,否则CPU将花费大部分时间等待读取或写入数据。

请尝试以下规则:

  1. 设计编译器以便在多个独立的步骤中工作。目标是能够在不同的线程中运行每个步骤,以便您可以使用多核CPU。它还有助于并行化整个编译过程(即同时编译多个文件)

    它还允许您提前加载许多源文件并对其进行预处理,以便实际编译步骤可以更快地运行。

  2. 尝试允许独立编译文件。例如,为项目创建“缺少符号池”。缺少符号不应导致编译失败。如果您在某处找到丢失的符号,请将其从池中删除。编译完所有文件后,检查池是否为空。

  3. 创建包含重要信息的缓存。例如:文件X使用文件Y中的符号。这样,当Y更改时,您可以跳过编译文件Z(在Y中不引用任何内容)。如果您想更进一步,请将所有在池中定义的符号放在池中。如果文件以添加/删除符号的方式更改,您将立即知道哪些文件受到影响(甚至不打开它们)。

  4. 在后台编译。启动编译器进程,检查项目目录是否有更改,并在用户保存文件后立即编译它们。这样,您每次只需编译几个文件而不是一切。从长远来看,您将进行更多编译,但对于用户而言,周转时间将更短(=用户必须等到更改后才能运行编译结果的时间)。

  5. 使用“及时”编译器(即在使用文件时编译文件,例如在import语句中)。然后,项目以源代码形式分发,并在第一次运行时进行编译。 Python做到了这一点。要使其执行,您可以在安装编译器期间预编译库。

  6. 不要使用头文件。将所有信息保存在一个位置,并在必要时从源生成头文件。也许将头文件保存在内存中,永远不要将它们保存到磁盘中。

答案 1 :(得分:3)

  

我可以用什么方法最小化编译时间?

  • 无编译(解释性语言)
  • 延迟(及时)编译
  • 增量编译
  • 预编译的头文件

答案 2 :(得分:3)

我自己实现了一个编译器,一旦人们开始批量提供数百个源文件,最终不得不看一下。我发现的事情让我很惊讶。

事实证明,你可以优化的最重要的事情不是你的语法。它也不是你的词法分析器或你的解析器。相反,速度方面最重要的是从磁盘读取源文件的代码。磁盘的I / O是。真的很慢。您可以通过它执行的磁盘I / O数量来衡量编译器的速度。

所以事实证明,你可以做的最好的事情是加速编译器是将整个文件读入一个大的I / O内存,从RAM中执行所有的lexing,解析等,然后写在一个大的I / O中将结果输出到磁盘。

我与其中一位负责维护Gnat(GCC的Ada编译器)的人讨论了这个问题,他告诉我他实际上已经将所有可能的东西放到RAM磁盘上,这样即使他的文件I / O也只是RAM读取写道。

答案 3 :(得分:2)

在大多数语言中(除C ++之外的其他所有语言),编译单个编译单元的速度非常快。

绑定/链接通常很慢 - 链接器必须引用整个程序而不是单个单元。

除非你使用pImpl习语,否则C ++会受到影响 - 它需要每个对象的实现细节和所有内联函数来编译客户端代码。

Java(源到字节码)受到影响,因为语法不区分对象和类 - 您必须加载Foo类以查看Foo.Bar.Baz是否是由Bar静态字段引用的对象的Baz字段Foo类,或Foo.Bar类的静态字段。您可以在两者之间对Foo类的源进行更改,而不是更改客户端代码的源,但仍然必须重新编译客户端代码,因为字节码区分两种形式,即使语法不。 AFAIK Python字节码不区分两者 - 模块是其父母的真实成员。

如果包含的标头多于所需的标头,则C ++和C会受到影响,因为预处理器必须多次处理每个标头,并且编译器会对它们进行编译。最小化头大小和复杂性有帮助,建议更好的模块化将改善编译时间。并不总是可以缓存头编译,因为预处理头时存在的定义可以改变其语义,甚至语法。

如果你经常使用预处理器,那么会受到影响,但实际的编译速度很快;很多C代码使用typedef struct _X* X_ptr来比C ++更好地隐藏实现 - C头很容易包含typedef和函数声明,从而提供更好的封装。

所以我建议让你的语言隐藏客户端代码的实现细节,如果你是一个包含实例成员和命名空间的OO语言,请使用两种语言来明确访问这两种语言。允许使用真正的模块,因此客户端代码只需要知道接口而不是实现细节。不允许预处理器宏或其他变体机制改变引用模块的语义。

答案 4 :(得分:2)

以下是我们通过测量编译速度及其影响因素所学到的一些性能技巧:

  • 编写一个两遍编译器:字符为IR,IR为代码。 (编写三个 -pass编译器更容易编写字符 - > AST - > IR - >代码,但速度不是很快。)

  • 作为必然结果,没有优化器;编写快速优化器很难。

  • 考虑生成字节码而不是本机机器码。 Lua的虚拟机是一个很好的模型。

  • 尝试使用线性扫描寄存器分配器或Fraser和Hanson在lcc中使用的简单寄存器分配器。

  • 在简单的编译器中,词法分析通常是最大的性能瓶颈。如果您正在编写C或C ++代码,请使用re2c。如果你正在使用另一种语言(你会发现它更令人愉快),请阅读文章,并学习经验教训。

  • 使用maximal munch或iburg生成代码。

  • 令人惊讶的是,GNU汇编程序是许多编译器的瓶颈。如果可以直接生成二进制,请执行此操作。或者查看New Jersey Machine-Code Toolkit

  • 如上所述,设计您的语言以避免#include之类的内容。要么不使用接口文件,要么预先编译接口文件。这种策略大大减少了词法分析器的负担,正如我所说,这通常是最大的瓶颈。

答案 5 :(得分:1)

埃菲尔对冻结的不同状态有所了解,重新编译并不一定意味着整个班级都被重新编译。

你可以分解多少可兼容的模块,以及你需要多少跟踪它们?

答案 6 :(得分:1)

  • 使语法简单明了,因此可以快速轻松地解析。
  • 严格限制文件包含。
  • 尽可能允许编译时没有完整信息(例如,C和C ++中的预先声明)。
  • 如果可能的话,一次通过编译。

答案 7 :(得分:1)

这是一个镜头..

如果您的工具链支持,请使用增量编译。 (制作,视觉工作室等)。

例如,在GCC / make中,如果要编译许多文件,但只在一个文件中进行更改,则只编译该文件。

答案 8 :(得分:1)

到目前为止,答案中有一个令人惊讶的缺失:让你做一个无语境的语法等等。要好好看看Wirth设计的语言,比如Pascal& Modula-2的。您不必重新实现Pascal,但语法设计是为快速编译而定制的。然后看看你是否能找到任何关于Anders拉动实施Turbo Pascal的技巧的旧文章。提示:桌子驱动。

答案 9 :(得分:0)

在过去,您可以通过设置RAM驱动器并在那里进行编译来获得显着的加速。不过,不知道这是否仍然适用。

答案 10 :(得分:0)

在C ++中,您可以使用Incredibuild

等工具进行分布式编译

答案 11 :(得分:0)

一个简单的方法:确保编译器本身可以利用多核CPU。

答案 12 :(得分:0)

这取决于您编程的语言/平台。对于.NET开发,最小化解决方案中的项目数量。

答案 13 :(得分:0)

  • 确保在您尝试编译它时,可以编译所有内容。例如。禁止前瞻性参考。
  • 使用无上下文语法,以便找到没有符号表的正确解析树。
  • 确保语法可以从语法中推断出来,这样你就可以直接构造正确的AST,而不是通过解析树和符号表。

答案 14 :(得分:0)

这对编译器有多严重?

除非语法相当复杂,否则解析器的运行速度应该比仅通过输入文件字符索引的速度慢10-100倍。

同样,代码生成应受输出格式限制。

你不应该遇到任何性能问题,除非你正在做一个大型的,严​​肃的编译器,能够处理包含大量头文件的大型应用程序。

然后您需要担心预编译头,优化传递和链接。

答案 15 :(得分:0)

我没有看到为缩短编译时间做了很多工作。但是我想到了一些想法:

  1. 保持语法简单。复杂的语法会增加你的编译时间。
  2. 尝试使用多核GPU或CPU来实现并行性。
  3. 对现代编译器进行基准测试,看看有哪些瓶颈以及您可以用编译器/语言做些什么来避免它们。
  4. 除非您正在编写高度专业化的语言,否则编译时间并不是真正的问题。

答案 16 :(得分:0)

制作一个不吸吮的构建系统!

有大量的程序可能有3个源文件,需要花费一秒钟来编译,但在你走到那么远之前,你必须通过一个需要大约2分钟检查大小的大小的自动脚本文件。一个int。如果你在一分钟之后去编译别的东西,它会让你坐下几乎完全相同的一组测试。

因此,除非您的编译器 为用户做了一些糟糕的事情,例如更改其int的大小或更改运行之间的基本功能实现,只需将该信息转储到文件中即可让他们在一秒钟而不是2分钟内得到它。