我花了一天时间尝试调试 Python 脚本中的内存问题。我使用 SQL Alchemy 作为我的 ORM。这里有几个令人困惑的问题,我希望如果我把它们都列出来,有人能指出我正确的方向。
为了达到我想要的性能,我读入了一个表中的所有记录(~400k),然后遍历电子表格,匹配我之前读入的记录,然后创建新记录(~800k)到另一张桌子。代码大致如下:
dimensionMap = {}
for d in connection.session.query(Dimension):
dimensionMap[d.businessKey] = d.primarySyntheticKey
# len(dimensionMap) == ~400k, sys.getsizeof(dimensionMap) == ~4MB
allfacts = []
sheet = open_spreadsheet(path)
for row in sheet.allrows():
dimensionId = dimensionMap[row[0]]
metric = row[1]
fact = Fact(dimensionId, metric)
connection.session.add(fact)
allfacts.append(fact)
if row.number % 20000 == 0:
connection.session.flush()
# len(allfacts) == ~800k, sys.getsizeof(allfacts) == ~50MB
connection.session.commit()
sys.stdout.write('All Done')
400k 和 800k 对我来说似乎不是特别大的数字,但我仍然遇到了内存问题,一台 4GB 内存的机器。这对我来说真的很奇怪,因为我在我的两个最大的集合上运行了 sys.getsizeof,而且它们都在任何会导致问题的大小之下。
在试图弄清楚这一点时,我注意到脚本运行得非常非常缓慢。所以我在上面运行了一个配置文件,希望结果能把我引向记忆问题的方向,并提出了两个令人困惑的问题。
首先,87% 的程序时间都花在了提交上,特别是在这行代码上:
self.transaction._new[state] = True
这可以在 中找到session.py:1367
。 self.transaction._new
是 的一个实例weakref.WeakKeyDictionary()
。为什么要weakref:261:__setitem__
占用这么多时间?
其次,即使程序完成('All Done' 已打印到标准输出),脚本仍在继续,看似永远,使用了 2.2GB 的内存。
我已经对weakrefs 进行了一些搜索,但没有看到有人提到我面临的性能问题。最终,鉴于它深埋在 SQL Alchemy 中,我对此无能为力,但我仍然会很感激任何想法。
主要学习
正如@zzzeek 所提到的,维护持久对象需要大量开销。这是一个显示增长的小图表。
趋势线表明每个持久实例占用大约 2KB 的内存开销,即使实例本身只有 30 个字节。这实际上给我带来了我学到的另一件事,那就是sys.getsizeof
带着一大粒盐。
此函数仅返回对象的浅大小,并且不考虑任何其他需要存在的对象才能使第一个对象有意义(__dict__
例如 )。你真的需要使用像 Heapy 这样的东西来很好地理解实例的实际内存占用。
我学到的最后一件事是,当 Python 即将耗尽内存并且疯狂地颠簸时,会发生不应该作为问题的一部分的奇怪事情。在我的情况下,大规模减速、指向弱引用创建的配置文件以及程序完成后的挂断都是内存问题的影响。一旦我停止创建和保留持久实例,而是只保留我需要的对象属性,所有其他问题都消失了。