绝对的说法都是错误的。

贪食蟒

多说也挺惨的,不能平静的死去,不知道哪里冒出一堆垃圾用户 ..


Python 慢是因为这货吃 CPU,而且多线程还不能有效利用多核,然而这货不仅吃 CPU 还吃内存,非常贪婪。

我的前一篇文章里面提到过一个项目,一直都没留心,这货大概加载了 70M 的磁盘文件到内存中以 dict 的形式保存,内存使用立马就飙升了大几百兆 .. (⊙v⊙)

看看 Python 的对象到底有多吃内存(下面的代码都是基于 Python3.6 的,Python2.x 只会多不会少):

>>> import sys
>>> sys.getsizeof(1)
 28  # bytes
>>> sys.getsizeof(1<<64)  # long in Py2
 36
>>> sys.getsizeof(1.1)
 24
>>> sys.getsizeof('s')
 50
>>> sys.getsizeof('ss')
 51
>>> sys.getsizeof(b'b')
 34
>>> sys.getsizeof(b'bb')
 35
>>> from decimal import Decimal
>>> sys.getsizeof(Decimal(3.4))
 104

对于容器类型的对象,我们得使用这段 代码 递归的计算内存大小,这里就直接复制过来了:

from __future__ import print_function
from sys import getsizeof, stderr
from itertools import chain
from collections import deque
try:
    from reprlib import repr
except ImportError:
    pass

def total_size(o, handlers={}, verbose=False):
    """ Returns the approximate memory footprint an object and all of its contents.

    Automatically finds the contents of the following builtin containers and
    their subclasses:  tuple, list, deque, dict, set and frozenset.
    To search other containers, add handlers to iterate over their contents:

        handlers = {SomeContainerClass: iter,
                    OtherContainerClass: OtherContainerClass.get_elements}
    """
    dict_handler = lambda d: chain.from_iterable(d.items())
    all_handlers = {
        tuple: iter,
        list: iter,
        deque: iter,
        dict: dict_handler,
        set: iter,
        frozenset: iter,
    }
    all_handlers.update(handlers)     # user handlers take precedence
    seen = set()                      # track which object id's have already been seen
    default_size = getsizeof(0)       # estimate sizeof object without __sizeof__

    def sizeof(o):
        if id(o) in seen:       # do not double count the same object
            return 0
        seen.add(id(o))
        s = getsizeof(o, default_size)

        if verbose:
            print(s, type(o), repr(o), file=stderr)

        for typ, handler in all_handlers.items():
            if isinstance(o, typ):
                s += sum(map(sizeof, handler(o)))
                break
        return s

    return sizeof(o)


##### Example call #####
if __name__ == '__main__':
    d = dict(
        a=1, b=2.5, c=1<<64,
        d=(1, 2, 3), e=[4, 5, 6], f={7, 8, 9},
        g=b'bytes', h='unicode'
    )
    print(total_size(d, verbose=True))

输出是这样的:

368 <class 'dict'> {'a': 1, 'b': 2.5, 'c': 18446744073709551616, 'd': (1, 2, 3), ...}
50 <class 'str'> 'a'
28 <class 'int'> 1
50 <class 'str'> 'b'
24 <class 'float'> 2.5
50 <class 'str'> 'c'
36 <class 'int'> 18446744073709551616
50 <class 'str'> 'd'
72 <class 'tuple'> (1, 2, 3)
28 <class 'int'> 2
28 <class 'int'> 3
50 <class 'str'> 'e'
88 <class 'list'> [4, 5, 6]
28 <class 'int'> 4
28 <class 'int'> 5
28 <class 'int'> 6
50 <class 'str'> 'f'
224 <class 'set'> {7, 8, 9}
28 <class 'int'> 8
28 <class 'int'> 9
28 <class 'int'> 7
50 <class 'str'> 'g'
38 <class 'bytes'> b'bytes'
50 <class 'str'> 'h'
56 <class 'str'> 'unicode'
1558

没看错,这个小小的 dict 就干掉了近 1.5K 的内存… 可以的,实力在这里大家都看得到。

当然 Python 对于小型对象会使用对象池的方式来优化内存的使用率,但是这只能应用于大量相同的小对象,而且这篇文章里面提到了:

CPython manages small objects (less than 256 bytes) in special pools on 8-byte boundaries. There are pools for 1-8 bytes, 9-16 bytes, and all the way to 249-256 bytes. When an object of size 10 is allocated, it is allocated from the 16-byte pool for objects 9-16 bytes in size. So, even though it contains only 10 bytes of data, it will cost 16 bytes of memory. If you allocate 1,000,000 objects of size 10, you actually use 16,000,000 bytes and not 10,000,000 bytes as you may assume. This 60% overhead is obviously not trivial.

所以,谨慎使用 Python 对象缓存过大的数据集,万能的 Google 告诉我们可以使用标准库 shelve / sqlite3.connect(':memory:') ,第三方工具 numpy / redis 以及优化过的数据结构 trie 等等来代替 dict-like 的数据集。

2017/4/14: 今天有时间尝试着优化了下文章开头提到的那个项目,借助 guppy 这个工具追踪了对象的内存使用率,确实是 dict 占用了较大的内存,由于 SDK 的属性,为了适配 Java 那边的文件存储格式并且使用尽量少的依赖和避免不必要的程序复杂性,替换掉 dict 也显得不那么必要,但是仔细看发现 unicode 占用较大的内存,实际上就是 json.load 加载的解析后的字符串都是 unicode 的,所以就直接把 unicode 都转成的了 bytes,想要吐糟的是 Pythonjson 库对自定义 decode 的支持弱爆了,需要用时间复杂度来换灵活性,尝试 hack 内置的Decoder,看了几个版本的 Python 源码太不划算就放弃了,加载的时候慢点但是减少了一百多兆的内存也值了,其实这些字符串大部分包含的是数字,转成数字会减少更多的内存,也有小几十兆,但是想想写文件的时候又得一次转型增加程序的复杂性也挺不划算的,暂时能优化的就这么多,毕竟没有其它对象占太多内存,毕竟线上机器性能也强劲 ..

BTW

这篇文章提供了一个案例教我们怎样去优化内存的使用率,主要使用了 Heapy 这个工具来定位吃内存较多的对象,通过干掉临时对象(del large_data),使用__slots__ 魔法,干掉 tuple,使用 Cython,将对象的方法变成函数等方式来优化内存使用率。

另外,objgraph 可以用来追查内存泄露相关的问题。