Python 内存占用也太大了。

2018-09-25 10:12:15 +08:00
 skinny

我现有 5 个 collections.Counter 的 pickle 文件,单个文件在 84MB-240MB 之间,总共 664MB,总记录在 3 千多万,实际数据大小也就 400MB 左右。之所以分为 5 个文件是因为之前有几个 GB 的数据,我电脑内存小( 6GB ),又是机械硬盘,根本没办法一下子读取和处理,我分割成小块处理,最后变成了这 5 个文件,然后我想合并这些文件进行最后处理。

最终合并前,我预估过用 C 实现类似的字典( D[char[10],uint])合并,内存用的不会很多,即便是最粗糙的字典实现也只需要 680M 左右内存,我看 pickle 文件加起来才 664MB,就算翻 4 翻的内存占用机器也撑得住,可是一运行内存就被耗光,然后机器死机了,只能强制关机,根据死机前的 Python 占用情况,最终可能需要内存要 6GB-8GB 才能加载处理总共 664M 的这 5 个 pickle 文件。(只有 load 和 plus 操作)

可能有人会问我为什么不用 Redis 或者数据库查询,因为我没安装,我也就这一次需要以这种方式处理这种文件。前面用 Python 处理单个小文件时还好,虽然速度不敢恭维,不过还能接受,胜在写起来简单方便。

目前对 Python 感受就是慢、吃内存,但是写起来简单(当然也有非常复杂的,比如 asynio ),真的是胶水一样的语言。

22650 次点击
所在节点    Python
98 条回复
skinny
2018-09-25 15:07:34 +08:00
@nooper 下次遇到相同问题用这个。
skinny
2018-09-25 15:22:52 +08:00
@hahastudio 我知道高级语言会比 C 之类占用更多内存,我没有觉得有什么,一开始有心理准备,我粗糙的设计了下 C 的数据结构,预估了内存占用,然后看 pickle 文件和实际数据都不大,全部变成 Python 对象 collections.Counter[str, int]应该可以接受几倍的内存占用,当时我觉得 3GB 到 4GB 内存是可以搞定的,结果没想到爆了。我有考虑不周和偷懒的问题不存在,甚至还有穷的问题存在,不过一些人不知道他们到底在 diss 什么。
laike9m
2018-09-25 15:24:14 +08:00
如果 3kw 的 D[char[10],uint] 都要放进内存才能处理,是不是得考虑下算法的优化问题了
skinny
2018-09-25 15:36:54 +08:00
@laike9m 实际是用 C 根本没问题,顶天了 600MB 左右(粗糙 dict 实现),因为实际数据才 400MB 左右,我吐槽的是内存占用,我根本不在乎快到几分钟内完成,只要在半小时内搞定,不影响我同时在进行的编码任务就行,只要不是过高的 CPU 使用率或是内存占用高到导致其它操作(如编码)不能正常进行就行。一开始我是觉得没问题才这样做的,要是考虑算法,我干嘛用 collections.Counter。
hahastudio
2018-09-25 15:38:40 +08:00
@skinny 我猜是因为开始很多人认为 pickle 就是直接把内存 dump 下来?
顺带一提,你是不是遇到了类似于这边的问题? https://stackoverflow.com/questions/16288936/how-do-i-prevent-memory-leak-when-i-load-large-pickle-files-in-a-for-loop
lihongjie0209
2018-09-25 15:38:55 +08:00
@skinny 所以我说给一份测试数据然后大家自己跑一跑, 一直在这里说最后还有人怀疑你硬件有问题呢
skinny
2018-09-25 15:54:54 +08:00
@hahastudio 刚刚验证了你发的那个链接,按那个解决方法试了,结果还是一样。实际上就是太多小对象,Python 对象底层数据结构又比较大。
skinny
2018-09-25 15:57:50 +08:00
@lihongjie0209 追加里再次那么细致的说了数据类型、大小、数量、结构,眼神不好?你要不然按#28 楼说的用 range 生成一批数据测试,也没人说你,是不?
est
2018-09-25 16:09:49 +08:00
@skinny 你补充了那么多,比你第一次发帖的还长。你第一次发帖别人 1 楼注解回复了一句:不懂。

感受下。

好吧,我们说内存占用大的问题。一句话「 collections.Counter 数据总共 664MB 结果加载进去 6G 内存不够用」 不就完事了么。

非得说 Python 内存占用大。这和大多数人的经验相比而言是反直觉的。因为基本上能见到的 jvm 的应用起步 8G 内存。py 写的程序 200M 都算非常大的了。
est
2018-09-25 16:15:53 +08:00
@skinny 从你补充的信息来看, 大量 collections.Counter 数据做 plus 操作(就是合并?)是有额外的 copy 开销的。这里考虑用一些的 inline 的方式去优化。

你无论用什么语言都会遇到大 dict 合并问题,大 list 合并问题的。
est
2018-09-25 16:19:58 +08:00
其实最大的可能就是你反序列化之后的对象没有被 gc 掉。依然占着内存。看起来是 5 个 pickle 文件实际内存里存了好几份。

你试试 del 下那个对象。23333

或者你 pickle 有内存泄露。
2owe
2018-09-25 16:29:07 +08:00
强制使用 pickle protocol 高版本,pickle.HIGHEST_PROTOCOL ?只能说有改善,核心问题还是如你所知。
dbw9580
2018-09-25 16:31:30 +08:00
典型的又要马儿跑,又要马儿不吃草。
laqow
2018-09-25 16:38:21 +08:00
不用 Counter 和 pickle 不就完了吗
misaka19000
2018-09-25 16:52:22 +08:00
怎么感觉楼主好像在喷 Python ?
lihongjie0209
2018-09-25 16:53:31 +08:00
@skinny 知道这些信息并没有帮助, 写排序算法的时候也知道输入输出, 数据类型, 那么冒泡算法和归并算法就一样了? 你说 python 有什么问题的前提是你的代码要合理. 就你给的这些信息只够过过嘴瘾, diss python 而已, 并没有什么实际意义.

我已经说第三遍了, 给测试数据, 给需求, 大家都写不出来那是 python 的问题, 大家写的出来那是你的问题.
wutiantong
2018-09-25 17:39:07 +08:00
楼主的不友善态度和杠精精神好像传染了我
我一个没学过 python 的花了点时间搜了一圈 google
又按照楼主的描述写了点测试,我后面贴一下
shm7
2018-09-25 17:40:28 +08:00
9 楼,大文件存储用 hdf5,h5py -> pytable 都可以用。
wutiantong
2018-09-25 17:43:15 +08:00
实际运行环境是新版 MBP,python3.7

```
import collections
import pickle

def make_cnt(section):
cnt = collections.Counter()
for i in range(6000000):
cnt[str(i +6000000*section)] = i
return cnt

if __name__ == '__main__':
cnt0 = make_cnt(0)
pickle.dump(cnt0, open('/tmp/cnt0', 'wb'))
input("Press Enter to continue...")
cnt1 = make_cnt(1)
pickle.dump(cnt1, open('/tmp/cnt1', 'wb'))
input("Press Enter to continue...")
cnt2 = make_cnt(2)
pickle.dump(cnt2, open('/tmp/cnt2', 'wb'))
input("Press Enter to continue...")
cnt3 = make_cnt(3)
pickle.dump(cnt3, open('/tmp/cnt3', 'wb'))
input("Press Enter to continue...")
cnt4 = make_cnt(4)
pickle.dump(cnt4, open('/tmp/cnt4', 'wb'))
input("Press Enter to continue...")
```
这个代码主要是观察 Counter 的内存占用情况,观察结果是:
1. 一个 600 万条的 Counter 占用 900M 内存,平均每条记录占用 150 个字节,5 个这样的 Counter 总计占用内存 4.5G (个人觉得并不算很过分)
2. pickle 序列化后得到的文件是 130 多 M


```
import collections
import pickle

if __name__ == '__main__':
cnt0 = pickle.load(open('/tmp/cnt0', 'rb'))
input("Press Enter to continue...")
cnt1 = pickle.load(open('/tmp/cnt1', 'rb'))
input("Press Enter to continue...")
cnt2 = pickle.load(open('/tmp/cnt2', 'rb'))
input("Press Enter to continue...")
cnt3 = pickle.load(open('/tmp/cnt3', 'rb'))
input("Press Enter to continue...")
cnt4 = pickle.load(open('/tmp/cnt4', 'rb'))
input("Press Enter to continue...")
```
这个代码主要是观察 pickle 反序列化过程中会不会产生额外的内存占用,以及反序列化出来的对象实际大小如何,观察结果是:
1. pickle 的反序列化过程有轻微的内存使用上涨但并不会产生内存泄漏
2. 反序列化出来的 Counter 略小于直接动态生成的 Counter,5 个 Counter 总计占用内存 4.3G


总结一下:
1. pickle 这个库挺不错的,压缩率和运算性能都很可观,而且也没观察到内存爆增或者内存泄漏的情况(至少在我的测试环境下是如此的)
2. 楼主所用的 Counter 每条记录占用 150 字节左右,总计应该是需要 4.5G 左右的内存,6G 内存应该是能撑得住的
3. 我猜楼主在后续的合并操作没有及时回收内存,故而需求了几乎翻倍的内存
sunhk25
2018-09-25 17:51:52 +08:00

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/492253

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX