将语言编译为C是一个好主意吗?
在整个networking中,我感觉到为编译器编写C后端不再是一个好主意。 GHC的C后端没有被积极开发(这是我没有支持的感觉)。 编译器的目标是C–或LLVM。
通常情况下,我认为GCC是一个很好的老成熟的编译器,在优化代码方面performance良好,因此编译为C将使用GCC的成熟度来产生更好更快的代码。 这是不是真的?
我意识到这个问题在很大程度上取决于正在编译的语言的性质和其他因素,以便获得更多可维护的代码。 我正在寻找一个比较一般的答案(关于编译语言),它只关注性能(不考虑代码质量,等等)。 如果答案中包含一个关于GHC为什么偏离C的解释,以及为什么LLVM能够更好地作为后端( 参见这个 )或者其他编译器的例子,我也不太了解。
虽然我不是一个编译器专家,但我认为这归结于这样一个事实,即在翻译为C时丢失了某些东西,而不是翻译成LLVM的中间语言。
如果您考虑编译到C的过程,您将创build一个编译器转换为C代码,然后C编译器将转换为中间表示(内存中的AST),然后将其转换为机器代码。 C编译器的创build者可能花了很多时间来优化语言中的某些人造模式,但是你不可能创build足够多的从源语言到C语言的编译器来模拟人类编写的方式码。 C语言保真度的损失 – C编译器没有任何有关您的原始代码结构的知识。 为了获得这些优化,你基本上是在回溯你的编译器来试图生成C编译器,C编译器知道如何在构build它的AST时进行优化。 乱。
但是,如果直接翻译为LLVM的中间语言,就好像将代码编译为一个与机器无关的高级字节码,类似于C编译器,您可以访问它来指定AST应包含的内容。 从本质上讲,你可以删除parsingC代码的中间人,直接转到高级代表,这样可以减less翻译,从而保留代码的更多特征。
与性能相关,LLVM可以为运行时生成二进制代码等dynamic语言做一些非常棘手的事情。 这是即时编译的“酷”部分:编写二进制代码以便在运行时执行,而不是被编译时创build的代码所困住。
让我列出编译为C时遇到的两个最大的问题。如果这对于您的语言来说是一个问题,则取决于您拥有哪种function。
-
垃圾收集当你有垃圾收集时,你可能不得不在程序中的任何点中断常规执行,此时你需要访问所有指向堆的指针。 如果你编译为C,你不知道这些指针可能在哪里。 C负责局部variables,参数等。指针可能位于堆栈上(或者可能位于SPARC上的其他寄存器窗口中),但是不能真正访问堆栈。 即使你扫描堆栈,哪些值是指针? LLVM实际上解决了这个问题(我不知道自从我从来没有使用过LLVM和GC)。
-
尾调用许多语言都假设尾调用起作用(即,它们不会增长堆栈); 计划要求它,Haskell承担它。 在C中情况并非如此。在某些情况下,你可以说服一些C编译器进行尾部调用。 但是你想尾巴呼叫是可靠的,例如,当尾巴呼叫一个未知的function。 有拙劣的变通办法,像蹦床一样,但没有什么比较令人满意的。
GHC离开旧C后端的一部分原因是由GHC生成的代码并不是gcc能够特别优化的代码。 所以GHC的本地代码生成器越来越好,大量工作的回报就less了。 从6.12开始,在很less的情况下,NCG的代码只比C编译的代码慢,所以在ghc-7中NCG变得更好,没有足够的动力来保持gcc后端的活力。 LLVM是一个更好的目标,因为它更模块化,在将结果传递给中间表示之前,可以对其进行许多优化。
另一方面,最后我看了,JHC仍然生成了C和最后的二进制文件,通常是gcc(完全是?)。 而JHC的二进制文件往往是相当快的。
所以,如果你能生成C代码编译器的代码,那么这仍然是一个不错的select,但是如果你可以通过另一条路线生成好的可执行文件,可能不值得跳过太多的循环来生成好的C.
如你所说,C是否是一个好的目标语言,很大程度上取决于你的源语言。 因此,与LLVM或自定义目标语言相比,C有一些缺点:
-
垃圾收集 :想要支持高效垃圾收集的语言需要知道干扰C的额外信息。如果分配失败,GC需要find堆栈中的哪些值和寄存器中的指针,哪些不是。 由于寄存器分配器不在我们的控制之下,因此我们需要使用更昂贵的技术,例如将所有指针写入单独的堆栈。 这只是尝试在C之上支持现代GC的许多问题之一。(请注意,LLVM在该领域还存在一些问题,但是我听说它正在进行中)。
-
特征映射和语言特定的优化 :某些语言依赖于某些优化,例如,Scheme依赖于尾部优化。 现代C编译器可以做到这一点,但不能保证做到这一点,如果一个程序依赖于这个正确性可能会导致问题。 在C之上可能难以支持的另一个特征是共同例程。
大多数dynamictypes的语言也不能被C编译器优化。 例如,Cython将Python编译为C,但生成的C使用对许多通用函数的调用,即使是最新的GCC版本,也不太可能对其进行优化。 即时编译ala PyPy / LuaJIT / TraceMonkey / V8更适合为dynamic语言提供良好的性能(代价是更高的实现成本)。
-
开发经验 :有一个解释器或JIT也可以给你一个更方便的开发经验 – 生成C代码,然后编译和链接,肯定会更慢,更不方便。
也就是说,我仍然认为使用C作为新语言原型的编译目标是一个合理的select。 考虑到LLVM被明确devise为编译器后端,如果有充分理由不使用LLVM,我只会考虑C。 但是,如果源代码级语言是非常高级的,那么很可能需要更早的更高级别的优化传递,因为LLVM的确是非常低级的(例如,GHC在生成调用到LLVM之前执行大部分有趣的优化)。 噢,如果你正在使用一种语言进行原型devise,那么使用一个解释器可能是最简单的,只是试图避免那些过于依赖解释器实现的特性。
除了所有的codegenerator质量的原因,还有其他问题:
- 免费的C编译器(gcc,clang)是以Unix为中心的
- 支持多个编译器(例如Unix上的gcc和Windows上的MSVC)需要重复工作。
- 编译器可能会在运行时库中拖动(甚至是* nix仿真),这很痛苦。 基于两个不同的C运行时(例如,Linux libc和msvcrt)使您的运行时间和维护复杂化
- 在你的项目中,你会得到一个大的外部版本blob,这意味着一个主要的版本转换(例如,改变mangling可能伤害你的运行库,ABI改变像alignment改变)可能需要相当多的工作。 请注意,这适用于编译器和外部版本(部分)运行时库。 而多个编译器就是这样的。 这对于后端来说并不是那么糟糕,就像在你直接连接(读:押在)后端的情况下一样,就像是一个gcc / llvm前端。
- 在遵循这条道路的许多语言中,你会看到Cisms渗透到主要语言中。 当然这不会对你感到高兴,但你会被诱惑:-)
- 不直接映射到标准C的语言function(如嵌套过程和其他需要堆栈处理的东西)很困难。
请注意,第4点也意味着当外部项目发展时,您将不得不花费时间来保持工作。 那个时候通常并没有真正进入你的项目,而且由于这个项目更具活力,多平台版本将需要大量额外的发布工程来迎合变化。
所以简而言之,从我所看到的,虽然这样的举动可以快速启动(为许多架构免费获得一个合理的代码生成器),但也有缺点。 他们中的大多数与失去控制和* gix等* nix中心项目的Windows支持不佳有关。 (LLVM从长远来说太新了,但是他们的言辞听起来很像十年前的gcc)。 如果一个你非常依赖的项目保持某个特定的过程(比如GCC会很慢),那么你就会陷入困境。
首先,决定是否要严重的非* nix(OS X更unixy)的支持,或者只有一个Linux的编译器与Windows mingw权宜之计? 许多编译器需要首先对Windows进行支持。
二,如何做好产品? 主要受众是什么? 它是可以处理DIY工具链的开源开发者的工具,还是你想要瞄准初学者市场(像许多第三方产品,如RealBasic)?
或者您是否真的想为深度集成和完整工具链的专业人员提供一个全面的产品?
这三个都是编译器项目的有效指示。 问问自己你的主要方向是什么,不要以为有更多的select可以及时获得。 例如,评估九十年代初select成为海湾合作委员会前端的项目。
从根本上来说,unix的方式是走得更远(最大化平台)
完整的套件(如VS和Delphi,后者最近也开始支持OS X并在过去支持Linux)进行了深入的尝试,尝试最大化生产力。 (几乎完全支持windows平台的深度集成)
第三方项目不太清楚。 他们更多的是自雇程序员和利基商店。 他们有更less的开发人员资源,但更好地pipe理和关注它们。
还没有提出的一点是,你的语言与C有多接近? 如果你正在编译一个相当低级的命令式语言,C的语义可能会非常接近你正在执行的语言。 如果是这样的话,这可能是一个胜利,因为用你的语言编写的代码很可能类似于用C写的代码。 Haskell的C后端肯定不是这样,这是C后端优化如此糟糕的原因之一。
另一个反对使用C后端的观点是C的语义实际上并不像看起来那么简单 。 如果你的语言与C有很大分歧,那么使用C后端意味着你将不得不跟踪所有那些令人生气的复杂性,以及C编译器之间的差异。 使用LLVM可能会更简单,语义更简单,或者devise自己的后端,而不是跟踪所有这些。
就我个人而言,我会编译为C.这样你就有一个通用的中介语言,不需要关心你的编译器是否支持每个平台。 使用LLVM可能会获得一些性能提升(尽pipe我认为通过调整您的C代码生成方式可以实现更好的优化),但是它会locking您仅支持LLVM支持的目标,并且不得不等待LLVM在你想支持新的,旧的,不同的或模糊的东西时添加一个目标。
据我所知,C无法查询或操纵处理器标志。
这个答案是对C作为目标语言的一些观点的反驳。
-
尾巴调用优化
任何可以被尾调用优化的函数实际上等价于一个迭代(这是一个迭代过程,在SICP术语中)。 另外,由于性能原因,使用累加器等,许多recursion函数可以并且应该被尾recursion。
因此,为了让你的语言保证尾部调用优化,你必须检测它,而不是将这些函数映射到常规的C函数 – 而是从它们中创build迭代。
-
垃圾收集
它可以实际上用C语言实现。你可以为你的语言创build一个运行时系统,它包含一些C语言内存模型的基本抽象 – 例如使用你自己的内存分配器,构造函数,源语言对象的特殊指针,等等
例如,不是为源语言中的对象使用常规C指针,而是可以创build一个特殊的结构,通过它可以实现垃圾回收algorithm 。 你的语言中的对象(更准确地说是引用)可以像在Java中一样行事,但是在C语言中,它们可以和元信息一起表示(如果你只是用指针工作的话,你不会有这些信息)。
当然,这样的系统在整合现有的C工具时可能会遇到问题,这取决于你愿意做的实现和权衡。
-
缺乏操作
嬉皮士指出 ,C缺乏处理器支持的旋转算子(我假定他是指循环移位)。 如果这些操作在指令集中可用,则可以使用内联汇编来添加它们。
在这种情况下,前端必须检测它正在运行的体系结构并提供正确的片段。 还应该提供某种forms的常规function回退。
这个答案似乎是在严肃地解决一些核心问题。 我想看看C的语义究竟是由哪些问题引起的。
如果您正在编写具有强大的安全性*或可靠性要求的编程语言,则会遇到特殊情况。
首先,需要几年的时间才能充分了解C的足够大的子集,以便知道在编译过程中select使用的所有C操作是安全的,不会引起未定义的行为。 其次,你必须find你可以信任的C的实现(这将意味着一个微小的可信代码库,可能不会非常有效)。 更不用说,你需要find一个可信的链接器,能够执行已编译的C代码的操作系统,以及一些基本的库,所有这些都需要定义好和可信。
因此,在这种情况下,如果您关心机器独立性,则可以使用汇编语言,也可以使用一些中间表示法。
*请注意,这里的“强有力的安全”与银行和IT业务声称拥有的东西无关