加快读取多个泡菜文件

Posted

技术标签:

【中文标题】加快读取多个泡菜文件【英文标题】:Speed up reading multiple pickle files 【发布时间】:2021-05-26 14:55:06 【问题描述】:

我有很多泡菜文件。目前我循环阅读它们,但这需要很多时间。我想加快速度,但不知道该怎么做。

多处理不起作用,因为为了将数据从子子进程传输到主进程,需要对数据进行序列化(腌制)和反序列化。

由于 GIL,使用线程也无济于事。

我认为解决方案是一些用 C 语言编写的库,它需要读取文件列表,然后运行多个线程(没有 GIL)。周围有这样的东西吗?

更新 回答您的问题:

文件是用于 ML 的数据处理的部分产品 有 pandas.Series 对象,但预先不知道 dtype 我想要很多文件,因为我们想轻松选择任何子集 我想要多个小文件而不是一个大文件,因为一个大文件的反序列​​化会占用更多内存(在某个时间点,我们已经序列化字符串和反序列化对象) 文件的大小可能会有很大差异 我使用 python 3.7 所以我相信它实际上是 cPickle 使用 pickle 非常灵活,因为我不必担心底层类型 - 我可以保存任何东西

【问题讨论】:

这有帮助吗? ***.com/a/50479955/3288092 @BernardL 不是。我从一张光盘读取数据,并没有看到使用线程的任何增益。我认为解压和反序列化是在 GIL 下运行的,IO 对总时间的影响较小。 我认为这个进程更多的是 I/O 绑定然后是处理绑定。 如果瓶颈主要涉及从 pickle 数据创建 Python 对象,如果不以某种方式重新架构代码或切换到不强加GIL 的局限性。 泡菜文件里有什么?我的意思是什么样的对象?你试过cpickle吗? 【参考方案1】:

我同意 cmets 中指出的内容,即由于 python 本身的限制(主要是 GIL 锁,正如您所指出的),除了您现在正在做的事情之外,加载信息可能没有更快的速度.或者,如果有一种方法,它可能既是高度技术性的,而且最终只会给您带来适度的速度提升。

也就是说,根据您拥有的数据类型,使用quickle 或pyrobuf 可能会更快。

【讨论】:

...或cpickle,正如@MarkSetchell 建议的那样。如果我没看错,cpickle 将与现有数据兼容。 - 似乎pyrobuf 需要Cython,这将消除 GIL,从而彻底改变问题的性质。 @CryptoFool - 值得添加,但我没有使用它,但出于不同的原因:pickle 和(显然是 cpickle)自动运行代码。这是让我每次都畏缩的事情。如果这只是我的东西,当然。但是,如果我正在发送或接收某些东西,那就是我不愿意承担的风险。 @MarkSetchell - 我在寻找 cipickle 的存储库时遇到问题。显然,pickle 现在在内部使用 cpickel (***.com/questions/37132899/…) 并且已经这样做了一段时间了。所以这似乎没有任何好处。这与你的经验相符吗?【参考方案2】:

我认为解决方案是一些用 C 编写的库 获取要读取的文件列表,然后运行多个线程(没有 吉尔)。 周围有这样的东西吗?

简而言之:不。 pickle 显然对足够多的人来说已经足够好了,以至于没有与 pickle 协议完全兼容的主要替代实现。在 python 3 中的某个时候,cPicklepickle 合并,并且无论如何都不会释放 GIL,这就是为什么线程对您没有帮助(在 _pickle.c 中搜索 Py_BEGIN_ALLOW_THREADS,您将一无所获)。

如果您的数据可以重组为更简单的数据格式(如 csv)或二进制格式(如numpy 的 npy),则读取数据时的 CPU 开销会更少。 Pickle 首先是为了灵活性而不是速度或紧凑性而构建的。对于更复杂的速度较低的规则,一个可能的例外是使用h5py 的 HDF5 格式,这可能相当复杂,而且我曾经将 sata ssd 的带宽最大化。

最后你提到你有很多泡菜文件,这本身可能会造成不小的开销。每次打开新文件时,操作系统都会产生一些开销。您可以方便地通过简单地将泡菜文件附加在一起来组合它们。然后你可以调用Unpickler.load() 直到你到达文件的末尾。这是一个使用shutil将两个pickle文件组合在一起的快速示例

import pickle, shutil, os

#some dummy data
d1 = 'a': 1, 'b': 2, 1: 'a', 2: 'b'
d2 = 'c': 3, 'd': 4, 3: 'c', 4: 'd'

#create two pickles
with open('test1.pickle', 'wb') as f:
    pickle.Pickler(f).dump(d1)
with open('test2.pickle', 'wb') as f:
    pickle.Pickler(f).dump(d2)
    
#combine list of pickle files
with open('test3.pickle', 'wb') as dst:
    for pickle_file in ['test1.pickle', 'test2.pickle']:
        with open(pickle_file, 'rb') as src:
            shutil.copyfileobj(src, dst)
            
#unpack the data
with open('test3.pickle', 'rb') as f:
    p = pickle.Unpickler(f)
    while True:
        try:
            print(p.load())
        except EOFError:
            break
        
#cleanup
os.remove('test1.pickle')
os.remove('test2.pickle')
os.remove('test3.pickle')

【讨论】:

这不是竞争项目的指标所显示的。 @hrokr,如果有任何比pickle更快的pickle协议完全兼容的主要项目我不知道。 quicklepyrobuf 将属于第二段,鼓励转换为具有更快、更有效反序列化的另一种格式。 如果您查看对该问题的修改,您会注意到该要求是在提出原始问题五天后添加的。而且,虽然我知道 OP 可能想要一些可以处理任何数据类型的东西,但大多数东西都针对一个或另一个领域的速度进行了优化——这就是几个人提出的问题和原因。 @Aaron 感谢您指出缺少 Py_BEGIN_ALLOW_THREADS,这表明尝试使用来自 _pickle.c 的代码创建 C 模块无济于事。【参考方案3】:

我认为您应该尝试使用类似于open() 但速度更快的 mmap(内存映射文件)。

注意:如果您的每个文件都很大,则使用 mmap,否则如果文件很小,请使用常规方法。

我写了一个示例,你可以试试。

import mmap
from time import perf_counter as pf
def load_files(filelist):
    start = pf() # for rough time calculations
    for filename in filelist:
        with open(filename, mode="r", encoding="utf8") as file_obj:
            with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ) as mmap_file_obj:
                data = pickle.load(mmap_file_obj)
                print(data)
    print(f'Operation took pf()-start sec(s)')

这里mmap.ACCESS_READ是以二进制打开文件的模式。 open 返回的file_obj 仅用于获取file descriptor,该file descriptor 用于通过mmap 作为内存映射文件打开文件流。 正如您在下面的 python 文档中看到的那样,open 返回file descriptor 或简称fd。因此,我们不必明智地对 file_obj 操作做任何事情。我们只需要它的fileno() 方法来获取它的文件描述符。此外,我们不会在mmap_file_obj 之前关闭file_obj。请好好看看。我们首先关闭mmap 块。 正如你在评论中所说的那样。

open (file, flags[, mode])
Open the file file and set various flags according to flags and possibly its mode according to mode. 
The default mode is 0777 (octal), and the current umask value is first masked out. 
Return the file descriptor for the newly opened file.

试一试,看看它对您的运营有多大影响 您可以阅读有关 mmap here 的更多信息。还有关于文件描述符here

【讨论】:

你(1)不需要以二进制模式打开腌制文件吗? (2) 你正在破坏file_obj 调用open 所返回的mmap.mmap,这似乎不正确。 mmap.ACCESS_READ 是以二进制打开文件的模式。 open 返回的file_obj 仅用于获取file descriptor 用于通过mmap@Booboo 打开流到文件的流 是什么让您认为内存映射文件可以加快读取速度?如果您要对文件进行许多小读取,或者要对文件执行随机访问,这是正确的。如果您要批量读取文件,那么通过内存映射比直接读取文件更快吗?没有理由让它更快。 @SaGaR 这些是问题。就(1)而言,我已经尝试过使用二进制模式并且有效。就 (2) 而言,我还没有尝试过,但是您指向的链接肯定使用不同的变量来调用 mmap.mmap,并且 open 的上下文管理器将尝试在 file_obj 上调用 close,这可能不会失败,因为它可能对内存映射文件有效,但您可能仍将原始文件句柄保持打开状态。我不知道——它只是看起来有问题。如果我确定我会拒绝你而不是问。 @SaGaR - 我对事物运作方式的理解似乎与您所说的相反。为什么将整个文件读入内存映射比在解码之前将其读入 Python 的地址空间更快?我没有理由知道内存映射大文件或小文件应该提供任何优势。在这种情况下,文件 I/O 是相同的。内存映射文件的优势在于,当代码不打算以这种方式访问​​文件内容时,能够一次读取所有文件,而是以小块的形式,或者在文件中四处寻找。【参考方案4】:

您可以尝试多处理:

import os,pickle
pickle_list=os.listdir("pickles")

output_dict=dict.fromkeys(pickle_list, '')

def pickle_process_func(picklename):
    with open("pickles/"+picklename, 'rb') as file:
        dapickle=pickle.load(file)

    #if you need previus files output wait for it
    while(!output_dict[pickle_list[pickle_list.index(picklename)-1]]):
        continue

    #thandosomesh
    print("loaded")
    output_dict[picklename]=custom_func_i_dunno(dapickle)
    

from multiprocessing import Pool

with Pool(processes=10) as pool:
     pool.map(pickle_process_func, pickle_list)

【讨论】:

这已在问题中得到解决。multiprocessing.Pool.map 使用单个Queue(使用pickle 对数据进行序列化和反序列化)来接收来自子进程的结果,因此速度会在那里成为瓶颈反而。您仍然受到单核解封数据流的速度的限制。 使用共享内存传递结果怎么样? @CyrillePontvieux multiprocessing.shared_memory 仅公开二进制字节状的内存数组,并且不支持共享任意 python 对象。对于像 numpy 数组或 pandas 系列对象这样的底层数据只是二进制数组的对象来说非常有用,但结构化数据要困难得多。 @Aaron 将泡菜转换成 sql 怎么样? @RifatAlptekinÇetin 必须以速度为基准...似乎 OP 真的想要泡菜...【参考方案5】:

考虑通过h5py 而不是pickle 使用HDF5。在 Pandasnumpy 数据结构和 it supports most common data types 和压缩中使用数字数据时,性能通常比 pickle 好得多。

【讨论】:

以上是关于加快读取多个泡菜文件的主要内容,如果未能解决你的问题,请参考以下文章

允许泡菜有多个参数

在 C++ 中,如何使用多个线程读取一个文件?

多个线程从同一个文件中读取

如何使用多个线程读取大量文件,请帮助我!

如何使用多个工作人员加快批量导入谷歌云数据存储的速度?

如何使用多个 INNER JOIN 加快查询速度