5

《Python源码剖析》第一部分——Python对象基础

 2 years ago
source link: https://sund.site/posts/python/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Python的对象初始化

在Python中,对象就是为C中的结构体在堆上申请的一块内存,一般来说,对象是不能被静态初始化的,并且也不能在栈空间上生存。唯一的例外就是类型对象,Python中所有的内建的类型对象(如整数类型对象,字符串类型对象)都是被静态初始化的。

python 的对象不像 C 是分配在栈、堆、data segment等位置,而是全部分配在堆上!只有python内置类型在初始化时候才是被C语言层静态初始化。

PyObject内部就两样:引用计数器、类型对象指针。

类型对象的定义:

typedef struct _typeobject {
    PyObject_VAR_HEAD
    char *tp_name; /* For printing, in format "<module>.<name>" */
    int tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */
    destructor tp_dealloc;
    printfunc tp_print;
    ……
    /* More standard operations (here for binary compatibility) */
    hashfunc tp_hash;
    ternaryfunc tp_call;
    ……
} PyTypeObject;

PyObject_VAR_HEAD是可变类型的头信息,其中除了PyObject_HEAD的内容外,额外添加了一个代表该对象元素数量的整型。从上边代码可见,python的类型也是一个可变对象。

Python的多态

Python中所有类型在初始化后,在C语言层面都使用同一种指针PyObject *,所以python实现多态就非常容易。任何函数的参数都是一个PyObject类型指针,也就不存在编译器需要判断函数参数类型。

Python对象内存池

Python为了避免频繁的释放对象,采用了内存池的机制,在对象引用计数为0时,不会释放内存,而是将内存交还给内存池供python重新分配使用。每一种python类型,都有特定的内存池机制。

-5至257之间的小整数,存储在「小整数数组」里,这个数组Python自动创建,每次创建一个小整数,就指向这个数组里对应的PyIntObject值并把PyIntObject的计数加1。(因此在-5到257之间的数实际指向同一片内存空间,整数-5和-5的内存地址肯定是一样的)

大整数则由一个叫block_list的链表管理,每次分配一个大整数就在free_list(一个指向空闲内存block的指针)里拿出一个节点并把free_list后移一个block。关于free_list是如何把尚未分配的内存和已被释放的内存链接起来的,可以参见书中113页的插图理解。

值得注意的是,python用于分配给整型的堆内存是不会自行销毁的,而是不断复用。也就是说,同一时间如果同时使用的整型太多,会消耗大量内存,并且这些内存在python关闭之前一直被python持有着。

字符串对象

typedef struct {
    PyObject_VAR_HEAD
    long ob_shash;
    int ob_sstate;
    char ob_sval[1];
} PyStringObject;

在Python源码中的注释显示,预存字符串的hash值(为了节省字符串比较的时间)和这里的intern机制将Python虚拟机的执行效率提升了20%。

intern机制 将新建的字符串缓存在一个PyDictObject里,相同的字符串共用同一内存。

单一字符的字符串,除了用intern缓存外,还会缓存在系统自带的一个字符串缓冲池里:

static PyStringObject *characters[UCHAR_MAX + 1]; 

+操作符和join的效率问题 +连接n个操作符会创建n-1次临时空间,join会直接处理一个list里的字符串,只分配一次内存。节省开销。

参见C++ vector对象的存储方式。

Python使用散列表(时间复杂度O(1))而非红黑树(时间复杂度O(logN))来存储map结构。

**Hash冲突(碰撞)**不同的值映射到相同的键时,就产生了冲突。一般解决办法有:

  • 开链法(哈希桶):

  • 开放定址法: hash一次没有命中就再hash一次,直到找到为止……(二次探测)

小于8个元素的Dict,python使用PyDIctObject内部的smalltable数组保存元素内容。

PyDictObject对String类型的key做了特殊对待——简化了计算hash函数的过程(正常情况下key值是一个PyObject *对象,需要做大量类型判断,但是对PyStringObject就省了)。

**装载率(使用的空间/预先分配的空间)**大于2/3时,hash冲突的概率会急速升高,这时python就会动态分配更多的空间。与其他类型一样,如果装载率太小,也会自动缩减分配的空间。

在确定新的table的大小时,通常选用的策略是时新的table中entry的数量是现在table中Active态entry数量的4倍,选用4倍是为了使table中处于Active态的entry的分布更加稀疏,减少插入元素时的冲突概率……所以当table中Active态的entry数量非常大时,Python只会要求2倍的空间,这次又是以执行速度来交换内存空间。

注意这段话,执行速度和内存大小是反比关系,划分的空间越大,执行一次查找就越费时,所以分配的内存空间不是越大越好。

PyDictObject也使用了同PyListObject一样的缓冲池方式。参考列表对象部分内容。

笔者总结:从上面的各种类型的处理规律可以总结出Python遵循的原则:小变量缓存,大变量尽量整块分配内存,回收变量时不释放内存而是尽量复用,预分配的空间既要满足需要又不能太大(太大就缩减)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK