理论教育 编译器设计之路:存储分配技巧

编译器设计之路:存储分配技巧

时间:2023-11-04 理论教育 版权反馈
【摘要】:图8-4 存储分配示例根据先前的分析,编译器会为这个程序分配四个数据块,即全局数据块、aa函数数据块、bb函数数据块、main函数数据块。在编译技术中,通常,将这种分配方式称为“静态存储分配”。在i386体系结构的目标机上,讨论静态存储分配的意义不是很大,这是因为这种方案本身仍然存在一定的不足之处。从表面上来看,静态存储分配方案似乎解决了存储分配的基本问题,事实却并非如此。

编译器设计之路:存储分配技巧

本小节将详细分析基于数据块的存储分配,其中将涉及一些非常有趣的话题。那么,解决存储分配问题的关键是什么?简单来说,就是保证用户程序的各种数据对象都可以被正确地存取访问,不至于产生冲突。当然,这是基于语言的相关规则而言的,也就是说,在数据对象的作用域内,必须保证存取是有效的。下面看一个例子,如图8-4所示。

978-7-111-32164-4-Chapter08-5.jpg

图8-4 存储分配示例

根据先前的分析,编译器会为这个程序分配四个数据块,即全局数据块、aa函数数据块、bb函数数据块、main函数数据块。这里,为了便于讨论,在局部变量的名字前冠以函数名,全局变量则不作更名。下面,就来看看这个程序的存储分配方案。

首先看一个最容易想到的解决方案,在不考虑空间回收的情况下,将各数据块(共8字节)依次分配到静态区即可。当然,此时的全局变量、局部变量的差异仅仅是一种语义范畴,它们的实际生存期是完全一样的。这种方案既不能有效地利用存储空间,也不能很好地体现变量生存期的差异,因此,其可行性比较差。

那么,是否可以考虑对这一分配方案进行合理的优化呢?由于局部变量的生存期仅限于函数执行期间,因此,某些函数数据块是可以共享同一存储区的。不过,这种共享是有前提的,就是必须保证函数之间不可以存在任何调用关系,包括直接调用或间接调用。在上例中,main、aa之间是存在调用关系的,所以不能共享。而它们与bb之间是不存在调用关系的,故它们与bb是可以共享同一数据块的。因此,为上例分配6B的存储空间是可以保证程序正确性的。至于分析函数的调用关系对于编译器来说应该是易如反掌的。在编译技术中,通常,将这种分配方式称为“静态存储分配”。早期的Fortran编译器就是采用这种分配方案的,其可行性较先前要好一些。当然,在现代编译技术中,静态存储分配的实现可能比这里所讨论的方案要更优,并不满足于仅仅分析函数的调用关系,而是更深入地分析函数的控制流,试图在更大程度上实现空间的共享。然而,这种分析算法的实现却不简单,尤其是当语言支持指针时,编译器可能需要付出更大的代价。在i386体系结构的目标机上,讨论静态存储分配的意义不是很大,这是因为这种方案本身仍然存在一定的不足之处。不过,在一些不支持堆栈结构的目标机上,静态存储分配可能是唯一的可选方案。(www.daowen.com)

从表面上来看,静态存储分配方案似乎解决了存储分配的基本问题,事实却并非如此。这种分配方案的不足之处就在于它无法解决函数递归调用的问题。这是因为递归调用的深度是编译器无法预知的,当然,编译器也就不可能以静态方式分配存储空间。实践证明,即使是经典的Fortran编译器同样无法解决这一问题,因此,必须寻求更合适的动态分配方案。在现代编译技术中,提出了两种动态分配策略:栈式存储分配、堆式存储分配。

栈式存储分配:将过程或函数的局部数据对象分配在栈空间中。申请与回收存储空间付出的代价较小,便于实现。

堆式存储分配:与栈式存储不同,堆的管理通常离不开操作系统的协助。一般而言,操作系统会提供一个接口,用于申请与释放堆的空间。编译器同样生成相应的语句调用这个接口,实现空间的申请与释放。

在实际应用中,关于栈式存储分配的观点比较一致,一般都是由编译器管理、维护的,很少有语言会将栈的管理接口开放给程序员的,因此,对于程序员来说,栈结构是透明不可见的。不过,关于堆式存储分配的观点就不太一致了,不同的语言在这个问题上的实现差异比较大。有些语言将申请与释放堆空间的接口显式地暴露给程序员,一切基于堆的管理完全由程序员控制,相应的责任也由程序员承担,典型的例子就是C、Pascal。然而,有些语言仅开放申请接口,而释放空间的琐事就由垃圾回收机制来完成,以减轻程序员维护堆的负担,例如,Java、C#等。当然,也有些语言通过某些特殊语法机制隐藏了堆的管理,让程序员感觉不到堆的存在。这两种分配策略并不是互斥的,更多时候,它们是相互合作、互为补充的。下面详细分析这两种分配策略的基本思想。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈