如何减少Python中加载Pickle文件所需的时间

47

我在Python中创建了一个字典,并将其转储为pickle。它的大小增加到了300MB。现在,我想加载同样的pickle文件。

output = open('myfile.pkl', 'rb')
mydict = pickle.load(output)

加载pickle文件大约需要15秒的时间。 我该如何缩短这段时间?

硬件规格: Ubuntu 14.04, 4GB RAM

下面的代码展示了使用json、pickle和cPickle来转储或加载文件所需的时间。

转储后,文件大小约为300MB。

import json, pickle, cPickle
import os, timeit
import json

mydict= {all values to be added}

def dump_json():    
    output = open('myfile1.json', 'wb')
    json.dump(mydict, output)
    output.close()    

def dump_pickle():    
    output = open('myfile2.pkl', 'wb')
    pickle.dump(mydict, output,protocol=cPickle.HIGHEST_PROTOCOL)
    output.close()

def dump_cpickle():    
    output = open('myfile3.pkl', 'wb')
    cPickle.dump(mydict, output,protocol=cPickle.HIGHEST_PROTOCOL)
    output.close()

def load_json():
    output = open('myfile1.json', 'rb')
    mydict = json.load(output)
    output.close()

def load_pickle():
    output = open('myfile2.pkl', 'rb')
    mydict = pickle.load(output)
    output.close()

def load_cpickle():
    output = open('myfile3.pkl', 'rb')
    mydict = pickle.load(output)
    output.close()


if __name__ == '__main__':
    print "Json dump: "
    t = timeit.Timer(stmt="pickle_wr.dump_json()", setup="import pickle_wr")  
    print t.timeit(1),'\n'

    print "Pickle dump: "
    t = timeit.Timer(stmt="pickle_wr.dump_pickle()", setup="import pickle_wr")  
    print t.timeit(1),'\n'

    print "cPickle dump: "
    t = timeit.Timer(stmt="pickle_wr.dump_cpickle()", setup="import pickle_wr")  
    print t.timeit(1),'\n'

    print "Json load: "
    t = timeit.Timer(stmt="pickle_wr.load_json()", setup="import pickle_wr")  
    print t.timeit(1),'\n'

    print "pickle load: "
    t = timeit.Timer(stmt="pickle_wr.load_pickle()", setup="import pickle_wr")  
    print t.timeit(1),'\n'

    print "cPickle load: "
    t = timeit.Timer(stmt="pickle_wr.load_cpickle()", setup="import pickle_wr")  
    print t.timeit(1),'\n'

输出:

Json dump: 
42.5809804916 

Pickle dump: 
52.87407804489 

cPickle dump: 
1.1903790187836 

Json load: 
12.240660209656 

pickle load: 
24.48748306274 

cPickle load: 
24.4888298893

我看到cPickle在转储和加载时需要的时间较少,但是加载文件仍然需要很长时间

1
你是否正在使用cPickle?如果没有,请尝试一下。你可以将其作为一个即插即用的替代品。 - Carsten
@Carsten:谢谢。我听说 cPickle 比 Pickle 更快,但是它并没有减少我需要的那么多时间。 - iNikkz
1
当你转储字典时,请尝试添加一个可选的协议参数pickle.HIGHEST_PROTOCOL(或-1)。这将使用比默认的基于ASCII的数据格式更紧凑的二进制模式数据格式。 - martineau
2
@iNikkz 顺便说一下,如果这个答案有帮助,请通过点击绿色复选框来接受答案。 - twasbrillig
2
在load_cpickle()函数中,你应该调用cPickle.load(output)而不是pickle.load(output)! - Andreas Abel
显示剩余6条评论
3个回答

33

建议使用json代替pickle。在处理相对简单的对象——字典时,这应该是一个可选方案。

根据此网站的说法:

JSON 在读取(加载)上快25倍,在写入(转储)上快15倍。

另请参阅此问题:什么更快 - 加载一个 pickled 字典对象还是将 JSON 文件加载到字典中?

升级 Python 或使用marshal模块与固定的 Python 版本也可以提高速度(代码来自此处):

try: import cPickle
except: import pickle as cPickle
import pickle
import json, marshal, random
from time import time
from hashlib import md5

test_runs = 1000

if __name__ == "__main__":
    payload = {
        "float": [(random.randrange(0, 99) + random.random()) for i in range(1000)],
        "int": [random.randrange(0, 9999) for i in range(1000)],
        "str": [md5(str(random.random()).encode('utf8')).hexdigest() for i in range(1000)]
    }
    modules = [json, pickle, cPickle, marshal]

    for payload_type in payload:
        data = payload[payload_type]
        for module in modules:
            start = time()
            if module.__name__ in ['pickle', 'cPickle']:
                for i in range(test_runs): serialized = module.dumps(data, protocol=-1)
            else:
                for i in range(test_runs): serialized = module.dumps(data)
            w = time() - start
            start = time()
            for i in range(test_runs):
                unserialized = module.loads(serialized)
            r = time() - start
            print("%s %s W %.3f R %.3f" % (module.__name__, payload_type, w, r))

结果:

C:\Python27\python.exe -u "serialization_benchmark.py"
json int W 0.125 R 0.156
pickle int W 2.808 R 1.139
cPickle int W 0.047 R 0.046
marshal int W 0.016 R 0.031
json float W 1.981 R 0.624
pickle float W 2.607 R 1.092
cPickle float W 0.063 R 0.062
marshal float W 0.047 R 0.031
json str W 0.172 R 0.437
pickle str W 5.149 R 2.309
cPickle str W 0.281 R 0.156
marshal str W 0.109 R 0.047

C:\pypy-1.6\pypy-c -u "serialization_benchmark.py"
json int W 0.515 R 0.452
pickle int W 0.546 R 0.219
cPickle int W 0.577 R 0.171
marshal int W 0.032 R 0.031
json float W 2.390 R 1.341
pickle float W 0.656 R 0.436
cPickle float W 0.593 R 0.406
marshal float W 0.327 R 0.203
json str W 1.141 R 1.186
pickle str W 0.702 R 0.546
cPickle str W 0.828 R 0.562
marshal str W 0.265 R 0.078

c:\Python34\python -u "serialization_benchmark.py"
json int W 0.203 R 0.140
pickle int W 0.047 R 0.062
pickle int W 0.031 R 0.062
marshal int W 0.031 R 0.047
json float W 1.935 R 0.749
pickle float W 0.047 R 0.062
pickle float W 0.047 R 0.062
marshal float W 0.047 R 0.047
json str W 0.281 R 0.187
pickle str W 0.125 R 0.140
pickle str W 0.125 R 0.140
marshal str W 0.094 R 0.078

Python 3.4使用pickle协议3作为默认值,与协议4相比没有任何区别。Python 2的最高pickle协议是协议2(如果向dump提供负值,则选择该协议),速度是协议3的两倍慢。


3
@twasbrillig,我不想贬低,但是当我从“Scripts”目录运行“pip”时,遇到了https://dev59.com/sHE85IYBdhLWcg3wXCIv的问题。@Nikkz 相对时间应该是相同的。对于1000万个30字节的明文字符串,使用压缩将处理负担从慢速存储设备转移到快速CPU上。 - Cees Timmerman
1
@CeesTimmerman 我尝试使用 pip 安装 ujson,也遇到了这个错误。但是在这里有 Windows 二进制文件 http://www.lfd.uci.edu/~gohlke/pythonlibs/#ujson,我安装了 Python 2.7 和 3.4 的 64 位版本,两者都可以正常工作! - twasbrillig
1
@twasbrillig 谢谢。在我的64位机器上,32位Python 3.4中的marshalujson快2到3倍,并且产生的输出最多可以减少50%。 - Cees Timmerman
1
我在这里测试了zlibbz2压缩。zlib默认级别6大约是原始大小的一半,但加载速度慢5倍,尽管我只使用了RAM。 - Cees Timmerman
2
如果您的字典中有任何字节值,则JSON将无法工作,因此本文做出了巨大的假设。并非所有东西都可以进行JSON序列化! - Tommy
显示剩余5条评论

17
我在使用cPickle时,读取大文件(例如:~750 MB igraph对象-一个二进制pickle文件)方面有很好的结果。只需像这里提到的那样,简单地封装pickle load调用即可实现。
在您的情况下,示例片段可能如下所示:
import timeit
import cPickle as pickle
import gc


def load_cpickle_gc():
    output = open('myfile3.pkl', 'rb')

    # disable garbage collector
    gc.disable()

    mydict = pickle.load(output)

    # enable garbage collector again
    gc.enable()
    output.close()


if __name__ == '__main__':
    print "cPickle load (with gc workaround): "
    t = timeit.Timer(stmt="pickle_wr.load_cpickle_gc()", setup="import pickle_wr")
    print t.timeit(1),'\n'

当然,可能有更合适的方法来完成任务,但是这种解决方法可以大大缩短所需时间。

对我来说,它将时间从843.04秒减少到41.28秒,约为20倍。


如果我尝试这样做,就会出现错误:TypeError: 期望 str、bytes 或 os.PathLike 对象,而不是 _io.BufferedReader。这个 pickle 是用“wb”模式写入的。 - Varlor
你能提供一下序列化对象的代码片段或者尝试以下代码吗?with open(filename, 'wb') as output: pickle.dump(obj, output, pickle.HIGHEST_PROTOCOL) - Tejas Shah
cPickle 太快了! - jsj
非常感谢,这是加速我的脚本的非常方便的方法 :) - TabeaKischka
2
禁用垃圾回收器有什么作用或好处?如果有的话,为什么会有这种作用或好处? - Gokul NC
2
对于仍在阅读此答案的人,似乎cPickle已经被集成到Python 3中的pickle中了。https://dev59.com/s1oU5IYBdhLWcg3wxI2A#LLmkEYcBWogLw_1bOS0y - Carl H

7
如果您试图将字典存储到单个文件中,则会减慢加载时间。其中最简单的事情之一是将字典写入磁盘上的目录,并将每个字典条目作为单独的文件。然后,您可以使用多个线程(或使用多进程)对文件进行pickle和unpickle。对于非常大的字典,这应该比无论您选择哪种序列化器都从单个文件读取和写入要快得多。有一些软件包(如klepto和joblib)已经为您完成了很多(如果不是全部)。我建议您检查这些软件包。(注意: 我是klepto的作者。请参见https://github.com/uqfoundation/klepto。)

非常有趣的答案!我和你一样,我有各种序列化(100到300MB)的pickle文件,我想创建/加载到一个单独的字典中,但是逐个加载需要太多时间,我宁愿缓存。您是否可以提供或链接一个使用kelpto / joblib实现此目的的非常基本的示例? - nimig18
请查看 klepto.archives.dir_archiveklepto.archives.hdfdir_archive。它们都有一个稍微扩展了的字典接口。 - Mike McKerns
这个dir_archive的测试展示了一些基本功能:https://github.com/uqfoundation/klepto/blob/master/tests/test_readwrite.py - Mike McKerns

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接