编译器结构的详细指南

458 阅读8分钟

在这篇文章中,我们根据定义编译器架构的两个重要方面来讨论编译器架构,这两个方面是,通过编译器的数据颗粒度和编译器模块之间的控制流,我们也经历了一个好的编译器和生成的代码的特性,编译器设计中的可移植性和重定向性。

目录:

  1. 简介
  2. 编译器宽度
  3. 控制的流动
  4. 好的编译器的特性
  5. 生成代码的属性
  6. 可移植性和重定向性
  7. 总结
  8. 参考文献

引言

编译器是一个接受源代码的程序,例如java程序,并生成特定于计算机架构的机器代码。也就是说,如果我们打算为N种编程语言编写编译器,使其在M种不同的计算机体系结构上运行,那么我们需要为每种语言-体系结构组合编写NxM的编译器。

在这篇文章中,我们讨论了涉及编译器架构的问题,这些问题是编译器模块之间传递数据的粒度和编译器模块之间的控制流。我们还讨论了编译器的可移植性和可重定向性,前者涉及如何编写N+M编译器而不是NxM,后者涉及如何使一个编译器容易生成另一个机器架构的代码。

编译器的宽度

一个编译器会有一些模块在它们之间转换、完善和传递信息,即从一个模块Mn到Mn+1。

这些模块之间传递信息的大小影响着编译器的结构,这导致了两种类型的编译器,我们将把它们称为狭义广义

狭义的编译器读取程序的一小部分(tokens),对其进行处理并产生目标代码,然后抛弃有关tokens的信息并重复进行,直到程序文本的结束。

这些编译器需要的内存与程序的长度成线性关系,尽管比例常数比较慢,因为它们收集信息的速度要慢得多。

一个例子:

while not Finished:
    Read some data D from the source code;
    Process D and produce the corresponding object code, if any;

狭义的编译器包括一个如上图所示的命令式循环:

真正的 "编译器 "是作为狭义编译器实现的,然而狭义编译器也可以选择加入广义的成分,例如,C编译器完全读取每个例程,然后丢弃除获得的全局信息以外的一切。

这种类型的编译器需要与程序大小成比例的内存。
从教育和理论的角度来看,这些编译器是比较好的,因为它们代表了一种与函数式编程范式一致的更简单的模型。

一个例子

Object codeAssembly(
        CodeGeneration(
            ContextCheck(
                Parse(
                    Tokenize(
                        SourceCode
                    )
                )
            )
        )
    );

广义的编译器由函数调用组成,正如上面所见。

控制流

广义编译器中,每个模块在当前运行时都控制着处理器和数据。

狭义编译器中,数据从一个模块移动到另一个模块,因此控制将向前和向后移动,以便在适当的时间激活适当的模块。

我们将集中讨论狭义编译器,因为它显示了控制流的一些复杂性。

模块本质上是过滤器,它们读取、处理并产生结果,因此被编程为循环,执行函数调用以获得来自前一个模块的信息块,并通过常规调用将这些信息块写入下一个模块。

一个例子 - 一个过滤器

while ObtainedFromPreviousModule(Ch):
    if Ch = 'a':
        //check for other 'a's:
        if ObtainedFromPreviousModule(Ch1):
            if Ch1 = 'a':
                //we have 'aa':
                OutputToNextModule('b');
            else −− Ch1 != 'a':
                OutputToNextModule('a');
                OutputToNextModule(Ch1);
        else // No more characters:
            OutputToNextModule('a');
            exit;
    else −− Ch != 'a':
        OutputToNextModule(Ch);

上面的过滤器将输入的字符复制到输出,同时将序列aa放在b旁边。它通过调用模块序列中的前辈来获得输入,这个调用可能产生一个字符,也可能失败。

转换后的字符被传递给下一个模块。

除了对上一个和下一个模块的常规调用外,控制将一直保持在while循环内。

主循环很容易编程和理解,只是它们不能作为编译器模块的通用编程模型,因为它们不能很好地与其他主循环接口。

假设我们想把上面那个转换aa->b的主循环连接到另一个转换bb->c的主循环,这样第一个主循环的输出就成为第二个主循环的输入*(就像Linux的管道*)。在这种情况下,我们需要一个控制的转移,使两个环境都保持完整。

这可以通过冠词调用来解决,冠词调用涉及到为两个循环分离堆栈,以保留它们的环境。

好的编译器的特性

  1. 一个好的编译器应该产生正确的代码。
  2. 一个好的编译器应该完全符合语言规范。用这样的编译器开发的程序将表现出可移植性。
  3. 好的编译器应该能够在内存允许的范围内处理任意大小的程序。请记住,代码可以由程序员编写,也可以由程序生成。
  4. 速度,编译器编写者使他们的编译器在输入中保持线性,意味着编译时间将是输入文件长度的线性函数。
  5. 大小,这从来不是一个因素,因为现在的计算机有Gbs的主内存。

当程序在运行时再次调用编译器时,速度和大小是需要考虑的重要因素,例如,即时编译。

生成代码的属性

  1. 正确性
    这是生成的代码最重要的属性,同时也是最脆弱的属性。
    对付不正确的代码的主要武器是小的语义保全变换
    --从源代码到二进制目标代码的巨大变换被分解成几个语义保全变换,每个变换都小到可以被理解并被证明是正确的。

  2. 速度:
    为了产生更快的代码。

  • 我们设计的代码转换可以产生更快的代码,甚至更好的是完全没有代码,并为它们的正确应用做必要的分析。
  • 部分评估,即我们在编译期间评估程序的一部分。
  • 例如,用被调用的函数的主体来代替函数调用,用重复循环主体来解开循环语句。这些改进虽然不大,但可以使扩展后的代码得到优化。
    编译器之外的其他优化技术是高效的算法和用汇编语言
    编写代码。
  1. 代码的大小很重要,例如,嵌入式应用的代码,如在遥控器、智能卡、汽车中,内存限制了代码的大小。
    在这种应用中减少代码大小的技术包括积极抑制未使用的代码、各种代码压缩、线程代码、程序性抽象和使用特殊硬件。

  2. 电源管理包括节约能源以增加电池供电时的操作时间,其次是限制峰值散热以保护处理器。
    如果一个程序更快,它就会更快完成,并使用更少的能源。

可移植性,重定向性

如果一个程序需要合理的努力来使其在不同的机器类型上运行,那么它就是可移植的

今天,许多程序可以通过编辑makefile来反映当地的情况并进行编译来实现移植。通常情况下,适应本地情况的任务是自动化的,例如通过使用GNU的autoconf

对于编译器来说,对机器的依赖不仅存在于程序中,也存在于输出中,因此我们必须考虑机器独立性的另一种形式。
一个程序可以轻松地生成另一台机器的代码被称为可重定向性。这是通过替换整个编译器后端来实现的。现在,重定向性将涉及到想出一个新的后端的难度。

注意,替换后端并不意味着从头开始编写另一个后端,因为有些后端代码是与机器无关的。

总结

编译器是一个接受源代码的程序,例如一个java程序,并生成特定于计算机结构的机器代码。

我们已经讨论了涉及编译器架构、编译器模块之间传递的数据粒度以及编译器模块之间的控制流的架构问题。

如果要使程序在不同的机器类型上运行需要合理的努力,那么它就是可移植的

可重定向性是指一个程序可以很容易地被生成另一台机器的代码。

参考文献:

  1. 编译器原理、技术和工具》,A.V.Aho, R.Sethi & J.D.Ullman,培生
    教育
    出版社。
  2. 编译器设计原理》,A.V.Aho和J.D.Ullman, Addition - Wesley.
  3. 现代编译器设计第二版 Dick Grune - Kees van Reeuwijk - Henri E. Bal