2014-10-04-python-callstack-model

Posted on October 4, 2014

调用堆栈是程序执行时的核心结构,因此调用堆栈的设计很大程度上影响着程序运行的效率。在调用堆栈上哪怕是很小的冗余都会对程序运行效率造成很大的影响。

在这点上,C语言仍然是最最贴近机器的语言。 C语言的调用堆栈全部由CPU寄存器抽象而成, 每一个栈幁包含了栈基址(由EBP抽象),栈指针(由ESP抽象),函数返回地址,函数局部变量。 堆栈在逻辑地址空间连续构成,通过这些寄存器保证所有调用栈的操作都可以在最快的速度内完成。 结构体和原始类型都可以在栈幁上分配,但是他们的生命周期也就仅限于函数调用生命周期,调用完成,堆栈空间就会被系统回收。 一般而言,这是最贴近系统的调用堆栈模型,因为紧密贴合了底层的硬件因此也给上层的语言抽象带来了诸多的限制。

JVM构建在C语言的抽象之上,与C直接关联硬件不同,Java旨在于提供一套底层无关(跨平台)的抽象。如前所述C语言的问题在于与底层的硬件绑定非常紧密, 在一套硬件体系架构下生成的二进制文件很有可能在在另外的体系架构下无法运行,因为体系A支持的CPU指令,寄存器 体系B并不支持。 Java要提供跨平台的抽象,主要就是需要屏蔽这样的底层硬件细节。为了克服对特定指令的依赖,JVM抽象了一套指令供所有上层语言使用;为了克服对寄存器的依赖,JVM的实现版本采用了基于栈的指令运算。

在JVM内部每个运行的线程都由一个完整的调用堆栈,调用堆栈由一系列栈幁组成。每个栈幁包含了当前运行函数的信息,函数的返回值,局部变量,当前类引用,和操作数栈。 和C的栈幁不一样,java的栈幁不能自由的增长。 Java的栈幁里只包含相应对象的引用,这些引用的空间会在堆空间上分配,因此Java栈幁是根据调用的函数决定的,并不能运行时动态增长。这本身并不是很大的一个问题,并且一定程度上简化了内存空间管理。但是失去的灵活性就是对调用生命周期的对象管理, 在C里面仅仅存活于一个函数调用的对象在Java里面只剩下了原始类型, 一般的对象本来可以声明成仅仅存在于调用生命周期内的,现在要依赖于垃圾回收。

简单比较一下,JVM的栈幁的基本操作相比C语言增加了不少额外开销。 JVM的指令执行相比C语言也增加了不少额外开销。 假设是,为了Java语言的这些额外的好处,必须做出妥协。

Python的运行时和Java类似,每一个运行时栈幁都包含了:操作栈, 局部变量/全局变量/常量,父调用栈, 返回值, 调用函数的字节码对象。所有的指令操作都在操作栈上完成,结果保存回操作栈。在Python语言下,函数的地位发生了根本的变化, 在Java和C语言中,函数的内容仍然被视为特殊的对象存储在方法区,甚至单独分割成一段地址段。 而在Python里,函数成了一等公民:对象。 函数的字节码被视为函数对象的数据,随对象实例一起存储。 Python的函数对象并不依赖于函数栈幁而存在。 栈幁的执行过程和JVM也并不相同,JVM更倾向于对底层硬件的抽象,每一条指令都相对自成一体。 Python的执行器则更高屋建瓴,Python对执行栈的抽象远离Java/c的函数栈,对Python指令的解释可能要借助上下文的信息。 因此Java的运行时被成为虚拟机,而Python得运行时被称为解释器。 这种执行栈上的复杂性是Python为它的灵活性所付出的代价。

==