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

jka*_*imi 4 python memory nlp utf-8 python-2.7

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

aaa algeria americansamoa appliedethics accessiblecomputing ada anarchism ...
Run Code Online (Sandbox Code Playgroud)

但是,当我将此文本数据加载到 python 列表中时,它使用了 ~8 GB 的内存(~900 MB 用于列表,~8 GB 用于令牌):

with open('tokens.txt', 'r') as f:
    tokens = f.read().decode('utf-8').split()

import sys

print sys.getsizeof(tokens)
# 917450944 bytes for the list
print sum(sys.getsizeof(t) for t in tokens)
# 7067732908 bytes for the actual tokens
Run Code Online (Sandbox Code Playgroud)

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

Sha*_*ger 5

两个原因:

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

  2. 您使用的是 Python 2 和decodeing from strto unicode,这取决于您对 Python 2 的构建配置,每个字符使用固定的 2 或 4 个字节,即使对于纯 ASCII 字符串;根据您的数字,您使用的是 4 个字节/字符的系统。因此,数据不会超过对象标头开销 543 MB,而是需要超过 2 GB。

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

但是由于您的输入主要是 ASCII,您可以通过迁移到 Python 3 来减少内存使用量;在现代 Py3(3.3+ IIRC)中,它们使用动态大小的存储str;一个str只使用ASCII / Latin-1字符将每个字符使用一个字节(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))
Run Code Online (Sandbox Code Playgroud)

并且您应该看到字符串的内存使用量减少了,在字符串较长的情况下会显着减少(例如,在我的 Linux x64 安装中,u'examplestring'在 Py2 上使用 4 个字节/字符编译为 104 个字节unicode,而 Py3 上仅为 62 字节)。

另外,作为一种廉价的黑客,你可以尝试从转换回unicodestr上的Py2当你知道它是纯ASCII; 在 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)
Run Code Online (Sandbox Code Playgroud)

这应该为每个对象的标头节省约 1.7 GB,并在数据存储上节省约 1.5 GB,以换取可能让您了解 Py2 具有的str/unicode互操作性怪癖(并且是分离bytes和导入的很大一部分动机str) py 3)。