在替换大型字符串列表中的元素时,避免 Python 中的大量内存使用
Avoid huge memory usage in Python when substituting elements in large list of strings
所以我正在尝试将大量具有逗号分隔值的字符串列表解析为(列表)列表。我正在尝试“就地”执行此操作,例如不复制内存中已经很大的对象。
现在,理想情况下,在解析期间和之后,唯一需要的额外内存是将原始字符串表示为字符串列表的开销。但实际发生的事情要糟糕得多。
例如此字符串列表占用约 1.36 GB 的内存:
import psutil
l = [f"[23873498uh3149ubn34, 59ubn23459un3459, un3459-un345, 9u3n45iu9n345, {i}]" for i in range(10_000_000)]
psutil.Process().memory_info().rss / 1024**3
>> 1.3626747131347656
所需的解析最终结果会占用更多空间 (~1.8 GB):
import psutil
l = [["23873498uh3149ubn34", "59ubn23459un3459", "un3459-un345", "9u3n45iu9n345", str(i)] for i in range(10_000_000)]
psutil.Process().memory_info().rss / 1024**3
1.7964096069335938
然而,实际解析原始字符串需要高达 5 GB 的内存,即比初始字符串加上最终列表的总和还要多:
import psutil
l = [f"[23873498uh3149ubn34, 59ubn23459un3459, un3459-un345, 9u3n45iu9n345, {i}]" for i in range(10_000_000)]
for i, val in enumerate(l):
l[i] = val.split(", ")
psutil.Process().memory_info().rss / 1024**3
4.988628387451172
现在,我知道纯 Python 字符串和列表本身并不是非常有效(内存方面),但我不明白最终结果所需的内存之间的巨大差距(1.8 GB),以及在到达那里的过程中使用了什么 (5GB)。
谁能解释到底发生了什么,是否可以“就地”实际修改列表(同时实际释放替换值的内存),以及是否有更好的内存方式来做到这一点?
如果您不是一次使用整个列表,您可以使用生成器表达式
import psutil
data = (f"[23873498uh3149ubn34, 59ubn23459un3459, un3459-un345, 9u3n45iu9n345, {i}]" for i in range(10_000_000))
res = (x.split(', ') for x in data)
print(psutil.Process().memory_info().rss / 1024**3) # 0.08874893188476562
重新格式化以使其可读,您对它“应该占用”的内存的估计非常乐观:
l = [["23873498uh3149ubn34",
"59ubn23459un3459",
"un3459-un345",
"9u3n45iu9n345",
str(i)] for i in range(10_000_000)]
这太离谱了。例如,为 "9u3n45iu9n345"
创建的字符串对象只有一个,它在 1000 万个列表中 共享 。
>>> print(l[10][2] is l[56000][2]) # first indices are arbitrary
True
您的实际代码在每个索引位置创建了 1000 万个 distinct 个字符串对象。
尝试运行
sys._debugmallocstats()
不时获取有关内存用途的更多线索。
在 3.10.1 下,在 64 位 Windows 上,最后的一些摘要输出显示分配的 RAM 被非常有效地使用(几乎所有分配的块都在使用中):
# arenas allocated total = 6,601
# arenas reclaimed = 1,688
# arenas highwater mark = 4,913
# arenas allocated current = 4,913
4913 arenas * 1048576 bytes/arena = 5,151,653,888
# bytes in allocated blocks = 5,129,947,088
# bytes in available blocks = 701,584
53 unused pools * 16384 bytes = 868,352
# bytes lost to pool headers = 15,090,192
# bytes lost to quantization = 5,046,672
# bytes lost to arena alignment = 0
Total = 5,151,653,888
arena map counts
# arena map mid nodes = 1
# arena map bot nodes = 20
# bytes lost to arena map root = 8,192
# bytes lost to arena map mid = 8,192
# bytes lost to arena map bot = 40,960
Total = 57,344
注:节省约8亿字节的简单方法:
l[i] = tuple(val.split(", ")) # use this
# l[i] = val.split(", ") # instead of this
列出“过度分配”,以便一系列追加在分摊的 O(1)
时间内运行。元组不会 - 它们只分配保留初始内容所需的数量(这也是它们的最终内容,因为元组是不可变的)。
测量 RSS 是错误的。 RSS 是当前驻留在物理 RAM 中的虚拟内存的一部分。例如,如果我只有 4 GB 的物理 RAM,我永远不会测量超过 4 GB。所以:如果您想保持较小的数量,请从您的计算机中移除 RAM 模块。
字符串中的数据约为 650 MB。
一个列表需要 56 个字节的内存。您正在创建其中的 10.000.000 个,因此这些列表占用 560 MB 的额外内存。
一个字符串在内存中占用 49 个字节,加上字符本身。将长字符串拆分后,您有 50.000.000 个字符串,因此它们又占 2450 MB。
另一个问题是垃圾回收。使用 PSUtils,您是在询问操作系统它为 Python 分配了多少内存。但是,Python 可能只是请求了它,因为它认识到您正在处理大量数据。但是,对于 Python.
,其中一些仍然可以被认为是免费的 内部
除此之外,我认为您应该进行真实世界的测试。一个好的 CSV 解析器不会将整个文件加载到内存中然后拆分字符串。相反,它将有一个解析器,可以流式传输数据并在找到数据时以逗号分隔。
为了获得 类 的开销,我使用了
import sys
print(sys.getsizeof([]))
print(sys.getsizeof(""))
所以我正在尝试将大量具有逗号分隔值的字符串列表解析为(列表)列表。我正在尝试“就地”执行此操作,例如不复制内存中已经很大的对象。
现在,理想情况下,在解析期间和之后,唯一需要的额外内存是将原始字符串表示为字符串列表的开销。但实际发生的事情要糟糕得多。
例如此字符串列表占用约 1.36 GB 的内存:
import psutil
l = [f"[23873498uh3149ubn34, 59ubn23459un3459, un3459-un345, 9u3n45iu9n345, {i}]" for i in range(10_000_000)]
psutil.Process().memory_info().rss / 1024**3
>> 1.3626747131347656
所需的解析最终结果会占用更多空间 (~1.8 GB):
import psutil
l = [["23873498uh3149ubn34", "59ubn23459un3459", "un3459-un345", "9u3n45iu9n345", str(i)] for i in range(10_000_000)]
psutil.Process().memory_info().rss / 1024**3
1.7964096069335938
然而,实际解析原始字符串需要高达 5 GB 的内存,即比初始字符串加上最终列表的总和还要多:
import psutil
l = [f"[23873498uh3149ubn34, 59ubn23459un3459, un3459-un345, 9u3n45iu9n345, {i}]" for i in range(10_000_000)]
for i, val in enumerate(l):
l[i] = val.split(", ")
psutil.Process().memory_info().rss / 1024**3
4.988628387451172
现在,我知道纯 Python 字符串和列表本身并不是非常有效(内存方面),但我不明白最终结果所需的内存之间的巨大差距(1.8 GB),以及在到达那里的过程中使用了什么 (5GB)。
谁能解释到底发生了什么,是否可以“就地”实际修改列表(同时实际释放替换值的内存),以及是否有更好的内存方式来做到这一点?
如果您不是一次使用整个列表,您可以使用生成器表达式
import psutil
data = (f"[23873498uh3149ubn34, 59ubn23459un3459, un3459-un345, 9u3n45iu9n345, {i}]" for i in range(10_000_000))
res = (x.split(', ') for x in data)
print(psutil.Process().memory_info().rss / 1024**3) # 0.08874893188476562
重新格式化以使其可读,您对它“应该占用”的内存的估计非常乐观:
l = [["23873498uh3149ubn34",
"59ubn23459un3459",
"un3459-un345",
"9u3n45iu9n345",
str(i)] for i in range(10_000_000)]
这太离谱了。例如,为 "9u3n45iu9n345"
创建的字符串对象只有一个,它在 1000 万个列表中 共享 。
>>> print(l[10][2] is l[56000][2]) # first indices are arbitrary
True
您的实际代码在每个索引位置创建了 1000 万个 distinct 个字符串对象。
尝试运行
sys._debugmallocstats()
不时获取有关内存用途的更多线索。
在 3.10.1 下,在 64 位 Windows 上,最后的一些摘要输出显示分配的 RAM 被非常有效地使用(几乎所有分配的块都在使用中):
# arenas allocated total = 6,601
# arenas reclaimed = 1,688
# arenas highwater mark = 4,913
# arenas allocated current = 4,913
4913 arenas * 1048576 bytes/arena = 5,151,653,888
# bytes in allocated blocks = 5,129,947,088
# bytes in available blocks = 701,584
53 unused pools * 16384 bytes = 868,352
# bytes lost to pool headers = 15,090,192
# bytes lost to quantization = 5,046,672
# bytes lost to arena alignment = 0
Total = 5,151,653,888
arena map counts
# arena map mid nodes = 1
# arena map bot nodes = 20
# bytes lost to arena map root = 8,192
# bytes lost to arena map mid = 8,192
# bytes lost to arena map bot = 40,960
Total = 57,344
注:节省约8亿字节的简单方法:
l[i] = tuple(val.split(", ")) # use this
# l[i] = val.split(", ") # instead of this
列出“过度分配”,以便一系列追加在分摊的 O(1)
时间内运行。元组不会 - 它们只分配保留初始内容所需的数量(这也是它们的最终内容,因为元组是不可变的)。
测量 RSS 是错误的。 RSS 是当前驻留在物理 RAM 中的虚拟内存的一部分。例如,如果我只有 4 GB 的物理 RAM,我永远不会测量超过 4 GB。所以:如果您想保持较小的数量,请从您的计算机中移除 RAM 模块。
字符串中的数据约为 650 MB。
一个列表需要 56 个字节的内存。您正在创建其中的 10.000.000 个,因此这些列表占用 560 MB 的额外内存。
一个字符串在内存中占用 49 个字节,加上字符本身。将长字符串拆分后,您有 50.000.000 个字符串,因此它们又占 2450 MB。
另一个问题是垃圾回收。使用 PSUtils,您是在询问操作系统它为 Python 分配了多少内存。但是,Python 可能只是请求了它,因为它认识到您正在处理大量数据。但是,对于 Python.
,其中一些仍然可以被认为是免费的 内部除此之外,我认为您应该进行真实世界的测试。一个好的 CSV 解析器不会将整个文件加载到内存中然后拆分字符串。相反,它将有一个解析器,可以流式传输数据并在找到数据时以逗号分隔。
为了获得 类 的开销,我使用了
import sys
print(sys.getsizeof([]))
print(sys.getsizeof(""))