python内存使用:txt文件比包含文件文本的python列表小得多

python内存使用:txt文件比包含文件文本的python列表小得多,python,python-2.7,memory,utf-8,nlp,Python,Python 2.7,Memory,Utf 8,Nlp,我有一个543 MB txt文件,其中包含一行空格分隔的utf-8令牌: aaa algeria americansamoa appliedethics accessiblecomputing ada anarchism ... 但是,当我将这些文本数据加载到python列表中时,它使用了约8GB的内存(列表约900MB,令牌约8GB): 我预计内存使用量大约为文件大小+列表开销=1.5GB。为什么令牌在加载到列表中时会消耗更多内存?两个原因: CPython中的每个字符串在其C对象头中都有相

我有一个543 MB txt文件,其中包含一行空格分隔的utf-8令牌:

aaa algeria americansamoa appliedethics accessiblecomputing ada anarchism ...
但是,当我将这些文本数据加载到python列表中时,它使用了约8GB的内存(列表约900MB,令牌约8GB):

我预计内存使用量大约为文件大小+列表开销=1.5GB。为什么令牌在加载到列表中时会消耗更多内存?

两个原因:

  • CPython中的每个字符串在其C对象头中都有相当多的样板文件;在Python2 64位系统上,空的
    unicode
    对象使用52个字节,这是每个
    unicode
    对象的固定开销,甚至在您计算它包含的数据之前。如果您有1.14M
    unicode
    对象(不是像
    u'
    那样的单例对象),那么您仅在每个对象开销上就使用了近6GB

  • 您使用的是Python2,从
    str
    解码到
    unicode
    ,这取决于Python2的构建配置,每个字符使用固定的2或4个字节,即使对于纯ASCII字符串也是如此;根据您的数字,您使用的是4字节/字符系统。因此,它不需要超出对象头开销543MB的数据,而需要超过2GB的标题

  • 标头问题在很大程度上是无法克服的(Python对象在标头上总是会浪费几十个字节);每个Python对象都有很高的固定开销(如前所述,
    sys.getsizeof(u')
    在我的x64系统上是52,尽管只存储了8个字节的“真实”数据,
    str
    的长度)

    但是,由于您的输入主要是ASCII码,您可以通过使用Python 3来减少内存使用;在现代Py3(3.3+IIRC)中,它们使用动态大小的存储来存储
    str
    ;仅使用ASCII/latin-1字符的
    str
    将使用每个字符一个字节(latin-1使固定开销略高于ASCII,但每个字符的成本仍然为1),而不是两个或四个(基本多语言平面中的任何内容都将使用每个字符两个字节,而不是四个;只有非BMP字符串需要每个字符四个字节).
    str
    的头也稍微小一些(
    sys.getsizeof(“”)=49
    ,而不是52),因此您希望头的内存消耗减少约350 MB,而更紧凑的数据存储的内存消耗减少1.5 GB(因为它主要是ASCII)

    只需使用Py 3并将代码更改为:

    with open('tokens.txt', 'r', encoding='utf-8') as f:
        tokens = f.read().split()
    
    import sys
    
    print(sys.getsizeof(tokens))
    print(sum(sys.getsizeof(t) for t in tokens))
    
    您应该看到字符串的内存使用量大大减少,对于较长的字符串(例如,在我的Linux x64安装中,
    u'examplestring'
    在Py2上是104字节,使用4字节/char
    unicode
    ,在Py3上只有62字节)

    或者,作为一个廉价的黑客,当您知道Py2是纯ASCII时,您可以尝试将Py2上的
    unicode
    转换回
    str
    ;在Py2上,这两种类型在很大程度上是可互操作的,
    str
    的每个对象开销较小(37字节比52字节),并且只使用一个字节/字符。手动从
    unicode
    转换回ASCII是可行的,尽管这会降低速度。为此,请将代码更改为:

    # Open in binary mode
    with open('tokens.txt', 'rb') as f:
        # Defer decode and only do it for str with non-ASCII bytes
        # producing list of mostly ASCII str with a few unicode objects
        # when non-ASCII appears
        tokens = [w.decode('utf-8') if max(w) > '\x7f' else w
                  for w in f.read().split()]
    
    import sys
    
    print sys.getsizeof(tokens)
    print sum(sys.getsizeof(t) for t in tokens)
    

    这将为每个对象头节省约1.7 GB的空间,在数据存储上节省约1.5 GB的空间,作为交换,您可能会遇到Py2所具有的
    str
    /
    unicode
    互操作性怪癖(这也是Py3中分隔
    字节和
    str
    的主要动机)。

    如果您好奇的话,x64 Py2
    unicode
    使用的52个字节是:16个字节用于通用非GC-ed对象头(由一个8字节的refcnt和一个指向类对象的8字节指针组成),8个字节用于长度,8个字节用于指向数据的指针,8个字节用于缓存哈希存储,8个字节用于指向编码(
    str
    )的(最初为空)指针用于缓冲区协议黑客Py2允许的数据版本。添加字符数组的四个字节(对于空的
    unicode
    ,它只是NUL终止符),在存储任何实际数据之前,这是52个字节。
    # Open in binary mode
    with open('tokens.txt', 'rb') as f:
        # Defer decode and only do it for str with non-ASCII bytes
        # producing list of mostly ASCII str with a few unicode objects
        # when non-ASCII appears
        tokens = [w.decode('utf-8') if max(w) > '\x7f' else w
                  for w in f.read().split()]
    
    import sys
    
    print sys.getsizeof(tokens)
    print sum(sys.getsizeof(t) for t in tokens)