我理解的编译和解释的本质

这是我参与更文挑战的第10天,活动详情查看: 更文挑战

编译流程

编译需要先把源码转化成一种抽象的中间表示,然后基于抽象表示做各种转换,之后生成目标代码。

从源码到中间表示的过程叫做 parse,从中间表示到目标代码的过程叫做 generate。中间的中间表示到中间表示的转换叫做转换和优化。

parse 是基础、generate 是基础,这些都不是重点,重点是中间表示到中间表示的转换。

语义等价

中间表示之间之所以能转换是基于语义的等价,这点很关键。

基于语义等价,可以从树形中间表示到线性中间表示;

基于语义等价,可以做中间表示到中间表示的优化转换;

语义等价,那得先定义语义和检查语义正确性才行,不然之后的语义等价转换无从谈起。语义是区分语言的标志,比如类型、作用域等。在生成第一层中间表示(一般是树形中间表示 ast)之后,就要做语义检查。

语义检查先建立个符号表存信息。分析出 scope,做变量是否定义的检查(引用消解),做类型检查(声明时保存类型,使用的时候和声明的类型对比),以及其他琐碎的正确性检查(比如 continue 只能出现在循环里,运算符不能处理的一些数据类型等)。

做完语义检查之后,就可以放心的进行中间表示的转换了,之后不会再次检查。

中间表示的转换

虽然知道只是基于语义等价做转换,但是中间表示之间的转化并不简单,就像汉语到英语的翻译,虽然知道要语义等价,但是也没那么简单,要用一定的方法。

一般都是从树形中间表示(ast)到线性中间表示(三元式、四元式、逆波兰式等),每层都可以做不同的优化转换。

一般来说都是这样的顺序: 源码 –> 树形中间表示 —> 线性中间表示 —> 线性中间表示 —> 目标代码。但是有的时候也要反过来,从目标代码生成中间代码,然后逐步生成源码,反过来叫做逆向工程。

解释与编译

上面说的都是编译,解释和编译只是部分与整体的区别:

解释是一部分一部分的执行,编译是整体转成目标代码(AOT),整体执行。 但是解释器也会根据频率把一个小整体的代码用编译的方式优化(JIT),然后直接执行。

解释同样也要 源码 –> 树形中间表示, 可以到了树形中间表示就执行了,也可以再进一步 源码 –> 树形中间表示 —> 线性中间表示 然后再执行,这种线性中间表示如果是用字节表示操作符,就可以叫字节码。

解释和编译本质上还是一样的,编译整体转换完也是一条条指令的解释执行,解释局部执行但优化的时候也要把一块拿来编译。

总结

文章开始没有按照传统的编译流程描述来写,而是按照我理解的编译来描述了编译流程。没有细说词法和语法分析,因为我觉得那不是重点,parser、generator是入门基础。中间表示和基于语义等价的转换才是编译原理的重点。当然前提是要做语义检查,保证语义正确。一般都是基于树形中间表示AST做语义检查。

解释和编译都需要先把源码parse成中间表示,或者树形,或者再进一步转换为线性的,之后解释执行。也可能把一部分代码整体编译成可执行代码,直接执行。解释和编译的区别只是部分和整体的区别(时间上没有区别,因为运行时也会进行JIT编译),还是依赖于中间代码的转换。

学编译主要是学中间表示的基于语义等价的各种转换。这是我理解的编译最核心的东西

这篇半年前写的,是当时的认识,现在不这么觉得了,虽然说中间代码的分析和转换最重要,但是 parse 和 generate 也很重要。parse 是让计算机能够理解代码,理解了才有后续分析和转换可言,而 generate 则是根据不同的目标平台支持的指令集、api 等来生成合适的代码,要做指令选择,生成性能更优的代码。但是 parser 可以用一些 parser generator 比如 antlr 生成,generator 可以用 llvm 这种通用的优化器后端来做,所以可以不用自己搞,只要搞好语义分析和中间代码优化就可以。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享