Python内存优化技巧
我需要优化我应用程序的内存使用情况。
请不要给我讲那些关于在编写Python代码时不应该关心内存的道理。我现在遇到内存问题是因为我使用了非常大的默认字典(没错,我也想要速度快)。我目前的内存消耗是350MB,而且还在增加。我已经无法使用共享主机了,如果我的Apache开启更多进程,内存会翻倍甚至三倍……这可真是太贵了。
我已经进行了深入的性能分析,知道了问题出在哪里。
我有几个大的字典(超过10万条记录),键是Unicode字符串。一个字典的起始大小是140字节,而且增长得很快,但更大的问题是这些键。Python在内存中优化字符串(我听说过),这样查找时可以通过ID比较来加快速度(称为“字符串驻留”)。但我不确定这对Unicode字符串是否也适用(我没能做到“字符串驻留”)。
字典中存储的对象是元组的列表(一个对象,一个整数,一个整数)。
my_big_dict[some_unicode_string].append((my_object, an_int, another_int))
我发现将字典拆分成几个小字典是值得的,因为元组占用了很多空间……
我还发现,通过对字符串进行哈希处理后再用作键,可以节省内存!但可惜的是,我在32位系统上遇到了生日碰撞的问题。(顺便问一下:在32位系统上有没有可以使用的64位键字典?)
我在Linux(生产环境)和Windows上使用Python 2.6.5。有没有关于优化字典/列表/元组内存使用的建议?我甚至考虑过使用C语言——我不在乎这段小代码是否丑陋。它只是一个单独的位置。
提前谢谢你们!
7 个回答
我遇到过一些情况,需要对一堆大对象进行排序和筛选,方法是根据几个元数据属性来进行的。因为我不需要这些对象的大部分内容,所以我把它们存到了硬盘上。
由于你的数据类型很简单,使用一个快速的SQLite数据库可能会解决你所有的问题,甚至还能稍微加快速度。
对于一个网页应用,你应该使用数据库。你现在的做法是为每个apache进程创建一个字典的副本,这样非常浪费资源。如果你的服务器内存足够,数据库表会被缓存到内存中(如果内存不够放下一个副本,那就给服务器加点内存吧)。记得在数据库表上设置正确的索引,否则性能会很差。
我建议你这样做:把所有的值存储在数据库里,同时在内存中保持一个字典,用字符串的哈希值作为键。如果发生冲突,就从数据库中获取值;否则(大多数情况下)就直接使用字典。这样实际上就相当于一个巨大的缓存。
在Python中,字典有个问题,就是占用的空间比较大:即使是一个整数到整数的字典,在32位系统上每对键值对也要45-80字节。而一个array.array('i')
只需要8字节来存储一对整数,如果稍微做点管理,可以实现一个相对快速的基于数组的整数 → 整数字典。
一旦你有了一个内存使用效率高的整数到整数的字典,就可以把你的字符串 → (对象, 整数, 整数)字典拆分成三个字典,并用哈希值代替完整的字符串。这样你就会得到一个整数 → 对象字典和两个整数 → 整数字典。可以这样模拟整数 → 对象字典:保持一个对象的列表,把对象的索引作为整数 → 整数字典的值。
我知道要实现一个基于数组的字典需要写不少代码。我之前也遇到过类似的问题,已经实现了一个相对快速、非常节省内存的通用哈希整数字典。这是我的代码(BSD许可证)。它是基于数组的(每对8字节),处理了键的哈希和冲突检查,写入时保持数组(实际上是几个较小的数组)有序,读取时进行二分查找。你的代码可以简化成这样:
dictionary = HashIntDict(checking = HashIntDict.CHK_SHOUTING)
# ...
database.store(k, v)
try:
dictionary[k] = v
except CollisionError:
pass
# ...
try:
v = dictionary[k]
except CollisionError:
v = database.fetch(k)
其中checking
参数指定了发生冲突时的处理方式:CHK_SHOUTING
在读取和写入时会抛出CollisionError
,CHK_DELETING
在读取时返回None
,写入时保持安静,CHK_IGNORING
则不进行冲突检查。
接下来是我实现的简要描述,欢迎提供优化建议!顶层数据结构是一个常规的数组字典。每个数组最多可以包含2^16 = 65536
对整数(2^32
的平方根)。一个键k
和对应的值v
都存储在k/65536
这个数组中。数组是按需初始化的,并且根据键保持有序。每次读取和写入时都会进行二分查找。冲突检查是可选的。如果启用,尝试覆盖已存在的键时,会将该键和相关值从字典中移除,并将该键添加到冲突键的集合中,并且(同样是可选的)抛出异常。