我一直在编写关于编译器和工具的课程(本学期)。我已阅读到中间代码生成,并且还看到了DAG表示的最优性。有一件事是清楚的,编译器是什么中间代码已经生成,它必须映射到系统的指令集,以便我们可以运行我们的程序。
让我们说我有一个编写特定架构的编译器(比如A),其中两个数字之间的加法是 ADD R1,R2,R3 (来自A的指令集) R1-是目的地,R2,R3是来源。我已经映射了这些指令,就是当我想要添加两个数字(为了简单而不管它的类型),这在中间代码中表示,我将运行 ADD 操作码!
假设新架构已经进入市场,其中两个数字的添加具有不同的指令集,例如 AD R1,R2,R3 。现在显然我的编译器不会添加数字!
现在我的问题是当我为我的编程语言编写编译器时,我必须使用它们的指令集添加所有架构,以便我的编译器正确地执行它需要做什么?如果是这样,那么优化这种效果的所有方法是什么?因为添加所有指令集几乎会降低我的表现。
正确如果我错了!
答案 0 :(得分:4)
为特定指令集构建编译器,该指令集是所选“指令集架构(ISA)”的子集。 (许多指令集都有I / O指令,但编译器几乎从不生成这些指令)。 可能有几种不同的处理器设计执行这种“指令集架构” 这将适用于您选择的特定指令子集。
实践中发生了三种进化事件。
如果使用来自ISA的更多指令,您确定编译器会更好。例如,您可能会认为MULTIPLY指令允许编译器生成比您过去用于乘法的子例程调用更快的代码。在这种情况下,您稍微扩展编译器。
ISA(Intel,AMD,IBM,...)的所有者向ISA添加了全新的指令集。例如,数据在高速缓存行数据上的数据并行操作(“SIMD指令”)。您可以决定将其中一些添加到编译器中。这一事件可能很困难,因为新的指令系列通常会对数据的布局和处理方式做出不同的假设。
您会发现一些您想要处理的完全不同的ISA。在这种情况下,您将重建编译器的后端,因为insturction集完全不同,就存在的寄存器,它们的使用方式等而言。
编译器构建器通常构建编译器以分阶段运行。在生成实际机器代码之前的最后阶段通常将程序表示为对相当低级别数据(例如,对固定字大小值的操作)的抽象操作集合,具有相当标准的抽象操作(ADD,MULTIPLY,COMPARE,JUMP, CALL,STORE,LOAD,...)没有对实际ISA的承诺(特别是没有关于注册或特定机器指令的通信)。这样做可以独立于ISA进行更高级别的优化;我认为这是一个很好的模块化。最后几个阶段专门针对ISA;通常在舞台上分配寄存器,然后是一个阶段,模式将实际指令与抽象指令相匹配。
有关于更高层次优化的书籍,以及在最终代码生成状态上编写的其他书籍(通常是在单独章节中解决的书籍)。 [Aho& Ullman Dragon的书和Torczon的“工程编译器”都是关于这两个主题的相当好的书籍)。有许多技术允许用户写下最终的指令集和寄存器布局,并将产生大部分最后阶段; GCC就是这样。这项技术很复杂,不适合这句话;最好去看书。
一旦您以这种方式为第一个ISA工作,您就可以使用相同的技术构建变体。最终得到两个物理编译器,每个ISA一个。它们共享所有前端逻辑和抽象代码生成和优化。它们在最后阶段完全不同。
您应该了解的是,构建编译器以利用指令集是一个复杂的过程。
答案 1 :(得分:1)
这一切都取决于变化有多大。假设你有ISA X 1.0,带有ADD R1,R2,R3指令。如果你得到一个新的版本X 1.1,它将指令ADD R1,R2,R3替换为AD R1,R2,R3,则变化很小。基本上你有不同名称的相同指令。你可以用一个标志cc -arch X1.1来容纳这个变化,它会发出“AD”而不是“ADD”。
如果变化大于AD R1,R2(R2 < - R1 + R2)那么新指令与旧ADD不同。在这种情况下,您需要更改代码生成器并将此指令包含在您的可用指令集中。 cc -arch X1.1应该让发生器知道该指令可用。
如果更改甚至更大,例如将编译器重定向到ISA Y,那么您需要更改代码生成器生成的所有指令。这可能是很多工作。
现在,现有的低级编译器优化是否适用于新目标也值得怀疑。通常,定义良好的后端(如gcc)可以支持许多ISA。它使用低级中间表示(IR),其中您的指令有几个属性,包括操作码名称(“ADD”或“AD”)。但是,低级优化器并不像指令的其他特性那样关注名称,即它有多少个操作数?读/写哪些操作数?它访问内存吗?它会改变程序流程吗?等
如果您可以将目标体系结构调整到编译器框架中,那么您可以成功地利用现有的优化。