python3 常量池

最近有同事分享了一次java基础,里面提到了java基础类型的包装类型(比如Integer)和String都有自己的常量池,我突然想到在python中也有类似的东西,所以本来想写一篇关于分析python和java常量池的博客,但是后来我发现这里面的内容还挺多的,所以我决定先从整型常量池入手,下次再分析字符串的。

需要提到的是,我只是因为找到的有关python小整型池和大整型池的资料,才特意介绍python的。我并没有找到关于java大整型对象存储技术的文献,但是我猜都能猜到,如果java真有这方面的优化需求,是一定会有类似的东西,如果之后我了解到了,我会在自己的博客中再去介绍的。

java常量池

首先是java的常量池,我搜索了java的常量池,大部分提到了,“java中基本类型的包装类的大部分都实现了常量池技术”这句话,它们都提到了对于Interger这样的对象(Byte,Short,Long,Character类似)在值小于127的时候,会默认使用内存池中已经创建好的数据。

这也就是为什么:

Python
Integer i1 = 100; Integer i2 = 100;
1
2
3
Integer i1 = 100 ;
Integer i2 = 100 ;
 

的时候, i1 == i2会返回true,而在

Python
Integer i2 = 200; Integer i2 = 200;
1
2
3
Integer i2 = 200 ;
Integer i2 = 200 ;
 

的时候, i1 == i2会返回False。

这个很好理解,首先java中 == 对对象而言,代表是否是同一引用(equal用来比较是否值相等)。还有就是我们没有 new Integer,而是直接使用看上去像是赋值的操作,因为使用了装箱操作。

这里需要提到的是如果i1和i2都是int型的基础类型,那么他们无论有多大,比较起来都是相等的,因为这部分内存是放在栈的,它们仅仅是基础类型,而非引用堆中的对象。

python整型常量池

先说明的是,python中没有基础类型这种东西,万物皆对象,而且==在比较两个整型的时候就是在比较值大小,比较是否是同一对象的引用使用is关键字。

python中的整型常量池分为小整型对象池和大整型对象池。(这个说法出自python源码分析)

对于一些常用的整型,python也是提前初始化好的:

在[-5, 257)这个区间内的整数,被称为小整数对象,类似于java常量池,是一开始就初始化好的。

Python
a = 1 b = 1 a == b # True a is b # True print id(a) print id(b)
1
2
3
4
5
6
7
a = 1
b = 1
a == b # True
a is b # True
print id ( a )
print id ( b )
 

而对于超过此区间的对象,就需要在每次需要的时候创建了。

Python
a = 1000 b = 1000 a == b # True a is b # False print id(a) print id(b)
1
2
3
4
5
6
7
a = 1000
b = 1000
a == b # True
a is b # False
print id ( a )
print id ( b )
 

上面的例子是针对解释器运行,如果是运行脚本,还有不同的地方,后面会再解释。

但是这些要创建的大整型对象,并不是直接在一块堆内存上创建的(如果是那效率就太挫了),而是维护了一个专门的数据结构,我们称其为大整型对象池。

这个池的数据结构类似于一个单向链表,每一个节点是一个可以存放python Int对象的数组。然后在每次创建python大整型INT对象的时候,如果单向链表中没有空间可用,那就会创建一块新的python Int数组空间,链接到单向链表中。否则就直接使用数组中的内存就可以了。之所以要数组和链表结合使用,就是因为数组的查找要快,而链表的释放和创建自由灵活。这种模型在内存管理中很常见。

而且你应该知道python的对象回收机制,就是在引用计数减为0的时候,这个内存就会被回收。所以对我们刚才提到的大整型对象如果没有引用指向它,它就会被python虚拟机回收。但是坑的地方是,它并不会释放给操作系统,而仅仅是回收给这个大整型对象池的free区,用于再次使用。这也就是说你的这个大整型对象池,只增不减。这听上去很像是内存泄漏。

你可以试一试,如果创建了一大堆的python int对象,你的内存将飙高到几个G,然后即使你del 了这些用于存放int对象的容器,你的python进程的内存也没有变小,也就是说除非你的python进程结束,否则这些内存永远不会还给操作系统。

这是个问题?

这其实不是问题,首先它并不是内存泄漏,严格意义来讲,内存泄漏是指无法找到内存空间了,比如c++和c中的指针的作用域没了,访问那些内存的方式你找不到了。但是实际上对于大整型内存池而言,我们可以找到这些内存,它们也可以被我们重复使用。而且现代操作系统,内存是用缺页分配的。所以不会占用那么多“真实的内存”。

这是设计缺陷?

工程上的事,往往就是在做去权衡。还有就是那些认为这是设计缺陷的人,简直就是在说“我比python作者聪明”。我仔细想了想,这些可以重复使用的内存块,它们分散在链表上数组的各处,根本没什么好的办法释放它们,所以在运行效率上权衡,就只能设计成这样了。

证明

口说无凭,看看cpython的实现源码

Python
typedef struct{ PyObject_HEAD; long ob_ival; } PyIntObject;
1
2
3
4
5
typedef struct {
     PyObject_HEAD ;
     long ob_ival ;
} PyIntObject ;
 

上面就是提到的PyIntObject的C底层数据结构。

Python
#ifndef NSMALLPOSINTS #define NSMALLPOSINTS 257 #endif #ifndef NSMALLNEGINTS #define NSMALLNEGINTS 5 #endif #if NSMALLNEGINTS + NSMALLPOSINTS > 0 static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS]; #endif
1
2
3
4
5
6
7
8
9
10
#ifndef NSMALLPOSINTS
     #define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
     #define NSMALLNEGINTS 5
#endif
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
     static PyIntObject * small_ints [ NSMALLNEGINTS + NSMALLPOSINTS ] ;
#endif
 

上面就是小整型常量池的大小定义

Python
struct _intblock{ struct _intblock *next; PyIntObject objects[N_INTOBJECTS]; };
1
2
3
4
5
struct _intblock {
     struct _intblock * next ;
     PyIntObject objects [ N_INTOBJECTS ] ;   
} ;
 

上面就是我们说的用于给大整型对象的内存块节点,可以看到,每个节点是一个PyIntObject数组。

Python
typedef struct _inblock PyIntBlock; static PyIntBlock *block_list = NULL; static PyIntObject *free_list = NULL;
1
2
3
4
5
typedef struct _inblock PyIntBlock ;
 
static PyIntBlock * block_list = NULL ;
static PyIntObject * free_list = NULL ;
 

其中block_list指针,就是大整型数组对象链表的头节点,而free_list就是可用内存(空闲内存)的头节点。

而且我们提到,被删除的PyIntObject对象,它的空间可以被重新使用,这种重新使用的方式就是指它们会以单链表的形式串连在一起,而表头就是free_list

遗留问题,解释器运行和python程序运行

前面提到的那段python代码,在python的解释器运行和作为python代码运行,结果不一样的,我猜测可能是因为解释器是逐条执行,而python代码则存在整体处理的过程。
最后我在网上找到这样一种解答:

“Cpython代码的编译单元是函数,也就是说每个函数会单独编译,对于同一个编译单元中出现相同值的常量,只会出现一份。对于不同单元的编译单元,值相同的常量不一定会应用到运行时的同一对象。”

写两个例子,就全都明白了

Python
def m(): a = 1 print id(a) def n(): b = 1 print id(b) if __name__ == "__main__": m() n() def m(): a = 1000 print id(a) def n(): b = 1000 print id(b) if __name__ == "__main__": m() n()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def m ( ) :
     a = 1
     print id ( a )
 
def n ( ) :
     b = 1
     print id ( b )
 
if __name__ == "__main__" :
     m ( )
     n ( )
def m ( ) :
     a = 1000
     print id ( a )
 
def n ( ) :
     b = 1000
     print id ( b )
 
if __name__ == "__main__" :
     m ( )
     n ( )
 

所以解释器逐条执行的,应该是不同的编译单元。




  • zeropython 微信公众号 5868037 QQ号 5868037@qq.com QQ邮箱
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值