Theano教程:Python的内存管理

在写大型程序时候的一大挑战是如何保证最少的内存使用率。但是在Python中的内存管理是比较简单的。Python显示分配内存,使用引用计数系统管理对象,当指向某一个对象的引用数变为 0 的时候,该对象所占的内存就会被释放。理论上听起来很不错,也很简单,但是在实践中,我们需要知道一些Python内存管理的知识从而让程序在运行过程中能够更加高效地使用内存。其中一个方面我们需要知道的是基本的Python对象所占空间的大小,另一方面我们需要知道的是Python在内部到底是如何管理内存的。

基本对象

一个 int 对象占多大空间呢? C/C++程序员会说它是由具体的机器决定的,可能是32为或者64位,因此它最多占8个字节(一个字节8位)。那么在Python中也是如此吗?

下面写一个函数来揭示出对象占多大的空间(某些情况下需要递归,比如某一个对象类型不是基本的数据类型):

 1 import sys
 2
 3 def show_sizeof(x, level=0):
 4
 5     print "\t" * level, x.__class__, sys.getsizeof(x), x
 6
 7     if hasattr(x, ‘__iter__‘):
 8         if hasattr(x, ‘items‘):
 9             for xx in x.items():
10                 show_sizeof(xx, level + 1)
11         else:
12             for xx in x:
13                 show_sizeof(xx, level + 1)

我们可以用下面的函数调用来观察不同的基本数据类型所占空间大小:

show_sizeof(None)
show_sizeof(3)
show_sizeof(2**63)
show_sizeof(102947298469128649161972364837164)
show_sizeof(918659326943756134897561304875610348756384756193485761304875613948576297485698417)

在64-bit系统和2.7.8 Python上运行的结果:

 <type ‘NoneType‘> 16 None
 <type ‘int‘> 24 3
 <type ‘long‘> 36 9223372036854775808
 <type ‘long‘> 40 102947298469128649161972364837164
 <type ‘long‘> 60 918659326943756134897561304875610348756384756193485761304875613948576297485698417

可以看到None占了16个字节,int 占了24个字节,是64为系统中C的int64_t 的 3 倍,而且是能够被机器识别的整型。长整型(无限制的精确度)用来表示出了大于263 - 1的整数,所占空间最小为36个字节。而且这个所占空间大小会随着算法中整数的大小线性增长。

Python的float是特定实现的,看上去类似于C中double,但是Python中的 float 不会在数据超过8个字节时终止表示:

show_sizeof(3.14159265358979323846264338327950288)

在64为系统输出:

<type ‘float‘> 24 3.14159265359

可以看到又是C中double类型所占空间(8字节)的3倍.

那么对于字符串呢?

show_sizeof("")
show_sizeof("My hovercraft is full of eels")

在64位系统输出:

 <type ‘str‘> 33
 <type ‘str‘> 62 My hovercraft is full of eels

空字符串占33字节,随着字符串内容增加,所占空间线性增长。



下面测试常用的tuple,list 和 dictionary所占空间大小(均为在64为系统下的输入结果):

show_sizeof([])
show_sizeof([4, "toaster", 230.1])

输出:

 <type ‘list‘> 64 []
 <type ‘list‘> 88 [4, ‘toaster‘, 230.1]

空list占64个字节,而64位系统中的C++ std::list() 只占16个字节,达到了4倍。

对于tuple呢?dictionary?:

show_sizeof({})
show_sizeof({‘a‘:213, ‘b‘:2131})

输出:

 <type ‘dict‘> 272 {}
 <type ‘dict‘> 272 {‘a‘: 213, ‘b‘: 2131}
    <type ‘tuple‘> 64 (‘a‘, 213)
        <type ‘str‘> 34 a
        <type ‘int‘> 24 213
    <type ‘tuple‘> 64 (‘b‘, 2131)
        <type ‘str‘> 34 b
        <type ‘int‘> 24 2131

可以看出,对于字典中的每一个 key/value 对,占64字节,但是注意(‘a‘, 213)所占空间是64字节,而 ‘a‘ 所占空间是34字节,213 占空间是24字节,所以留出64 -(34+24) = 6字节给key/value本身;另外,我们看到整个字典占272字节,而不是64+64 = 128字节。字典本身是被设计成一个搜索效率高的数据结构,所以会用到必要的额外的空间。如果字典内部采用的是某种树结构,必须考虑到包含每一个值的节点和指向孩子节点的两个指针的空间消耗;如果字典内部采用哈希表实现,我们必须保证有足够的空闲空间从而保证性能。

字典与C++std::map结构对等,而C++的map在创建(空map)时占48个字节, C++空字符串占 8 个字节,整数占4个字节。

观察到了这么多现象,到底是怎么回事?看上去一个空字符串占8个字节还是占37个字节似乎改变不了什么。如果不扩展数据大小,确实如此。我们必须关心的是我们创建多少个对象会到达程序程序所使用的内存的限制。在实践应用中,这个问题很棘手。要想设计出一个管理内存的好策略,不但需要关心对象所占内存的大小,还需要所创建对象的数量以及这些对象的创建顺序,事实证明这对于Python很重要。一个关键元素就是理解Python是如何在内部分配内存的,也正是下面即将讨论的.

内部内存管理

为了加速内存分配(和重复使用),Python对小型对象使用列表来管理。每个列表包含的对象所占空间大小都很相近:如一个列表包含的对象均占1到8个字节,另一个列表包含的对象均占9到16个字节等。当需要创建一个小型对象时,要么重复使用列表中空闲块,要么分配一块新空间。

事实上,即使一个对象的空间被free了,它做占据的内存空间也不会被返回给Python的全局内存池,而是仅仅被标记为free然后加入到空闲列表。过期的(被消亡)对象的位置空间会在一个新的差不多大小的对象被创建时,进行重复使用,如果没有过期的对象释放的空间存在,那么就直接新分配空间。

如果小型对象的所占内存从未被释放,那么列表所占内存空间就会一直增大,那么内存慢慢就会被这些大量的小型对象占据。

因此,我们应该努力只分配空间给那些有必要的对象,在循环中只创建少量的对象,尽量使用生成器语法。

事实上,列表占据空间的自由增长似乎并不算是一个问题,以为呢列表所包含的空间仍然运行Python程序进入和使用。但是从操作系统的视角来看,程序所占内存的大小会超过系统分配给Python的总内存的大小。

为了证明上面所述,使用memory_profiler(依赖于 python-psutil包)来证明:

 1 import copy
 2 import memory_profiler
 3
 4 #这里加上@profile是来监视具体函数function的内存使用情况
 5 @profile
 6 def function():
 7     x = list(range(1000000))  # allocate a big list
 8     y = copy.deepcopy(x)
 9     del x
10     return y
11
12 if __name__ == "__main__":
13     function()

在Ubuntu上运行:

程序创建了包含1,000,000个int值(1,000,000*12 bytes = ~11.4MB),建立一个对list的引用变量x(1,000,000 * 8 bytes =~ 3.8MB), 总内存使用量大约为15.2MB.然后copy.deepcopy 进行深度拷贝操作和建立新的引用变量y,同样需要占用内存大约15.2MB,所以第8行的内存使用量增加了15.367MB. 注意第 9 行,del x, 内存使用量仅仅减少了3.824MB,这表明del操作只是释放了指向 list 引用变量的内存空间,而不是list中的整数所占内存空间,这些整数值保留在堆中,导致内存占用多了将近11.4MB.

在这个例子中分配了总共大约15.309 + 15.367 - 3.82 = ~26.8MB, 而我们存储一个list只需要大约11.4MB的内存,超出了1倍多! 所以,在编程中的也许我们不注意的地方,就会导致内存占用增长很快!

pickle

pickle是一种标准的把Python对象序列化到文件和以及从文件解序列化出来的方式。它的内存足迹(memory footprint)是什么? 它创建了额外的数据副本还是用一种更加聪明的方式?考虑:

 1 import memory_profiler
 2 import pickle
 3 import random
 4
 5 def random_string():
 6     return "".join([chr(64 + random.randint(0, 25)) for _ in xrange(20)])
 7
 8 @profile
 9 def create_file():
10     x = [(random.random(),
11           random_string(),
12           random.randint(0, 2 ** 64))
13          for _ in xrange(1000000)]
14
15     pickle.dump(x, open(‘machin.pkl‘, ‘w‘))
16
17 @profile
18 def load_file():
19     y = pickle.load(open(‘machin.pkl‘, ‘r‘))
20     return y
21
22 if __name__=="__main__":
23     create_file()
24     #load_file()

这个程序用来生成一些pickle 数据和读取pickle 数据(pickle数据的读取在这里注释了,首先没用让读取函数运行),使用memory_profiler,生成pickle数据过程中使用了大量内存:

再看看pickle数据的读取(把上面程序中第23行注释掉,把24行的注释去掉):

所以,pickle是非常消耗内存的做法,从上面的图看出,在数据的创建时,大约使用127MB内存,而一个pickle.dump操作就要额外使用差不多与数据相当的内存空间(117MB).

在unpickle操作中(即反序列化操作,从pkl中读取数据),看上去效率还好点,虽然确实占用了比原始数据(127MB)大的内存空间(188MB),但是还没到达有超1倍的程度。

总之,涉及pickle的操作应该在对内存容量要求较高的程序中尽量避免。那么,有没有可以替代的选择呢?我们知道pickle保存了数据结构的结构,即将数据原封不动保存起来(不仅仅保存数据,还要保存数据的结构信息),所以我们才能在需要的时候,将数据从pickle文件中恢复出来。但是,并不是所有时候都需要这样用pickle保存,就像上面例子中的list,完全可以用一个基于文本的文件格式按顺序保存里面的元素,没必要用pickle来保存:

 1 import memory_profiler
 2 import random
 3 import pickle
 4
 5 def random_string():
 6     return "".join([chr(64 + random.randint(0, 25)) for _ in xrange(20)])
 7
 8 @profile
 9 def create_file():
10     x = [(random.random(),
11           random_string(),
12           random.randint(0, 2 ** 64))
13          for _ in xrange(1000000) ]
14     # 这里使用文本来保存数据而不是pickle
15     f = open(‘machin.flat‘, ‘w‘)
16     for xx in x:
17         print >>f, xx
18     f.close()
19
20 @profile
21 def load_file():
22     y = []
23     f = open(‘machin.flat‘, ‘r‘)
24     for line in f:
25         y.append(eval(line))
26     f.close()
27     return y
28
29 if __name__== "__main__":
30     create_file()
31     #load_file()

建立文件时,内存足迹:

与上面pickle保存数据对比,可以发现,通过文本保存文件值占用几乎可以忽略的内存。

下面再来看看数据的读取时,内存足迹变化(将30行的代码注释,将31行的注释符去掉):

原始数据127MB,读取时占用内存139MB,和原始数据很接近,多出来的约10MB内存空间是分配给循环中产生的临时变量。

这个例子可以启示我们在处理数据的时候不要首先全部读取数据,然后再处理数据,而是每次读取几项,处理完这几项,释放这几项的空间,然后再读取几项处理,以此类推,这样,之前分配过的内存空间就可以重复使用。比如读取数据到一个Numpy的array中,我们可以先创建一个空array,然后逐行读取数据,逐行填入array,这样大约只需要和数据大小差不多的内存空间。如果使用pickle, 至少要分配2倍于数据大小的内存空间:一次是pickle在load时分配占用,一次是创建存储数据的array.

总结

Python 设计的目标根本上就不同于 C 语言设计的目标。后者是以更加复杂和显示的编程为代价让程序员能够更好地控制程序要做的事,而前者设计的目的是让代码更加迅速并且尽量隐藏细节。尽管听起来不错,但是在生产环境中,忽略执行效率会栽大跟头,所以在Python代码设计过程中,知道哪些代码执行的效率很低,从而尽量避免这种低效率编写对于生产环境来说很重要!

资料来源:http://deeplearning.net/software/theano/tutorial/python-memory-management.html#python-memory-management

时间: 2024-10-07 22:59:17

Theano教程:Python的内存管理的相关文章

[转载] python的内存管理机制

本文为转载,原作为http://www.cnblogs.com/CBDoctor/p/3781078.html,请大家支持原作者 先从较浅的层面来说,Python的内存管理机制可以从三个方面来讲 (1)垃圾回收 (2)引用计数 (3)内存池机制 一.垃圾回收: python不像C++,Java等语言一样,他们可以不用事先声明变量类型而直接对变量进行赋值.对Python语言来讲,对象的类型和内存都是在运行时确定的.这也是为什么我们称Python语言为动态类型的原因(这里我们把动态类型可以简单的归结

python的内存管理机制

先从较浅的层面来说,Python的内存管理机制可以从三个方面来讲 (1)垃圾回收 (2)引用计数 (3)内存池机制 一.垃圾回收: python不像C++,Java等语言一样,他们可以不用事先声明变量类型而直接对变量进行赋值.对Python语言来讲,对象的类型和内存都是在运行时确定的.这也是为什么我们称Python语言为动态类型的原因(这里我们把动态类型可以简单的归结为对变量内存地址的分配是在运行时自动判断变量类型并对变量进行赋值). 二.引用计数: Python采用了类似Windows内核对象

python的内存管理机制(zz)

本文转载自:http://www.cnblogs.com/CBDoctor/p/3781078.html 先从较浅的层面来说,Python的内存管理机制可以从三个方面来讲 (1)垃圾回收 (2)引用计数 (3)内存池机制 一.垃圾回收: python不像C++,Java等语言一样,他们可以不用事先声明变量类型而直接对变量进行赋值.对Python语言来讲,对象的类型和内存都是在运行时确定的.这也是为什么我们称Python语言为动态类型的原因(这里我们把动态类型可以简单的归结为对变量内存地址的分配是

你真的了解Python吗 ---Python的内存管理

请看下面的一段代码: origin = {'a':100,'b':[1,2,34,5]} obj_copy ={}; print origin; obj_copy['key1']= origin; obj_copy['key2']= origin; print(obj_copy) print('我们试图改变obj_copy中某个Key值的内容') obj_copy['key1']['a'] = 10000 print(obj_copy) obj_copy['key1']['b'] = "hell

python的内存管理与垃圾回收机制学习

一.python内存申请: 1.python的内存管理分为六层:最底的两层有OS控制.第三层是调用C的malloc和free等进行内存控制.第四层第五层是python的内存池.最上层使我们接触的直接对python对象进行操作. 2.python申请对象时候小于256Byte的字节申请回直接使用python自己的内存分配系统,当大于256Byte的时候会调用malloc直接分配一个256k的大内存空间.释放内存空间时候会回收到内存池中而不是直接调用free释放掉. 3.深浅拷贝的不同(id?内存地

《python解释器源码剖析》第17章--python的内存管理与垃圾回收

17.0 序 内存管理,对于python这样的动态语言是至关重要的一部分,它在很大程度上决定了python的执行效率,因为在python的运行中会创建和销毁大量的对象,这些都设计内存的管理.同理python还提供了了内存的垃圾回收(GC,garbage collection),将开发者从繁琐的手动维护内存的工作中解放出来.这一章我们就来分析python的GC是如何实现的. 17.1 内存管理架构 在python中内存管理机制是分层次的,我们可以看成有四层,0 1 2 3.在最底层,也就是第0层是

Python深入06 Python的内存管理

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 语言的内存管理是语言设计的一个重要方面.它是决定语言性能的重要因素.无论是C语言的手工管理,还是Java的垃圾回收,都成为语言最重要的特征.这里以Python语言为例子,说明一门动态类型的.面向对象的语言的内存管理方式. 对象的内存使用 赋值语句是语言最常见的功能了.但即使是最简单的赋值语句,也可以很有内涵.Python的赋值语句就很值得研究. a = 1 整数1为一个对象.而a

Python的内存管理

语言的内存管理是语言设计的一个重要方面. 它是决定语言性能的重要因素. 不管是C语言的手工管理,还是Java的垃圾回收,都成为语言最重要的特征. 这里以Python语言为样例,说明一门动态类型的.面向对象的语言的内存管理方式. 对象的内存使用 赋值语句是语言最常见的功能了. 但即使是最简单的赋值语句.也能够非常有内涵. Python的赋值语句就非常值得研究. a = 1 整数1为一个对象. 而a是一个引用.利用赋值语句.引用a指向对象1. Python是动态类型的语言(參考动态类型),对象与引用

Python的内存管理 小理解

请看下面的一段代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 origin = {'a':100,'b':[1,2,34,5]} obj_copy ={}; print origin; obj_copy['key1']= origin; obj_copy['key2']= origin; print(obj_copy) print('我们试图改变obj_copy中某个Key值的内容') obj_copy['key1']['a'] = 10000 pri