python常量池_python3 常量池

最近有同事分享了一次java基础,里面提到了java基础类型的包装类型(比如Integer)和String都有自己的常量池,我突然想到在

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

java常量池

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

这也就是为什么:

Integer i1 = 100;

Integer i2 = 100;

1

2

3

Integeri1=100;

Integeri2=100;

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

Integer i2 = 200;

Integer i2 = 200;

1

2

3

Integeri2=200;

Integeri2=200;

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

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

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

python整型常量池

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

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

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

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

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

aisb# True

printid(a)

printid(b)

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

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

aisb# False

printid(a)

printid(b)

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

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

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

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

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

这是个问题?

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

这是设计缺陷?

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

证明

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

typedef struct{

PyObject_HEAD;

long ob_ival;

} PyIntObject;

1

2

3

4

5

typedefstruct{

PyObject_HEAD;

longob_ival;

}PyIntObject;

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

#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

staticPyIntObject *small_ints[NSMALLNEGINTS+NSMALLPOSINTS];

#endif

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

struct _intblock{

struct _intblock *next;

PyIntObject objects[N_INTOBJECTS];

};

1

2

3

4

5

struct_intblock{

struct_intblock *next;

PyIntObjectobjects[N_INTOBJECTS];

};

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

typedef struct _inblock PyIntBlock;

static PyIntBlock *block_list = NULL;

static PyIntObject *free_list = NULL;

1

2

3

4

5

typedefstruct_inblockPyIntBlock;

staticPyIntBlock *block_list=NULL;

staticPyIntObject *free_list=NULL;

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

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

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

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

最后我在网上找到这样一种解答:

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

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

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

defm():

a=1

printid(a)

defn():

b=1

printid(b)

if__name__=="__main__":

m()

n()

defm():

a=1000

printid(a)

defn():

b=1000

printid(b)

if__name__=="__main__":

m()

n()

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

2018-05-29-012523.png

2020012309512985.png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值