将语言编译为C是一个好主意吗?

时间:2012-01-23 18:08:01

标签: c gcc compiler-construction ghc

在整个网络上,我感觉为编译器编写C后端不再是一个好主意了。 GHC的C后端不再被积极开发(这是我不支持的感觉)。编译器的目标是C--或LLVM。

通常情况下,我认为GCC是一个很好的老式成熟编译器,在优化代码方面表现良好,因此编译为C将使用GCC的成熟度来产生更好,更快的代码。这不是真的吗?

我意识到问题在很大程度上取决于所编译语言的性质以及其他因素,以便获得更易于维护的代码。我正在寻找一个更为一般的答案(w.r.t。编译语言),它只关注性能(无视代码质量,等等)。我也将是很高兴,如果答案包括为什么GHC是由C渐行渐远,为什么LLVM执行作为后端更好的解释(see this)或我不知道的编译器做同样的任何其他实例

11 个答案:

答案 0 :(得分:27)

让我列出编译为C时遇到的两个最大问题。如果这是您的语言问题,取决于您的功能类型。

  • 垃圾收集当你有垃圾收集时,你可能不得不在程序中的任何一点中断常规执行,此时你需要访问所有指向堆。如果你编译为C,你不知道那些指针可能在哪里。 C负责局部变量,参数等。指针可能在堆栈上(或者可能在SPARC上的其他寄存器窗口中),但是没有对堆栈的真正访问。即使你扫描堆栈,哪些值是指针? LLVM实际上解决了这个问题(我以为我从来没有使用过LLVM和GC)。

  • 尾调用许多语言都认为尾调用有效(即,它们不会增加堆栈);方案要求它,Haskell假定它。 C不是这种情况。在某些情况下,您可以说服某些C编译器进行尾调用。但是你希望尾调用是可靠的,例如,当尾调用未知函数时。有笨拙的解决方法,比如蹦床,但没有什么比较令人满意的。

答案 1 :(得分:23)

虽然我不是编译专家,但我认为这可以归结为这样一个事实,即你在翻译成C语言时会丢失一些内容而不是翻译成例如。 LLVM的中间语言。

如果您考虑编译为C的过程,您创建一个转换为C代码的编译器,然后C编译器转换为中间表示(内存中的AST),然后将其转换为机器代码。 C编译器的创建者可能花了很多时间来优化语言中的某些人造模式,但是你不太可能创建一个从源语言到C来模仿的方式。人类编写代码。 C语言失去了保真度 - C编译器对原始代码的结构没有任何了解。为了实现这些优化,您实际上已经回溯了编译器以尝试生成C代码,C编译器知道如何在构建其AST时进行优化。凌乱。

但是,如果您直接翻译成LLVM的中间语言,那就像将代码编译为与机器无关的高级字节码,这类似于C编译器,允许您指定访问权限究竟它的AST应该包含什么。从本质上讲,您删除了解析C代码并直接转到高级表示的中间人,通过减少翻译来保留代码的更多特性。

与性能有关,LLVM可以为动态语言做一些非常棘手的事情,比如在运行时生成二进制代码。这是"酷"即时编译的一部分:它正在编写要在运行时执行的二进制代码,而不是被编译时创建的内容所困扰。

答案 2 :(得分:8)

GHC退出旧C后端的部分原因是GHC生成的代码不是gcc特别优化的代码。因此,随着GHC的本机代码生成器变得越来越好,大量工作的回报也越来越少。从6.12开始,NCG的代码在极少数情况下仅比C编译代码慢,因此随着NCG在ghc-7中变得更好,没有足够的动力来保持gcc后端存活。 LLVM是一个更好的目标,因为它更模块化,并且可以在将结果传递给它之前对其中间表示进行许多优化。

另一方面,最后我看了一下,JHC仍然通过gcc生成C和最终二进制文件,通常(仅限于?)。而JHC的二进制文件往往非常快。

因此,如果你可以生成代码,C编译器处理得很好,那仍然是一个不错的选择,但是如果你可以通过另一条路径更容易地产生好的可执行文件,那么可能不值得跳过太多的箍来产生好的C。

答案 3 :(得分:8)

正如您所提到的,C是否是一种优秀的目标语言在很大程度上取决于您的源语言。因此,与LLVM或自定义目标语言相比,C有一些缺点:

  • 垃圾收集:一种想要支持高效垃圾收集的语言需要知道干扰C的额外信息。如果分配失败,GC需要查找堆栈中的哪些值在寄存器中是指针而哪些不是。由于寄存器分配器不在我们的控制之下,我们需要使用更昂贵的技术,例如将所有指针写入单独的堆栈。当尝试在C之上支持现代GC时,这只是众多问题中的一个。(请注意,LLVM在该领域仍然存在一些问题,但我听说它正在进行中。)

  • 功能映射&特定于语言的优化:某些语言依赖于某些优化,例如,Scheme依赖于尾调用优化。现代C编译器可以做到这一点但不保证这样做可能会导致问题,如果程序依赖于此正确性。在C之上难以支持的另一个特性是协同例程。

    C编译器也无法很好地优化大多数动态类型语言。例如,Cython将Python编译为C,但生成的C使用对许多通用函数的调用,即使最新的GCC版本也不太可能对其进行优化。即时编译ala PyPy / LuaJIT / TraceMonkey / V8更适合为动态语言提供良好的性能(代价是更高的实现工作量)。

  • 开发经验:拥有一个解释器或JIT也可以为开发人员提供更方便的体验 - 生成C代码,然后编译并链接它,肯定会更慢,不太方便。

尽管如此,我仍然认为使用C作为新语言原型的编译目标是一个合理的选择。鉴于LLVM被明确设计为编译器后端,如果有充分的理由不使用LLVM,我只会考虑C.但是,如果源级语言非常高级,则很可能需要更早的更高级别的优化传递,因为LLVM确实是非常低级的(例如,GHC在生成调用LLVM之前执行其大部分有趣的优化)。哦,如果你是一种语言的原型,使用解释器可能是最简单的 - 只是试着避免过多依赖于解释器实现的功能。

答案 4 :(得分:7)

除了形成所有代码生成器质量的原因外,还有其他问题:

  1. 免费的C编译器(gcc,clang)有点以Unix为中心
  2. 支持多个编译器(例如Unix上的gcc和Windows上的MSVC)需要重复工作。
  3. 编译器可能会在Windows上拖动运行时库(甚至是* nix仿真),这很痛苦。两个不同的C运行时(例如linux libc和msvcrt)基于使您自己的运行时及其维护复杂化
  4. 你的项目中有一个大的外部版本blob,这意味着一个主要的版本转换(例如,修改可能会损害你的运行时库,ABI更改,如更改对齐)可能需要相当多的工作。请注意,这适用于编译器和外部版本(部分)运行时库。并且多个编译器将其相乘。对于C和后端来说这并不是那么糟糕,就像你直接连接(读:下注)后端一样,就像是一个gcc / llvm前端一样。
  5. 在许多遵循这条道路的语言中,您会看到Cisms渗透到主要语言中。当然,这对你不开心,但你受到诱惑: - )
  6. 不直接映射到标准C的语言功能(如嵌套过程, 和其他需要堆叠的东西很难。
  7. 请注意,第4点也意味着您需要花时间在外部项目发展时保持工作。这个时间通常不会真正进入您的项目,并且由于项目更具动态性,因此多平台版本需要大量额外的发布工程来满足变更。

    简而言之,从我所见过的,虽然这样的举动允许快速启动(为许多架构免费获得合理的代码生成器),但也有缺点。其中大多数都与失去控制和Windows支持* gix等* nix中心项目有关。 (LLVM对于长期来说太新了,但是他们的言论听起来很像十年前的gcc)。如果一个你非常依赖的项目保持一定的路线(比如GCC会非常慢),那么你就会陷入困境。

    首先,确定您是否希望拥有严重的非* nix(OS X更加unixy)支持,或者只是具有适用于Windows的mingw权宜之计的Linux编译器?许多编译器需要率先支持Windows。

    第二,产品必须如何成品?什么是主要受众?它是开源开发人员可以处理DIY工具链的工具,还是想要针对初学者市场(如许多第三方产品,例如RealBasic)?

    或者您真的想为深度整合和完整工具链的专业人士提供全面的产品吗?

    这三个都是编译器项目的有效指示。问问自己你的主要方向是什么,并且不要假设有更多选项可以及时获得。例如。评估九十年代早期选择成为GCC前端的项目。

    基本上,unix方式是扩展(最大化平台)

    完整的套件(如VS和Delphi,后者最近也开始支持OS X并且过去支持linux)深入并尝试最大化生产力。 (几乎完全支持Windows平台,深度集成)

    第三方项目不太明确。他们更多地追求自雇程序员和小众商店。他们拥有较少的开发人员资源,但更好地管理和关注他们。

答案 5 :(得分:6)

尚未提出的一点是,你的语言与C有多接近?如果您正在编译一种相当低级的命令式语言,那么C的语义可能会与您正在实现的语言非常接近。如果是这种情况,它可能是一个胜利,因为用您的语言编写的代码可能类似于某人用C手写的代码。 Haskell的C后端肯定不是这种情况,这也是C后端优化得非常差的一个原因。

反对使用C后端的另一点是C的语义are actually not as simple as they look。如果您的语言与C显着不同,使用C后端意味着您将不得不跟踪所有这些令人生气的复杂性,以及C编译器之间可能存在的差异。使用LLVM可能更容易,语义更简单,或者设计自己的后端,而不是跟踪所有这些。

答案 6 :(得分:3)

就我个人而言,我会编译为C.这样你就拥有了一种通用的中间语言,并且不需要担心你的编译器是否支持那里的每个平台。使用LLVM可能会获得一些性能提升(虽然我认为可以通过调整C代码生成以实现更优化的方式来实现相同的效果),但它会锁定您只支持LLVM支持的目标,并且必须等待当您想要支持新的,旧的,不同的或模糊的东西时,LLVM可以添加目标。

答案 7 :(得分:2)

据我所知,C无法查询或操纵处理器标志。

答案 8 :(得分:2)

这个答案是对C作为目标语言的一些观点的反驳。

  1. 尾调用优化

    任何可以进行尾调用优化的函数实际上等同于迭代(它是一个迭代过程,在SICP术语中)。此外,出于性能原因,使用累加器等,可以而且应该对许多递归函数进行尾递归。

    因此,为了使您的语言能够保证尾部调用优化,您必须检测它并且只需将这些函数映射到常规C函数 - 而是从它们创建迭代。

  2. 垃圾收集

    它实际上可以在C中实现。您可以为您的语言创建一个运行时系统,该系统包含C内存模型的一些基本抽象 - 例如使用您自己的内存分配器,构造函数,对象中的对象的特殊指针。源语言等。

    例如,不是为源语言中的对象使用常规C指针,而是可以创建一个特殊的结构,通过该结构可以实现垃圾收集algorithm。您的语言中的对象(更准确地说,引用) - 可能就像在Java中一样,但在C语言中,它们可以与元信息一起表示(如果您只使用指针,则不会有这些信息)。

    当然,这样的系统可能在与现有C工具集成时遇到问题 - 取决于您的实施和您愿意做出的权衡。

  3. 缺乏操作

    hippietrail noted C缺少处理器支持的旋转运算符(我认为它意味着循环移位)。如果指令集中有此类操作,则可以使用inline assembly添加它们。

    在这种情况下,前端必须检测它正在运行的架构并提供正确的片段。还应提供某种常规功能形式的后备。

  4. This answer似乎正在认真解决一些核心问题。我希望看到更多关于C语义究竟是由哪些问题引起的证据。

答案 9 :(得分:1)

在某种特殊情况下,如果您正在编写具有强大安全性*或可靠性要求的编程语言。

首先,您需要花费数年时间才能充分了解足够大的C子集,以便您知道在编译中选择使用的所有C操作都是安全的,并且不会引起未定义的行为。其次,你必须找到一个你可以信任的C实现(这意味着一个很小的可信代码库,并且可能不会非常有效)。更不用说你需要找到一个可靠的链接器,能够执行编译的C代码的操作系统,以及一些基本库,所有这些都需要明确定义和信任。

所以在这种情况下你也可以使用汇编语言,如果你关心机器独立性,一些中间表示。

*请注意,此处的“强大安全性”与银行和IT业务声称拥有的内容完全无关

答案 10 :(得分:0)

<块引用>

将语言编译为 C 是个好主意吗?

No.

...这引出了一个明显的问题:为什么有些人仍然认为通过 C 编译是个好主意?

以这种方式支持 misusing C 的两大理由是它稳定标准化

由于这些和其他原因,散布在整个网络空间中的各种半成品、玩具式、实验室实验、单站点/使用以及其他可耻的via-C后端 - 被遗弃,大多数已经屈服于比特-腐烂。但是有一些项目确实成功地进入了主流,然后它们的成功被 via-C 支持者用来进一步延续这种幻想。

但如果您是这些支持者中的一员,请随意将幻想变为现实 - GCC 或 resurrected LLVM backend for C 正在开展这项工作。想象一下:两个构建良好、维护良好的 via-C 后端,所有先验知识的总和都可以导入其中。

他们只需要