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

ael*_*ndy 43 c compiler-construction gcc ghc

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

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

我意识到这个问题在很大程度上取决于所编译语言的性质以及其他因素,以便获得更易于维护的代码.我正在寻找一个更为一般的答案(编译语言),专注于性能(无视代码质量,等等).如果答案包括解释为什么GHC偏离C以及为什么LLVM作为后端表现更好(参见本文)或编译器做任何其他我不了解的编译器的例子,我也会很高兴.

aug*_*tss 26

让我列出编译为C时遇到的两个最大问题.如果这是您的语言问题,取决于您具有哪种功能.

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

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

  • Lennart就在这里 - 这是GHC通过C​​编译时遇到的两个主要问题.正确处理垃圾收集真的需要自己管理堆栈 - 另一种方法是使用保守的GC,这在一个真正不可行的生产系统.LLVM解决了尾调用问题,但它对GC问题的解决方案对于GHC来说还不够好(而且不清楚它是否会出现 - 在C--中有一个严重的尝试来做到这一点,甚至在那里它需要一些妥协). (8认同)

Mat*_*att 24

虽然我不是编译专家,但我相信它归结为这样一个事实,即你在翻译成C语言时失去了一些东西而不是翻译成LLVM的中间语言.

如果你想一想编译到C的过程中,创建一个编译器翻译成C代码,那么C编译器翻译成中间表示(在内存中的AST),然后转换该机器码.C编译器的创建者可能花了很多时间来优化语言中的某些人造模式,但是你不太可能创建一个从源语言到C的足够奇特的编译器来模仿人类编写的方式码.C语言失去了保真度 - C编译器对原始代码的结构没有任何了解.为了获得这些优化,你实际上是回调你的编译器,试图生成C代码,C编译器知道如何在构建AST时进行优化.乱.

但是,如果直接转换为LLVM的中间语言,就像将代码编译为与机器无关的高级字节码,这类似于C编译器,使您可以准确指定其AST应包含的内容.从本质上讲,您删除了解析C代码并直接转到高级表示的中间人,通过减少翻译来保留代码的更多特性.

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

  • 因此,要获得此优化,您必须构建_your_编译器 - 生成c的编译器 - 以了解两者之间的差异,并根据情况生成不同的c代码.如果您编译为LLVM字节码,LLVM可以通过例如检查您是否查看返回值并决定优化然后从生成的字节码中推断出这一点.GCC _may_足够聪明,因为这是一个非常简单的例子,但是当处理较低级别的语言(如LLVM字节码)时,优化器更容易找到这种低水平的结果,而不是处理c时. (3认同)
  • 好吧,想象一下C语句`x ++` - 这可以编译为将x复制到另一个寄存器,然后递增x的值,然后返回x的复制(前一个)值.一个非常明显的优化是使用[test-and-set](http://en.wikipedia.org/wiki/Test-and-set)指令编译它,如果处理器支持它,这正是这样,但是更快,更原子.如果你在C中用`x = x + 1`表示相同的语句,它可能不会被优化,因为它不完全相同 - 你永远不需要返回先前的值,对吧? (2认同)

Dan*_*her 8

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

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

所以,如果你能产生代码的C编译器处理好,这仍然是一个不错的选择,但它可能不值得了太多的跳铁圈产生好的C,如果你能很容易通过其他途径产生良好的可执行文件.

  • @R ..不像其他人所说的那样程度.C编译器擅长优化C代码,而不是优化仲裁低级代码; 在编码低级细节时,C远远低于(LLVM)汇编(并且像GHC这样的编译器在此级别进行优化).例如,尝试固定寄存器并有选择地用它们指向的代码替换函数指针 - 对于C,这需要一个模糊的GCC扩展和2000行Perl代码.对于LLVM,它需要定义一个调用约定,在生成代码时需要注意,以及180行Haskell. (3认同)
  • 好吧,LLVM后端为现有的寄存器分配器提供了很大的自由,可以在合理时(至少在函数调用之间)使用"固定"寄存器,而C后端通过使固定寄存器完全不可用于GCC的寄存器分配器来工作.又花多少钱?LLVM后端是在几个人月内创建的(好吧,我猜这里,但这是一个人的论文),代码大小很小,可以轻松地跟上(甚至击败)现有的后端并提供更大的灵活性. (3认同)
  • 这对我来说很重要:编写LLVM后端非常清楚,比编写和维护GCC后端更简单. (2认同)

nom*_*olo 7

正如您所提到的,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之前执行其大部分有趣的优化).哦,如果你是一种语言的原型,使用解释器可能是最简单的 - 只是尽量避免过多依赖于解释器实现的功能.


Mar*_*ort 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的语言功能(如嵌套过程和其他需要堆栈摆弄的东西)很难.

请注意,第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平台,深度集成)

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


R..*_*R.. 5

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

  • C是C的意思,不是GCC。世界上可能有超过 100 种不同的 C 编译器(至少如果您只需要 C89 而不是 C99 或 C11)。结合起来,它们支持的目标比 GCC 或 LLVM 所能支持的目标多得多。 (2认同)

Dan*_*ons 5

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

反对使用C后端的另一个观点是,C的语义实际上并不像它们看起来那么简单.如果您的语言与C显着不同,使用C后端意味着您将不得不跟踪所有这些令人生气的复杂性,以及C编译器之间可能存在的差异.使用LLVM可能更容易,它具有更简单的语义,或者设计自己的后端,而不是跟踪所有这些.

  • 我认为走向灭亡之路的第一步是相信C的语义很简单.:) (7认同)