实际项目中,pythoner更加关注的是Python的性能问题,之前也写过一篇文章《Python性能优化》介绍Python性能优化的一些方法。而本文,关注的是Python的内存优化,一般说来,如果不发生内存泄露,运行在服务端的Python代码不用太关心内存,但是如果运行在客户端(比如移动平台上),那还是有优化的必要。具体而言,本文主要针对的Cpython,而且不涉及C扩展。  

  我们知道,Python使用引用技术和垃圾回收来管理内存,底层也有各种类型的内存池,那我们怎么得知一段代码使用的内存情况呢?工欲善其事必先利其器,直接看windows下的任务管理器或者linux下的top肯定是不准的。

  本文地址:http://www.cnblogs.com/xybaby/p/7488216.html

Pytracemalloc

  对于基本类型,可以通过sys.getsizeof()来查看对象占用的内存大小。以下是在64位Linux下的一些结果:

>>> import sys

>>> sys.getsizeof(1)
24
>>> sys.getsizeof([])
72
>>> sys.getsizeof(())
56
>>> sys.getsizeof({})
280
>>> sys.getsizeof(True)
24

  可以看到,即使是一个int类型(1)也需要占用24个字节,远远高于C语言中int的范围。因为Python中一切都是对象,int也不例外(事实上是PyIntObject),除了真正存储的数值,还需要保存引用计数信息、类型信息,更具体的可以参见《Python源码剖析》。

   而对于更复杂的组合类型,复杂的代码,使用getsizeof来查看就不准确了,因为在Python中变量仅仅指向一个对象,这个时候就需要更高级的工具,比如guppypysizerpytracemallocobjgraph。在这里重点介绍pytracemalloc。

  在Python3.4中,已经支持了pytracemalloc,如果使用python2.7版本,则需要对源码打补丁,然后重新编译。pytracemalloc在pep454中提出,主要有以下几个特点:

  • Traceback where an object was allocated
  • Statistics on allocated memory blocks per filename and per line number: total size, number and average size of allocated memory blocks
  • Compute the differences between two snapshots to detect memory leaks

  简单来说,pytracemalloc hook住了python申请和释放内存的接口,从而能够追踪对象的分配和回收情况。对内存分配的统计数据可以精确到每个文件、每一行代码,也可以按照调用栈做聚合分析。而且还支持快照(snapshot)功能,比较两个快照之间的差异可以发现潜在的内存泄露。

  下面通过一个例子来简单介绍pytracemalloc的用法和接口,关于更详细用法和API,可以参考这份详尽的文档或者pytracemalloc的作者在pycon上的演讲ppt

 import tracemalloc

 NUM_OF_ATTR =  10
NUM_OF_INSTANCE = 100 class Slots(object):
__slots__ = ['attr%s'%i for i in range(NUM_OF_ATTR)]
def __init__(self):
value_lst = (1.0, True, [], {}, ())
for i in range(NUM_OF_ATTR):
setattr(self, 'attr%s'%i, value_lst[i % len(value_lst)]) class NoSlots(object):
def __init__(self):
value_lst = (1.0, True, [], {}, ())
for i in range(NUM_OF_ATTR):
setattr(self, 'attr%s'%i, value_lst[i % len(value_lst)]) def generate_some_objs():
lst = []
for i in range(NUM_OF_INSTANCE):
o = Slots() if i % 2 else NoSlots()
lst.append(o)
return lst if __name__ == '__main__':
tracemalloc.start(3) t = generate_some_objs() snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno') # lineno filename traceback print(tracemalloc.get_traced_memory())
for stat in top_stats[:10]:
print(stat)

  在上面的代码中,用到了pytracemalloc几个核心的API:

  start(nframe: int=1)  

    pytracemalloc的一大好处就是可以随时启停,start函数即开始追踪内存分配,相应的stop会停止追踪。start函数有一个参数,nframes : 内存分配时记录的栈的深度,这个值越大,pytracemalloc本身消耗的内存越多,在计算cumulative数据的时候有用。

  get_traced_memory()

    返回值是拥有两个元素的tuple,第一个元素是当前分配的内存,第二个元素是自内存追踪启动以来的内存峰值。

  take_snapshot()    

    返回当前内存分配快照,返回值是Snapshot对象,该对象可以按照单个文件、单行、单个调用栈统计内存分配情况    

  运行环境:windows 64位python3.4

(62280, 62920)

test.py:10: size=16.8 KiB, count=144, average=120 B
test.py:17: size=16.7 KiB, count=142, average=120 B
test.py:19: size=9952 B, count=100, average=100 B
test.py:26: size=9792 B, count=102, average=96 B
test.py:27: size=848 B, count=1, average=848 B
test.py:34: size=456 B, count=1, average=456 B
test.py:36: size=448 B, count=1, average=448 B
D:\Python3.4\lib\tracemalloc.py:474: size=64 B, count=1, average=64 B

  如果将第36行的“lineno“改成“filename”,那么结果如下

(62136, 62764)

test.py:0: size=54.5 KiB, count=491, average=114 B
D:\Python3.4\lib\tracemalloc.py:0: size=64 B, count=1, average=64 B

  

  有了Profile结果之后,可以看出来在哪个文件中有大量的内存分配。与性能优化相同,造成瓶颈的有两种情况:单个对象占用了大量的内存;同时大量存在的小对象。对于前者,优化的手段并不多,惰性初始化属性可能有一些帮助;而对于后者,当同样类型的对象大量存在时,可以使用slots进行优化。

Slots

  默认情况下,自定义的对象都使用dict来存储属性(通过obj.__dict__查看),而python中的dict大小一般比实际存储的元素个数要大(以此降低hash冲突概率),因此会浪费一定的空间。在新式类中使用__slots__,就是告诉Python虚拟机,这种类型的对象只会用到这些属性,因此虚拟机预留足够的空间就行了,如果声明了__slots__,那么对象就不会再有__dict__属性。

  使用slots到底能带来多少内存优化呢,首先看看这篇文章,对于一个只有三个属性的Image类,使用__slots__之后内存从25.5G下降到16.2G,节省了9G的空间!

  

  到底能省多少,取决于类自身有多少属性、属性的类型,以及同时存在多少个类的实例。下面通过一段简单代码测试一下:

 # -*- coding: utf-8 -*-
import sys
import tracemalloc NUM_OF_ATTR = 3 #3 # 10 # 30 #90
NUM_OF_INSTANCE = 10 # 10 # 100 class Slots(object):
__slots__ = ['attr%s'%i for i in range(NUM_OF_ATTR)]
def __init__(self):
value_lst = (1.0, True, [], {}, ())
for i in range(NUM_OF_ATTR):
setattr(self, 'attr%s'%i, value_lst[i % len(value_lst)]) class NoSlots(object):
def __init__(self):
value_lst = (1.0, True, [], {}, ())
for i in range(NUM_OF_ATTR):
setattr(self, 'attr%s'%i, value_lst[i % len(value_lst)]) if __name__ == '__main__':
clz = Slots if len(sys.argv) > 1 else NoSlots
tracemalloc.start()
objs = [clz() for i in range(NUM_OF_INSTANCE)]
print(tracemalloc.get_traced_memory()[0])

  

  上面的代码,主要是在每个实例的属性数目、并发存在的实例数目两个维度进行测试,并没有测试不同的属性类型。结果如下表:

  

  百分比为内存优化百分比,计算公式为(b - a) / b, 其中b为没有使用__slots__时分配的内存, a为使用了__slots__时分配的内存。

  

注意事项

  关于__slots__,Python文档有非常详尽的介绍,这里只强调几点注意事项

  第一:基类和子类都必须__slots__,即使基类或者子类没有属性

>>> class Base(object):

...     pass
...
>>> class Derived(Base):
...     __slots__ = ('a', )
...
>>> d.__slots__
('a',)
>>> getattr(d, '__dict__', 'No Dict')
{}

  从上面的示例可以看到,子类的对象还是有__dict__属性,原因就在于基类没有声明__slots__。因此,可以通过看子类的实例有没有__dict__属性来判断slots的使用是否正确

  第二:子类会继承基类的__slots__

  更准确的说,如果访问属性的时候没有在子类的__slots__找到,会继续在基类的__slots__查找,因为Python使用descriptor在类这个层级实现__slots__的,具体可以参见《python属性查找 深入理解》一文

>>> class Base(object):

...     __slots__ = ('a',)
...
>>> class Derived(Base):
...     __slots__ = ('b', )
...
>>> d = Derived()
>>> d.__slots__
('b',)
>>> getattr(d, '__dict__', 'No Dict')
'No Dict'
>>> d.a = 1
>>> d.c = 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Derived' object has no attribute 'c'

objgraph

  在大型工程中,怎么排查有哪些大量存在的对象呢,毕竟同一个类型存在的对象越多,优化越有效果。除了直接看代码,最好使的就是使用objgraph.py的show_most_common_types(N)函数,该函数返回Python gc管理的所有对象中,数目前N多的对象,在排除掉python builtin对象之后,剩下的就是可优化的对象。比如在最上面的代码中:在最后加上这么两句:

import objgraph

objgraph.show_most_common_types(25)

  输出如下:

  

再论Python dict

  前面介绍slots的时候,就提到Python自定义的对象中通过dict来管理属性。这种机制极大的提高了Python的灵活性 -- 可以随时给对象增加属性,但是其实现机制也带来了内存上的浪费。不管是python源码,还是Python程序,都大量使用了dict,因此这部分内存浪费不容小视。

  python中的dict使用的是散列表(类似C++中的std::unordered_map),当计算出的hash值冲突的时候,采用开放地址法解决冲突(另一种常见的冲突解决算法是链表法)。为了降低冲突概率,当装填因子(实际存储的元素与散列表长度的比值)超过2/3的时候就会对散列表进行扩容,因此散列表中一定会存在一些未使用的槽。

  下面简单看看PyDictObject的数据结构(python2.7.3 dictobject.h)

 #define PyDict_MINSIZE 8

 typedef struct {
/* Cached hash code of me_key. Note that hash codes are C longs.
* We have to use Py_ssize_t instead because dict_popitem() abuses
* me_hash to hold a search finger.
*/
Py_ssize_t me_hash;
PyObject *me_key;
PyObject *me_value;
} PyDictEntry; typedef struct _dictobject PyDictObject;
struct _dictobject {
PyObject_HEAD
Py_ssize_t ma_fill; /* # Active + # Dummy */
Py_ssize_t ma_used; /* # Active */ /* The table contains ma_mask + 1 slots, and that's a power of 2.
* We store the mask instead of the size because the mask is more
* frequently needed.
*/
Py_ssize_t ma_mask; /* ma_table points to ma_smalltable for small tables, else to
* additional malloc'ed memory. ma_table is never NULL! This rule
* saves repeated runtime null-tests in the workhorse getitem and
* setitem calls.
*/
PyDictEntry *ma_table;
PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
PyDictEntry ma_smalltable[PyDict_MINSIZE];
};

  从定义可以看出,除了固定的部分(几个Py_ssize_t),PyDictObject中主要是PyDictEntry对象,PyDictEntrty包含一个Py_ssize_t(int)和两个指针。上面源码中的注释(第26行)指出,当dict的元素比较少时,ma_table指向ma_smalltable,当元素增多时,ma_table会指向新申请的空间。ma_smalltable的作用在于Python(不管是源码还是代码)都大量使用dict,一般来说,存储的元素也不会太多,因此Python就先开辟好PyDict_MINSIZE(默认为8)个空间。

  为什么说PyDictObject存在浪费呢,PyDictEntry在32位下也有12个字节,那么即使在ma_smalltable(ma_table)中大量的位置没有被使用时,也要占用这么多字节。用这篇文章中的例子:

  假设有这么一个dict:   d = {'timmy': 'red', 'barry': 'green', 'guido': 'blue'}

  在Python源码中的视图就是这样的:

 #  下面的entries就是ma_smalltable

entries = [['--', '--', '--'],
[-8522787127447073495, 'barry', 'green'],
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
[-9092791511155847987, 'timmy', 'red'],
['--', '--', '--'],
[-6480567542315338377, 'guido', 'blue']]

  然而,完全可以这么存储:

 indices = [None, 1, None, None, None, 0, None, 2]

entries =  [[-9092791511155847987, 'timmy', 'red'],
[-8522787127447073495, 'barry', 'green'],
[-6480567542315338377, 'guido', 'blue']]

  indices的作用类似ma_smalltable,但只存储一个数组的索引值,数组只存储实际存在的元素(PyDictEntry),当dict中的元素越稀疏,相比上一种存储方式使用的内存越少。而且,这种实现, dict就是有序的(按插入时间排序)

  这就是python3.6中新的dict实现,Compact dict! Stackoverflow上也有相关讨论

总结

  本文中介绍了Python内存优化的Profile工具,最有效的优化方法:使用slots,也介绍了在python3.6中新的dict实现。

  当然,还有一些良好的编码习惯。比如尽量使用immutable而不是mutable对象:使用tuple而不是list,使用frozenset而不是set;另外,就是尽量使用迭代器,比如python2.7中,使用xrange而不是range,dict的iterxx版本。

references

pytracemalloc

pep454: Add a new tracemalloc module to trace Python memory allocations  

save-ram-with-python-slots/

Python源码分析-PyDictObject

More compact dictionaries with faster iteration

Python内存优化:Profile,slots,compact dict的更多相关文章

  1. Python内存优化

    实际项目中,pythoner更加关注的是Python的性能问题,之前也写过一篇文章<Python性能优化>介绍Python性能优化的一些方法.而本文,关注的是Python的内存优化,一般说 ...

  2. [转] 使用gc && objgraph 优化python内存

    转自https://www.cnblogs.com/xybaby/p/7491656.html 使用gc.objgraph干掉python内存泄露与循环引用!   目录 一分钟版本 python内存管 ...

  3. python内存泄露memory leak排查记录

    问题描述 A服务,是一个检测MGR集群主节点是否发生变化的服务,使用python语言实现的. 针对每个集群,主线程会创建一个子线程,并由子线程去检测.子线程会频繁的创建和销毁. 上线以后,由于经常会有 ...

  4. python性能优化

      注意:本文除非特殊指明,”python“都是代表CPython,即C语言实现的标准python,且本文所讨论的是版本为2.7的CPython. python为什么性能差: 当我们提到一门编程语言的 ...

  5. 使用gc、objgraph干掉python内存泄露与循环引用!

    Python使用引用计数和垃圾回收来做内存管理,前面也写过一遍文章<Python内存优化>,介绍了在python中,如何profile内存使用情况,并做出相应的优化.本文介绍两个更致命的问 ...

  6. python内存管理及垃圾回收

    一.python的内存管理 python内部将所有类型分成两种,一种由单个元素组成,一种由多个元素组成.利用不同结构体进行区分 /* Nothing is actually declared to b ...

  7. Python3中对Dict的内存优化

    众所周知,python3.6这个版本对dict的实现是做了较大优化的,特别是在内存使用率方面,因此我觉得有必要研究一下最新的dict的源码实现. 前后断断续续看了大概一周多一点,主要在研究dict和创 ...

  8. Python内存管理机制及优化简析(转载)

    from:http://kkpattern.github.io/2015/06/20/python-memory-optimization-zh.html 准备工作 为了方便解释Python的内存管理 ...

  9. python性能优化、内存优化、内存泄露;与其他语音比较效率如何?

    1.内存泄露:http://www.cnblogs.com/xybaby/p/7491656.html 2.内存优化:http://www.cnblogs.com/xybaby/p/7488216.h ...

随机推荐

  1. MySql Host is blocked because of many connection errors; unblock with 'mysqladmi

    原因: 同一个ip在短时间内产生太多(超过mysql数据库max_connection_errors的最大值)中断的数据库连接而导致的阻塞: 解决方法: 1.提高允许的max_connection_e ...

  2. HTML常用标签及属性

    标签格式 格式: 双边:<标签名 属性1="值1" 属性2='值2' 属性3=值3>内容</标签名> 单边:<标签名 属性1="值1&quo ...

  3. 【代码笔记】Web-HTML-CSS

    一,效果图. 二,代码. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> ...

  4. CloudSim源代码学习——服务代理商(DatacenterBroker)

    DatacenterBroker.java文件如下: (其中,相关语句已经做好标注) /* * Title: CloudSim Toolkit * Description: CloudSim (Clo ...

  5. HBuilder离线打包启用Chrome Inspect调试

    解决方法: 修改这个文件 assets/data/dcloud_control.xml <msc version="1.9.9.39354" debug="true ...

  6. 微信小程序-01-项目组成文件介绍(入门篇)

    自古开篇先说两句,写这些笔记不是学习用的,主要是后续分享一些遇到的坑,碰到过什么样的问题,怎么去解决,如果你不是一个很耐心无看文章的人,建议去 网易云课堂找一些课程,跟着别人的脚步或许会更有动力,我的 ...

  7. idea 自动换行

    如下:

  8. SolrCloud集群搭建(基于zookeeper)

    1. 环境准备 1.1 三台Linux机器,x64系统 1.2 jdk1.8 1.3 Solr5.5 2. 安装zookeeper集群 2.1 分别在三台机器上创建目录 mkdir /usr/hdp/ ...

  9. 10-openldap同步原理

    openldap同步原理 阅读视图 openldap同步原理 syncrepl.slurpd同步机制优缺点 OpenLDAP同步条件 OpenLDAP同步参数 1. openldap同步原理 Open ...

  10. 【第八篇】SAP ABAP7.5x新语法之F4增强【续】

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:SAP ABAP7.5x系列之F4增强[续]   ...