编译原理(7):运行存储分配
声明:本系列文章,是根据中国大学MOOC网 哈工大的编译原理 这门课学习而成的学习笔记。
运行存储分配
一、运行存储分配概述
运行存储分配策略
- 编译器在工作过程中,必须为源程序中出现的一些数据对象分配运行时的存储空间
- 对于那些在编译时刻就可以确定大小的数据对象,可以在编译时刻就为它们分配存储空间,这样的分配策略称为静态存储分配
- 反之,如果不能在编译时完全确定数据对象的大小,就要采用动态存储分配的策略。即在编译时仅产生各种必要的信息,而在运行时刻,再动态地分配数据对象的存储空间
- 栈式存储分配
- 堆式存储分配
运行时内存的划分
活动记录
- 使用过程(或函数、方法)作为用户自定义动作的单元的 语言,其编译器通常以过程为单位分配存储空间
- 过程体的每次执行称为该过程的一个活动(activation)
- 过程每执行一次,就为它分配一块连续存储区,用来管理过程一次执行所需的信息,这块连续存储区称为活动记录( activation record )
活动记录的一般形式
二、静态存储分配
静态存储分配
- 在静态存储分配中,编译器为每个过程确定其活动记录在目标程序中的位置
- 这样,过程中每个名字的存储位置就确定了
- 因此,这些名字的存储地址可以被编译到目标代码中
- 过程每次执行时,它的名字都绑定到同样的存储单元
静态存储分配的限制条件
- 适合静态存储分配的语言必须满足以下条件
- 数组上下界必须是常数
- 不允许过程的递归调用
- 不允许动态建立数据实体
- 满足这些条件的语言有BASIC和FORTRAN等
常用的静态存储分配方法
- 顺序分配法
- 层次分配法
顺序分配法
- 按照过程出现的先后顺序逐段分配存储空间
- 各过程的活动记录占用互不相交的存储空间
层次分配法
通过对过程间的调用关系进行分析,凡属无相互调用关系的并列过程,尽量使其局部数据共享存储空间
层次分配算法
三、栈式存储分配
栈式存储分配
- 有些语言使用过程、函数或方法作为用户自定义动作的单元,几乎所有针对这些语言的编译器都把它们的(至少一部分的)运行时刻存储以栈的形式进行管理,称为栈式存储分配
- 当一个过程被调用时,该过程的活动记录被压入栈;当过程结束时,该活动记录被弹出栈
- 这种安排不仅允许活跃时段不交叠的多个过程调用之间共享空间,而且允许以如下方式为一个过程编译代码:它的非局部变量的相对地址总是固定的,和过程调用序列无关
活动树
- 用来描述程序运行期间控制进入和离开各个活动的情况的树称为活动树
- 树中的每个结点对应于一个活动。根结点是启动程序执行的main过程的活动
- 在表示过程p的某个活动的结点上,其子结点对应于被p的这次活动调用的各个过程的活动。按照这些活动被调用的顺序,自左向右地显示它们。一个子结点必须在其右兄弟结点的活动开始之前结束
最开始main进栈,r进栈
r调用完毕,人出栈,q(1,9)进栈,同时,q(1,9)调用p(1,9),故p(1,9)进栈
P(1,9)调用结束,出栈,同时q(1,9)继续调用q(1,3),q(1,3)进栈,q(1,3)调用p(1,3),p(1,3)进栈。
设计活动记录的一些原则
四、调用序列和返回序列
调用序列和返回序列
- 过程调用和过程返回都需要执行一些代码来管理活动记录栈,保存或恢复机器状态等
- 调用序列
实现过程调用的代码段。为一个活动记录在栈中分配空间,并在此记录的字段中填写信息 - 返回序列
恢复机器状态,使得调用过程能够在调用结束之后继续执行
- 调用序列
- 一个调用代码序列中的代码通常被分割到调用过程(调用者)和被调用过程(被调用者)中。返回序列也是如此
调用序列
返回序列
调用者和被调用者之间的任务划分
变长数据的存储分配
- 在现代程序设计语言中,在编译时刻不能确定大小的对象将被分配在堆区。但是,如果它们是过程的局部对象,也可以将它们分配在运行时刻栈中。尽量将对象放置在栈区的原因:可以避免对它们的空间进行垃圾回收,也就减少了相应的开销
- 只有一个数据对象局部于某个过程,且当此过程结束时它变得不可访问,才可以使用栈为这个对象分配空间
访问动态分配的数组
五、非局部数据的访问
非局部数据的访问
一个过程除了可以使用过程自身定义的局部数据以外,还可以使用过程外定义的非局部数据
语言可以分为两种类型
- 支持过程嵌套声明的语言
可以在一个过程中声明另一个过程 ,例: Pascal - 不支持过程嵌套声明的语言
不可以在一个过程中声明另一个过程,例:C
支持过程嵌套声明的语言
不支持过程嵌套声明的语言
无过程嵌套声明时的数据访问
变量的存储分配和访问
- 全局变量被分配在静态区,使用静态确定的地址访问它们
- 其它变量一定是栈顶活动的局部变量。可以通过运行时刻栈的top_sp指针访问它们
有过程嵌套声明时的数据访问
嵌套深度
-
过程的嵌套深度
不内嵌在任何其它过程中的过程,设其嵌套深度为1
如果一个过程p在一个嵌套深度为i的过程中定义,则设定p 的嵌套深度为i +1 -
变量的嵌套深度
将变量声明所在过程的嵌套深度作为该变量的嵌套深度
访问链 (Access Links)
- 静态作用域规则:只要过程b的声明嵌套在过程a的声明中,过程b就可以访问过程a中声明的对象
- 可以在相互嵌套的过程的活动记录之间建立一种称为**访问链(Access link)**的指针,使得内嵌的过程可以访问外层过程中声明的对象
- 如果过程b在源代码中直接嵌套在过程a中(b的嵌套深度比a的嵌套深度多1),那么b的任何活动中的访问链都指向最近的a的活动
访问链的建立
六、堆式存储分配
堆式存储分配
- 堆式存储分配是把连续存储区域分成块,当活动记录或其它对象需要时就分配
- 块的释放可以按任意次序进行,所以经过一段时间后,对可能包含交错的正在使用和已经释放的区域
堆式存储分配的示意图
申请
设当前自由块总长为M,欲申请长度为n
如果存在若干个长度大于或等于n的存储块,可按以下策略之一进行存储分配
- 取长度m满足需求的第1个自由块,将长度为m-n的剩余部 分仍放在自由链中
- 取长度m满足需求的最小的自由块
- 取长度m满足需求的最大的自由块
如果不存在长度大于或等于n的存储块
- 如果M≥n,将自由块在堆中进行移位和重组(对各有关部分都需作相应的修改,是一件十分复杂和困难的工作)
- 如果M<n,则应采用更复杂的策略来解决堆的管理问题
释放
只需将被释放的存储块作为新的自由块插入自由链中,并删除已占块记录表中相应的记录即可
为实现堆式存储管理
- 须完成大量的辅助操作
如排序、查表、填表、插入、删除、…… - 其空间和时间的开销较大
七、符号表
符号表的组织
为每个作用域(程序块)建立一个独立的符号表