2023总结感想
“山重水复疑无路,柳暗花明又一村”
一学期过得飞快,在因为递归下降的bug de不出来而抓耳挠腮的场景还恍若昨日时,代码优化的工作却已悄然接近尾声。仿佛在不经意之间,我们的SysY编译器就已经从一个稚嫩的孩童长成了风度翩翩的少年。
当然,这一段成长并不像表面看起来那么一帆风顺,尽管每一项作业的提交列表最终都是绿油油的100分,但是旁边不起眼的提交备注却记录了每一次提交的过程,记录了将近四个月时间里点点滴滴的努力,记录了熬过的数不清的夜,和因为敲代码而错过去吃的不知道多少顿午餐和晚餐。
无论是最为简单的DFA词法分析,还是用递归下降进行语法分析,抑或是无数细节令人抓狂的错误处理和后面难度不言而喻的代码生成和代码优化,每一个模块的背后都包含着许多令人捉摸不透的点,已经记不清有多少次因为理不清某个模块背后的执行逻辑而一筹莫展,心情郁闷。但是就像我在文章开头所说的那样,“山重水复疑无路,柳暗花明又一村”,每每在我感到举足无措的时候,新的思路都会想一道闪电一样直击我的脑海,最终帮助我突破限制我很久的瓶颈。
总而言之,与编译为伴的这学期是充实且十分具有挑战性的,当然这些挑战也无疑给我的能力带来了莫大的提升。接下来,我想在这篇总结里分两个部分分别阐述我本学期编译课程中的收获以及对这门课程的些许期望。
一、个人收获
1.编译理论更加熟悉
经过一个学期编译课程的学习,对我而言最直观的收获一定是对一个编译器的完整构造有了全局性的认识,并且对其中各个部分的实现做到了比较熟悉的程度。
就拿我自己设计和实现的SysY编译器而言,它采用了$1+4+N$的架构(如下图),1指的就是编译器的程序入口,4分别指词法分析子程序、语法分析子程序、语义分析和中间代码生成子程序和目标代码生成子程序,$N$则指的是其他众多的辅助程序,例如错误处理子程序、关键字分析子程序、中间代码优化程序、目标代码优化程序等等。
其中,词法分析采用的是将线性文法转换成确定的有穷自动机(DFA)的方式进行解析,首先根据文法中包含的单词类别设计多个不同的自动机,然后每一次按照读到的字符类型进入相应自动机,如果最终产生了被该自动机接受的单词,就将其类型和值返回,直到文件读取结束,如此就实现了词法分析功能。经过这一阶段的学习和实践,我对乔姆斯基文法体系中的三型文法(正则文法)有了更加深刻的理解,知道了三型文法和有限状态自动机的对应关系。
语法分析采用的则是标准的递归下降进行解析,在这次作业之前,我其实心里对递归下降始终有一种恐惧的感觉,因为上学期OO第一单元我对递归下降的方式并没有完全理解,最后实现得也是非常痛苦,但是再这次系统性的学习和实践之后,我认为自己对递归下降思想的精髓基本掌握了,能够巧妙地利用递归下降的思想去完成语法分析以及后续的常量计算等工作,并且能够在实现的过程中加入一些自己的逻辑,去让一些复杂的事情简单化。
错误处理阶段可以说是比较繁琐和复杂的了,因为需要考虑的错误种类非常多,并且第一次引入了符号表的数据结构,建立子表和从栈中弹出子表的时机都不好把握,同时还有函数参数的维数不匹配这一项需要跨越不同表判断的错误类型出现,导致整个错误处理的过程比较复杂,第一次写出来的bug也非常多,而且还需要不断地重构和调整数据的结构,因此整体的实现耗时比较长,也比较困难。(当然,和后面中间代码、目标代码生成、代码优化的工作量比起来还是小巫见大巫了)
语义分析和中间代码生成阶段我选择了和大多数同学不同的道路——将自己设计的四元式作为中间代码。虽然选择这条路纯属意外,但是现在看来,当初选择这条路我也并不后悔,因为对四元式的设计和四元式和MIPS代码对应关系的确定,让我对整个中间代码和目标代码体系都有了更加深刻的理解。在中间代码生成的阶段,我遇到最大的困难就是一开始脑子里没有处理好静态代码和动态代码的关系,将两者在函数调用等部分混淆起来,导致一直没能理解运行栈这个东西到底在中间代码生成阶段处于怎样的一个地位。当然最后搞清楚静态代码和运行时代码的关系之后,这些问题也都迎刃而解了。除此之外,当时令我比较头疼的问题还有短路求值的实现,因为我在解析Cond
条件表达式时采用的是递归下降解析语法树的方式,因此一时间难以弄清楚or和and优先级如何确定,当然,后来进行了仔细的分析之后,这个问题也最终清晰了起来。
目标代码生成和中间代码息息相关,更多的是一种对中间代码的翻译过程。在这一过程中,我学会了大二上学期学习MIPS一直没搞懂的$fp
这个寄存器的用处,也学会了函数调用过程中临时寄存器和$ra
等寄存器规范的保存和恢复方式。整体而言对MIPS这个体系结构的认识更上了一层楼。
最后的代码优化阶段可谓是“八仙过海,各显神通”了,因为当初没有选择LLVM作为中间代码,导致课程网站上的实验教程基本对我的代码优化没有什么帮助了,因此我只好根据理论课上学习的内容,硬生生地完成了基本块的划分、活跃变量数据流分析、到达定义数据流分析、死代码删除、常量传播、复制传播、局部窥孔、公共子表达式删除、图着色全局寄存器分配等等一系列的中间代码和目标代码优化。因为没有太多参考实验教程的内容,导致我对理论课上讲的相关知识有了更加深刻的理解,也在完全通过自己设计数据结构进行优化的过程中对编译器的工作原理有了一个更高层次上的理解。
总之,经过这一系列从简单到复杂,从表面到全局的编译器实现过程,我对相关知识的掌握程度更加牢固,同时也弥补了理论课纸上谈兵的缺陷,让编译器的工作原理真正被铭记在了自己的脑子里。
2.解决问题的思路和心态有所提升
我觉得相比于前面提到的理论知识的掌握更加牢固,这几个月的编译学习给我带来的更大的改变体现在思维和心态的层次。
从思维的角度而言,编译让我的思维能够更加自如地在全局和局部之间进行转换。就拿编译优化中基本块的划分这一基本操作而言,在进行基本块的划分之前我是很焦虑的,因为我不知道该如何把在理论课上用手和脑完成的事情完全交给计算机去完成。但是,当我站在全局的思维角度去思考这个问题的时候,一切又似乎不是那么复杂了,因为基本块的划分实际上只和被跳转与跳转的语句有关,只要按照这个简单的划分标准就能初步将基本块划分出来。当然,划分的具体过程中又需要将思维切换到一个较小的层次,需要去仔细分析被跳转到的语句都有什么共性(例如都是标签),还有哪些可能遗漏的细节(例如第一个基本块并不一定有跳转)等等。这种思考问题的模式让我的思维能够随时在全局和局部之间进行切换,提高了思维的活跃度和整体的思维层次。
从心态的角度而言,我认为经过整个编译实验的过程,尤其是经过最后的代码优化阶段,我的心态发生了很大的转变,变得更加积极,更加上进,也更加乐观。就拿最后的代码优化阶段而言,一开始因为我选择的四元式找不到很好的优化教程,其实我是打算放弃代码优化的,但是后来我逐渐转变了心态,觉得既然没有优化教程,何必不自己去发掘呢,于是我便开始着手于将理论课上讲的代码优化方式中的操作逻辑抽象出来,然后在脑海中构建它的Java实现形式,最终再尝试用Java真正地实现它。就用这个思路,我不断提醒自己不要放弃,最终依次实现了基本块划分、常量传播、死代码删除、图着色寄存器分配、局部窥孔、DAG图公共子表达式删除等等的优化操作。截止落笔时,我的各个测试点优化结果如下:
在159名已提交的同学中,testfile1的排名为16名,testfile2-4的排名为第4名,testfile5的排名为第5名,testfile6排名为23名,testfile7排名为11名,testfile8的排名相对靠后,为32名。整体而言优化结果还算不错,重要的是经过那一段时间与自己关于“优化还是不优化”的思想斗争,最终以我战胜自己告终。
同时,我认为我的耐力也获得了很大提高,可以看到,在竞速排序中,我目前的提交次数为68次,这里面包含了我对编译器的68次修改,固然也有很多次优化毫无提升,有很多次优化出了bug,甚至有很多次做出了负优化。但是我一直没有放弃,坚持将自己能够做到的优化基本都实现了一遍。
总体而言,我认为一学期的编译课程对我的磨练是全方位的,既锻炼了我的思维和思考方式,又同时从心理和身体两方面提高了我的耐力。
二、对课程的一些期待
我认为编译这门课程整体而言是非常不错的,理论和实验的衔接相对来说比较的紧密和自然,同时实验课程的进度安排也是比较合理,但是我觉得还有以下几个可以优化的点:
- 在实验课程初期对三种实验模式的介绍可以更加详细一些,比如选择llvm作为中间代码和选择四元式作为中间代码各有什么优缺点等,可以帮助同学们在课程初期对整个实验的流程有一个更加具象化的认知。
- 作业的介绍可以更加全面一些,例如下面这一段中,
需自行设计四元式中间代码
可以换一种说法,因为很多同学也选择了LLVM作为中间代码最终生成MIPS汇编代码。所以类似的介绍也可以包含使用LLVM作为中间代码这一选择。
总之,希望编译这门课程能够保持自己的优势,同时不断优化,让更多同学感受到编译的精巧和魅力所在。