【问题标题】:ASM code generation look ahead and complexityASM 代码生成前瞻性和复杂性
【发布时间】:2018-02-24 22:09:32
【问题描述】:

我有一个带有 funcDef、funcCall、literals 等节点的 AST。我正在研究我的编译器的代码生成器部分,为 x86_64 生成程序集。我的问题是生成 AST 的代码的“正确”(行业标准)方式是什么:是否多次传递,例如计算需要多少局部变量以便使用该值来递减堆栈指针?此外,复杂的 AST,例如一个 funcCall,一个参数是另一个 funcCall 等等,是如何解析为 ASM 的?

将我的 AST 转换为非常简单的 IR(SSA?)是否明智?另外,由于我对代码生成理论一窍不通,有没有什么好书专注于这方面的?

谢谢!

【问题讨论】:

  • 没有标准答案。作为编译器编写者,您为编译器设定目标。答案取决于目标。最小、最快、最简单的编译器需要很少的内存,直接从解析器一次性发出代码,但质量不高。最高度优化的编译器使用几种中间形式、辅助数据结构和多次传递。而且它们非常复杂。如果您陈述自己的目标,就有可能为实现目标的合理方法设定界限。
  • @Gene:我正在寻找最简单的解决方案(我这样做是为了学习基础知识)。一次运行的编译器如何可能?如果您在 funcCall 中有一个类似 OP 且 funcCall 作为变量的 AST,那么您需要知道以便能够在父调用之前movq %rax, rdi,不是吗?再次为愚蠢的问题感到抱歉,我真的只是在努力学习。谢谢!
  • 对于变量,通常至少有两个阶段——SSA 表单生成和寄存器分配,只有后者关注目标主机细节,因为前者可以与架构无关。组装阶段本身也至少有两次通过,以便以最简单的方式解决向前跳跃的目标。很难更详细地回答您的问题,因为您所问的主题非常广泛,基本上约占编译器设计的三分之二。
  • 关于实际代码生成。例如,如果您针对基于 x86 的 Intel 处理器,您可能希望阅读 ia 32 architectures optimization manual
  • 你可能想看看 LLVM 工具链。它需要一个 SSA(主要是)IR,并且可以生成所有数量的架构机器代码。

标签: assembly compiler-construction abstract-syntax-tree codegen ssa


【解决方案1】:

生成机器代码的“正确”/标准方法是使用优化编译器,该编译器通过内部表示(通常是SSA 形式)进行转换,并且看起来很难进行各种优化。

解释器更容易编写,如果编写得好可以提供比低效/天真生成的 asm 更好的性能,因此没有标准的“简单”方式来生成 asm,因为没有人想要那样。 (除了作为一个自学编译器的业余项目,我猜。)


自己编写一个好的编译器需要几十年的工作。请参阅Why are there so few C compilers?,尤其是 Basile Starynkevitch 的回答。即使对于行为不如现代 x86-64 复杂的“简单”CPU 也是如此;优化掉多余的工作,决定何时内联函数等等,并不容易。

但是针对现代 x86-64 的优化范围从简单(乱序执行不太关心指令顺序)到神秘(例如 inc eaxadd eax,1but on some CPUs in some cases it's slower 相比节省代码大小;多个微指令或部分标志停顿)。或者,与英特尔 Sandybridge 系列 CPU 上的 2 个单独的 LEA / ADD 指令相比,3 组件 LEA 具有更高的延迟(但可能具有更好的吞吐量)。 另请参阅Agner Fog's optimization guidesthe x86 tag wiki 中的其他性能优化链接。 如果您要尝试优化,则只需要担心这一点。高效地完成大量冗余工作并没有那么有用。


要为一门新语言制作编译器,您只需编写一个生成 LLVM-IR 的前端并将其提供给 LLVM 库,以便它优化和生成 asm 或机器代码. (你可以对 GIMPLE 做同样的事情,使用 gcc 的优化中间/后端而不是 LLVM)。作为奖励,您的编译器有望在 LLVM 或 gcc 支持的大多数 CPU 架构上运行,而不仅仅是 x86。

例如看这个Implementing a Language with LLVM 教程。


天真地将每个表达式的每个部分分别转译为 asm 指令会产生缓慢而臃肿的 asm。可能与您从clang -O0 得到的相似,但它确实在表达式内进行了优化,因此10 + x + 4 仍然与x + 14 编译相同。 clang -O0 还增加了在每条语句之后将所有内容溢出到内存的负担,因此您可以在任何断点处使用调试器修改内存中的 C 变量。 (这是-O0 的一部分含义:保证一致的调试,以及以最少的优化工作量快速编译。)

一个不关心这一点的幼稚编译器可能会跟踪哪些值存在于哪个寄存器中,并在需要新寄存器时溢出旧值。如果您不提前考虑很快需要哪些值,而宁愿将这些值保存在寄存器中,这很容易变得很糟糕。


如果您不关心生成的 asm 的质量,那么当然,做任何方便的事情。

TinyCC is a one-pass C compiler。当它发出一个函数序言时,它还没有决定它需要保留多少字节的堆栈空间。 (一旦到达函数的末尾,它就会返回并填充它。)请参阅Tiny C Compiler's generated code emits extra (unnecessary?) NOPs and JMPs 以了解其有趣的结果:nop 填充其函数序言的一个版本。

IDK 它在内部做了什么,但大概当它遇到新的变量声明时,它会将它们附加到它要保留的堆栈帧的末尾(因此不会将偏移量从 rbp 更改为任何现有变量,因为它可能已经使用它们发出)。

TCC 是开源的,写得小/简单(编译速度快),不是创建好的 asm,所以你可能想看看它的源代码,看看它是什么可以。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-01-20
    • 2021-03-03
    • 2018-08-23
    • 2013-01-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多