【问题标题】:How many times does a Common Lisp compiler recompile?Common Lisp 编译器要重新编译多少次?
【发布时间】:2015-02-23 21:40:01
【问题描述】:

虽然不是所有的 Common Lisp 实现都编译成机器代码,但其中一些实现了,包括 SBCL 和 CCL。

在 C/C++ 中,如果源文件不改变,C/C++ 编译器的二进制输出也不会改变,假设底层系统保持不变。

在 Common Lisp 编译器中,与 C/C++ 不同,编译不受用户直接控制。我的问题是,如果 Lisp 源文件没有改变,在什么情况下 CL 编译器会多次编译代码,为什么?如果可能,一个简单的说明性示例会有所帮助。

【问题讨论】:

  • 编译器不编译文件。它编译代码。例如,您可以输入 REPL (compile nil (list 'lambda (list 'x) (list '+ 'x 'x)))。根本不涉及任何文件。
  • @JoshuaTaylor 仍然可以将代码放入文件中,然后从那里编译。
  • 当然可以(见我的回答),但在这种情况下,会从文件中读取一些代码,然后将编译后的内容写入另一个文件,然后再加载该文件。
  • @JoshuaTaylor 好的,如果问题足够不妥,那么删除它可能是个好主意。您对此有何看法?
  • 我不一定认为这是一个不好的问题;错误的假设是我们过去都做过的事情,许多人对 Common Lisp 中的编译工作方式有一些误解。我认为这个问题及其答案可能对未来的人们有用。

标签: compilation common-lisp


【解决方案1】:

我认为这个问题是基于一些误解。编译器不编译文件,这不是用户无法控制的。编译器很容易通过compile 函数获得。编译器在代码上运行,而不是在文件上运行。例如,您可以在 REPL 中键入

CL-USER> (compile nil (list 'lambda (list 'x) (list '+ 'x 'x)))
#<FUNCTION (LAMBDA (X)) {100460E24B}>
NIL
NIL

根本不涉及任何文件。不过还有一个compile-file函数,但是注意它的描述是:

compile-file 转换指定文件的内容 将输入文件转换为与实现相关的二进制数据,这些数据被放置 在 output-file 指定的文件中。

文件的内容被编译。那么那个编译文件可以是loaded。 (您也可以 load 未编译的源文件。)我认为您的问题可能归结为询问在什么情况下 compile-file 会生成具有不同内容的文件。我认为这真的取决于实现,而且它并不是真正可预测的。我不知道您对其他语言的编译器的描述是否必然成立:

在 C/C++ 中,如果源文件不变,则 C/C++ 编译器也不会改变,假设底层系统 保持不变。

如果编译器碰巧在某个数据段的输出中包含时间戳怎么办?然后你每次都会得到不同的二进制输出。确实,一些常见的脚本编译/构建系统(例如,ma​​ke 和类似系统)会根据输入文件在此期间是否发生变化来检查是否可以重用之前的输出。不过,这并没有真正说明 编译器 的作用。

【讨论】:

  • 其实是文件编译器编译文件。 COMPILE 只编译单个函数。请参阅文件编译器的 CLHS 文档。
  • 是的,有一个文件编译器,在3.2.3 File Compilation 中描述。我特别提到了 compile-file 来指出是的,我们通常会“编译文件”,实际上,编译的不是文件,而是读取的 forms从文件中。大多数语言确实如此,但内部结构在 Common Lisp 中暴露得更多。
  • 对,关键是很多表单在文件编译器处理时都有特殊含义,而很多表单不能用COMPILE以类似的有意义的方式编译。因此,文件编译器不仅仅是阅读表单并一一编译它们。还有更多。编译器可能是创建中间文件的多通道编译器,它可能在读取所有表单后对表单进行优化等。一些编译器操作具有文件范围等。
【解决方案2】:

规则几乎相同,但在 Common Lisp 中,将声明与实现分开不是一种做法,因此通常您必须重新编译每个依赖项才能确定。这是动态环境的共同实际结果。

假设存在这样的分离,以下是需要重新编译特定依赖文件的明显示例(显然并非详尽无遗),因为输出可能会有所不同:

  • 更改的包定义
  • 更改的宏字符或其代码发生更改
  • 已更改的宏
  • 添加或删除 inlinenotinline 声明
  • 全局类型或函数类型声明的更改
  • #.defvardefparameterdefconstantload-time-valueeql 专门人员、make-load-form 生成的代码、defmacro 等中使用的更改函数(例如 setf 扩展程序)。 ..
  • Lisp 编译器或基础映像中的更改

我的意思是,您可以看到确定哪些文件需要重新编译并非易事。有时,答案是“所有后续文件”,例如更改"(双引号)宏字符,这可能会影响每个文字字符串,或者编译器以非向后兼容的方式发展。从本质上讲,我们从开始的地方结束:您只能确保完全重新编译,而不是跨编译重用 fasls。有时它比确定需要重新编译的最小文件集更快。

在实践中,您最终会在开发过程中大量编译单个定义(例如使用 Slime),而不是当 fasl 与源文件一样旧或更年轻时重新编译文件。很多时候,您重用来自例如的文件。快点。但对于测试和部署,我建议清除所有 fasls 并重新编译所有内容。

已经努力使用 SBCL 自动化最小依赖项编译,但我认为当您更频繁地更改临时项目时它太慢了(它涉及大量分叉,因此在 Windows 中它要么不可行要么非常慢)。但是,对于很少更改(如果有的话)的基础库,这可能会节省时间。

另一种方法是使用内置的基本库(即您始终加载的那些)制作自定义基本映像。它将节省编译和加载时间。

【讨论】:

  • 嗯,你认为 ASDF 和类似的东西是干什么用的? :depends-on:in-order-tocommon-lisp.net/project/asdf/asdf/… Lisp 项目使用像 ASDF 这样的构建工具来描述文件之间的依赖关系,并允许最小化编译/加载活动。
  • "需要重新编译特定依赖文件的更改,因为输出可能不同" 确实,此类更改需要重新编译,尽管不是必要整个文件。但是,问题中有一个(不正确的)前提:在 Common Lisp 编译器中,与 C/C++ 不同,编译不受用户直接控制。 但是,这种重新编译都不是自动的。 Lisp 系统不会监视它曾经编译的源文件的更改,以便在文件更改时重新编译它。当这些事情发生变化时,仍然由用户重新编译。
  • 这正是我的观点,即使是关于@RainerJoswig 提到的 ASDF。在 C/C++ 世界中,当您谈论最小化所需的编译时,它归结为预编译的头文件和特定的 .c/.cpp 文件。在 Lisp 中,您必须重新编译依赖项目,这是通常的粒度。另请注意,这是关于文件编译的,我不知道有任何足够聪明的实现只重新编译某些顶级表达式(那真的是真的聪明)。
  • @PauloMadeira:不一定是依赖项目。 ASDF 让我为这些文件依赖项组指定依赖文件和编译策略。然后,这样的工具计划进行最小编译。然后我有包定义、基本实用程序、基本宏、通用代码等文件。例如,请参阅 McCLIM 的 ASDF 声明...
  • 是的,但是现在想一想一个项目是否直接依赖于 McCLIM,如果 McCLIM 发生变化,你能不能让它只重新编译需要的文件?
猜你喜欢
  • 2011-01-26
  • 2012-07-31
  • 1970-01-01
  • 1970-01-01
  • 2014-07-12
  • 1970-01-01
  • 2013-06-03
  • 1970-01-01
  • 2011-03-30
相关资源
最近更新 更多