基础教材系列:编译原理——B站笔记
一、编译器是什么
源程序→预处理器→经过预处理的源程序→编译器→汇编语言程序→汇编器→可重定位的机器代码→链接器/加载器→目标机器代码。
编译器的结构:
与源语言相关:字符流→词法分析器→词法单元流→语法分析器→语法树→语义分析器→语法树→中间代码生成器→
中间表示形式→机器无关代码优化器→中间表示形式→
与目标语言相关:目标代码生成器→目标机器语言→机器相关代码优化器→目标机器语言
二、编译时,内存如何分配?
运行时存储分配——堆栈内存分配策略
静态存储分配:对于编译时刻就可以确定大小的数据对象,可以在编译时刻就为它们分配存储空间。
动态存储分配:编译时仅产生各种必要的信息,在运行时刻再动态地分配数据对象的存储空间。
堆和栈是相对于动态而言的。
所以运行时内存分为以下三大部分,动态区又有自己的三部分。
静态代码区 | |
静态数据区 | |
动态数据区域 | 栈区 |
空闲内存 | |
堆区 |
栈和堆分别在空闲内存地址两端,他俩大小随程序运行而变化。
栈存储了活动记录,过程调用时生成并进栈,过程返回时出栈,
堆存放长生命周期的数据对象
静态存储分配的限制条件:数组上下界必须是常数;不允许过程的递归调用;不允许动态建立数据实体;
栈式存储分配:对于函数(或者叫过程、方法),几乎所有编译器都把他们的运行时刻存储以栈的形式进行管理,称为栈式存储分配。
当一个过程被调用时,该过程的活动记录被压入栈;当过程结束时,该活动记录被弹出栈。
这种安排不仅允许活跃时段不交叠的过程调用之间共享空间,而且允许以如下方式为一个过程编译代码:它的非局部变量的相对地址总是固定的,和过程调用序列无关。
(扩展概念——活动树:用来描述程序运行期间控制进入和离开各个活动的情况的数称为活动树。
树中每个结点对应于一个活动。根结点是启动程序执行的main函数的活动。
在表示函数p的某个活动的结点上,其子结点对应于被p的这次活动调用的各个过程的活动。按照这些活动被调用的顺序,自左向右地显示它们。一个子结点必须在其右兄弟结点的活动开始之前结束。)
变长数据的存储分配:在编译时刻不能确定大小的对象,将被分配在堆区。但是,如果它们是过程的局部对象,也可以将它们分配在运行时刻栈中。
尽量将对象放置在栈区的原因:可以避免对它们的空间进行垃圾回收,也就减少了相应的开销。
只有一个数据对象局部于某个函数,且当此函数结束时它变得不可访问,才可以使用栈为这个对象分配空间。
访问链:静态作用域规则——只要函数b的声明,嵌套在函数a的声明里,那么函数b就可以访问函数a中声明的对象。
可以在相互嵌套的函数的活动记录之间,建立一种称为访问链的指针,使得内嵌的函数访问外层函数中声明的对象。
如果函数b在源码中,直接嵌套在函数a中,那么b的人和活动中的访问链都指向最近的a的活动。
三、既然这样编译,那代码就这样优化
要优化先要梳理代码,就是将代码看作流图,流图由若干块儿组成。
优化分类:
机器无关优化——针对中间代码;机器相关优化——针对目标代码;
或局部代码优化——单个基本块范围的优化;全局代码优化——面向多个基本块的优化
常用的优化方法:删除公共子表达式、常量合并、代码移动、强度削弱、删除归纳变量。
什么是代码移动:指那些不管循环多少次都得到相同结果的表达式,即循环不变计算,在进入循环前就对他们求值。
什么是强度削弱:用快的操作代替慢的操作,如用加代替乘。
什么是归纳变量:对于一个变量x,如果存在一个正或负的常数c使得每次x被赋值时,他的总值总是增加c,那么x就称为归纳变量。在沿着循环运行时,如果有一组归纳变量的值得变化保持步调一致,常常可以将这组变量删除为只剩一个
删除公共子表达式:意思就是公共部分的运算,可以用赋值的形式代替。另外,赋值操作,也尽量能省则省。
综上,代码优化的宗旨就是减少计算>赋值代替>减少赋值