简介
由于我们看不到您的代码片段来了解您如何处理解析器中的循环和输出代码,以及您可能想要展开的特定循环的示例,因此很难再提供比已经给出的详细建议。全球任何地方都没有比已经阅读您的问题的人更有经验的编译器作者或教师了!所以我们需要探索其他方法来解释如何解决这样的问题。
人们经常无法发布他们的代码示例,因为他们从作为课堂练习的一部分或来自开源存储库的重要代码库开始,并且他们不完全了解它是如何工作的找到合适的代码片段来发布。让我们想象一下,您拥有用于真实语言的工作编译器的完整源代码,并且想要为现有的工作编译器添加一些循环优化,然后您可能会说,“什么源代码,我如何显示一些源代码? " (因为实际上是几万行代码)。
示例编译器
在没有一些代码可以参考的情况下,另一种方法是创建一个,作为范例,来解释问题和解决方案。这通常是在编译器教科书或编译器类中完成的。我将使用一个类似的简单示例来演示如何使用 flex 和 bison 工具实现此类优化。
首先,我们需要定义示例的语言。为了保持在 SO 答案的合理大小限制内,语言必须非常简单。我将使用简单的表达式赋值作为我的语言中唯一的语句形式。该语言中的变量将是单个字母,常量将是正整数。唯一的表达式运算符是加号 (+)。我的语言中的示例程序可能是:
i = j + k; j = 1 + 2
编译器生成的输出代码将是具有四个指令LDA、STO、ADD 和STP 的单个累加器机器的简单汇编程序。为上述语句生成的代码将是:
LDA j
ADD k
STO i
LDA #1
ADD #2
STO j
STP
LDA 将值或变量加载到累加器中,ADD 将变量或值添加到累加器,STO 将累加器存储回变量。 STP 是程序结束的“停止”。
弹性程序
上面显示的语言需要 ID 和 NUMBER 的标记,并且还应该跳过空格。以下内容就足够了:
%{
#define yyterminate() return (END);
%}
digit [0-9]
id [a-z]
ws [\t\n\r ]
%%
{ws}+ /* Skip whitespace */
{digit}+ {yylval = (int)(0l - atol(yytext)); return(NUMBER); }
{id} {yylval = yytext[0]; return(ID); }
"+" {return('+'); }
"=" {return('='); }
血淋淋的细节
只是一些关于这是如何工作的注释。我使用atol 转换整数,以处理读取MAXINT 时可能发生的潜在整数溢出。我正在否定常量,以便可以轻松地将它们与在一个字节中为正的标识符区分开来。我存储单个字符标识符以避免说明符号表代码的负担,因此允许使用非常小的词法分析器、解析器和代码生成器。
野牛计划
要解析语言并从野牛动作生成一些代码,我们可以通过以下野牛程序来实现:
%{
#include <stdio.h>
%}
%token NUMBER ID END
%%
program : statements END { printf("STP\n"); return(0) ; }
;
statements : statement
| statements ';' statement
;
statement : ID '=' expression { printf("STO %c\n",$1); }
|
;
expression : operand {
/* Load operand into accumulator */
if ($1 <= 0)
printf("LDA #%d\n",(int)0l-$1);
else printf("LDA %c\n",$1);
}
| expression '+' operand {
/* Add operand to accumulator */
if ($3 <= 0)
printf("ADD #%d\n",(int)0l-$3);
else printf("ADD %c\n",$3);
}
;
operand : NUMBER
| ID
;
%%
#include "lex.yy.c"
方法说明
本段适用于那些知道如何执行此操作并且可能会质疑我的示例中使用的方法的人。我刻意避免构建一棵树并进行树遍历,尽管这将是代码生成和优化的正统技术。我想避免在示例中添加所有必要的代码开销来管理树并遍历它。这样我的示例编译器可以非常小。但是,仅限于使用 bison 操作来执行代码生成限制了我对 bison 规则匹配的排序。这意味着只能真正生成伪机器代码。使用这种方法,源到源示例将不太容易处理。我选择了一个理想化的机器,它是 MU0 和无寄存器 PDP/11 之间的交叉点,同样具有最少的功能来演示代码的一些优化。
优化
现在我们用几行代码就可以为一种语言提供一个工作编译器,我们可以开始演示添加代码优化的过程是如何工作的。
正如受人尊敬的@Chris Dodd 所说:
如果要在解析后进行程序转换,则应在解析后进行。您可以增量地执行它们(在解析部分输入后从您的野牛代码中调用转换例程),或者在解析完成后,但无论哪种方式,它们都会在解析您正在转换的程序部分之后发生。
此编译器通过在解析部分输入后增量地发出代码来工作。随着每条语句的识别,bison 动作(在{...} 子句中)被调用以生成代码。如果要将其转换为更优化的代码,则必须更改此代码以生成所需的优化。为了能够实现有效的优化,我们需要清楚地了解要优化的语言特征以及最佳转换应该是什么。
恒定折叠
可以在编译器中完成的常见优化(或代码转换)是常量折叠。在常量折叠中,编译器用结果替换完全由数字组成的表达式。例如考虑以下内容:
i = 1 + 2
一种优化是将其视为:
i = 3
因此,1 + 2 的添加是由编译器添加的,并没有放入生成的代码中以在运行时发生。我们预计会产生以下输出:
LDA #3
STO i
改进的代码生成器
我们可以通过寻找在expression '+' operand 两边都有NUMBER 的显式情况来实现改进的代码。为此,我们必须延迟对expression : operand 采取任何行动,以允许该值继续传播。由于expression 的值可能没有被评估,我们必须在赋值和加法时进行评估,这导致if 语句的轻微爆炸。我们只需要更改规则statement和expression的动作,如下所示:
statement : ID '=' expression {
/* Check for constant expression */
if ($3 <= 0) printf("LDA #%d\n",(int)0l-$3);
else
/* Check if expression in accumulator */
if ($3 != 'A') printf("LDA %c\n",$3);
/* Now store accumulator */
printf("STO %c\n",$1);
}
| /* empty statement */
;
expression : operand { $$ = $1 ; }
| expression '+' operand {
/* First check for constant expression */
if ( ($1 <= 0) && ($3 <= 0)) $$ = $1 + $3 ;
else { /* No constant folding */
/* See if $1 already in accumulator */
if ($1 != 'A')
/* Load operand $1 into accumulator */
if ($1 <= 0)
printf("LDA #%d\n",(int)0l-$1);
else printf("LDA %c\n",$1);
/* Add operand $3 to accumulator */
if ($3 <= 0)
printf("ADD #%d\n",(int)0l-$3);
else printf("ADD %c\n",$3);
$$ = 'A'; /* Note accumulator result */
}
}
;
如果您构建生成的编译器,您会发现它确实会生成更好的代码并执行常量折叠转换。
循环展开
您在问题中特别询问的转换是循环展开。在循环展开中,编译器将在循环开始和结束条件中查找一些特定的整数表达式值,以确定是否应该执行展开的代码转换。然后编译器可以为循环生成两个可能的代码替代序列,展开的和标准的循环代码。我们可以在这个示例迷你编译器中使用整数增量来演示这个概念。
如果我们想象机器代码有一条 INC 指令,它将累加器加一并且比执行 ADD #1 指令更快,我们可以通过寻找特定情况来进一步改进编译器。这涉及评估整数常量表达式并与特定值进行比较以确定是否应使用替代代码序列 - 就像在循环展开中一样。例如:
i = j + 1
应该导致:
LDA j
INC
STO i
最终代码生成器
要更改为n + 1 生成的代码,我们只需要重新编码expression 语义的一部分,并测试当不折叠常量时,要使用的常量是否为1(在本例中被否定)。结果代码变为:
expression : operand { $$ = $1 ; }
| expression '+' operand {
/* First check for constant expression */
if ( ($1 <= 0) && ($3 <= 0)) $$ = $1 + $3 ;
else { /* No constant folding */
/* Check for special case of constant 1 on LHS */
if ($1 == -1) {
/* Swap LHS/RHS to permit INC usage */
$1 = $3;
$3 = -1;
}
/* See if $1 already in accumulator */
if ($1 != 'A')
/* Load operand $1 into accumulator */
if ($1 <= 0)
printf("LDA #%d\n",(int)0l-$1);
else printf("LDA %c\n",$1);
/* Add operand $3 to accumulator */
if ($3 <= 0)
/* test if ADD or INC */
if ($3 == -1) printf("INC\n");
else printf("ADD #%d\n",(int)0l-$3);
else printf("ADD %c\n",$3);
$$ = 'A'; /* Note accumulator result */
}
}
;
总结
在这个迷你教程中,我们定义了一门完整的语言、一个完整的机器代码、编写了一个词法分析器、一个编译器、一个代码生成器和一个优化器。它简要展示了代码生成的过程,并指出(尽管一般而言)如何执行代码转换和优化。它应该能够在其他(尚未见过的)编译器中进行类似的改进,并解决了识别循环展开条件和针对这种情况生成特定改进的问题。
它也应该说清楚,如果没有具体的examples of some program code 参考,回答问题是多么困难。