Python内存优化

作者: 南城城南 | 来源:发表于2017-12-02 22:50 被阅读0次

    姓名:米芃

    学号:16040520018

    [嵌牛导读]Python内存优化的Profile工具,最有效的优化方法:使用slots,在python3.6中新的dict实现。

    [嵌牛鼻子]python Linux 内存分配

    [嵌牛提问]Python不规范代码,质量较差的代码对内存的影响是?

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

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

    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中变量仅仅指向一个对象,这个时候就需要更高级的工具,比如guppy,pysizer,pytracemalloc,objgraph。在这里重点介绍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(https://github.com/haypo/conf/blob/master/2014-Pycon-Montreal/tracemalloc.pdf)。

    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_pytracemalloc_use_py3.4.py:10: size=16.8 KiB, count=144, average=120 B

    test_pytracemalloc_use_py3.4.py:17: size=16.7 KiB, count=142, average=120 B

    test_pytracemalloc_use_py3.4.py:19: size=9952 B, count=100, average=100 B

    test_pytracemalloc_use_py3.4.py:26: size=9792 B, count=102, average=96 B

    test_pytracemalloc_use_py3.4.py:27: size=848 B, count=1, average=848 B

    test_pytracemalloc_use_py3.4.py:34: size=456 B, count=1, average=456 B

    test_pytracemalloc_use_py3.4.py:36: size=448 B, count=1, average=448 B

    D:Python3.4libtracemalloc.py:474: size=64 B, count=1, average=64 B

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

    (62136, 62764)

    test_pytracemalloc_use_py3.4.py:0: size=54.5 KiB, count=491, average=114 B

    D:Python3.4libtracemalloc.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__时分配的内存。

    摘自微信公众号“程序员大咖”,有删改。

    相关文章

      网友评论

        本文标题:Python内存优化

        本文链接:https://www.haomeiwen.com/subject/xipobxtx.html