以块的形式处理比物理内存大得多的数据

Posted

技术标签:

【中文标题】以块的形式处理比物理内存大得多的数据【英文标题】:Process data, much larger than physical memory, in chunks 【发布时间】:2013-07-16 15:40:23 【问题描述】:

我需要处理一些比 RAM 大几百倍的数据。我想读一大块,处理它,保存结果,释放内存并重复。有没有办法在 python 中提高效率?

【问题讨论】:

可能重复:***.com/questions/519633/… 使用 python 查看 pandas 和 pytables / hdf 或 hadoop 流。如果你在 Linux 上,你可以使用 dumbo 来促进 hadoop python 交互。 Python 拥有强大而充满活力的数据分析社区;使用 Google 搜索很难错过。 不是重复,但也相关:Python file iterator over a binary file with newer idiom. 另见Why doesn't Python's mmap work with large files?。它没有直接关系,但它有一些关于滑动 mmap 窗口的有用讨论,以及 mmapread 在幕后的不同之处,等等。 答案:与C相比,没有。不客气。请接受我的评论。 【参考方案1】:

一般的关键是你要迭代地处理文件。

如果您只是处理一个文本文件,这很简单:for line in f: 一次只能读取一行。 (实际上它会缓冲一些东西,但缓冲区足够小,您不必担心。)

如果您正在处理一些其他特定的文件类型,例如 numpy 二进制文件、CSV 文件、XML 文档等,通常有类似的专用解决方案,但除非您告诉我们您拥有什么样的数据。

但是如果你有一个通用的二进制文件呢?


首先,read 方法需要一个可选的最大字节数来读取。所以,而不是这个:

data = f.read()
process(data)

你可以这样做:

while True:
    data = f.read(8192)
    if not data:
        break
    process(data)

你可能想写一个这样的函数:

def chunks(f):
    while True:
        data = f.read(8192)
        if not data:
            break
        yield data

那么你可以这样做:

for chunk in chunks(f):
    process(chunk)

您也可以使用两个参数 iter 来做到这一点,但很多人觉得这有点晦涩:

for chunk in iter(partial(f.read, 8192), b''):
    process(chunk)

无论哪种方式,此选项都适用于下面的所有其他变体(除了单个 mmap,这是微不足道的,没有意义)。


那里的数字 8192 并没有什么神奇之处。您通常确实需要 2 的幂,理想情况下是系统页面大小的倍数。除此之外,无论您使用 4KB 还是 4MB,您的性能都不会有太大差异——如果是这样,您必须测试最适合您的用例的方法。


无论如何,这假设您可以一次只处理每个 8K,而无需保留任何上下文。例如,如果您要将数据输入渐进式解码器或哈希器或其他东西,那就完美了。

但是,如果您需要一次处理一个“块”,那么您的块最终可能会跨越 8K 边界。你是怎么处理的?

这取决于您的块在文件中的分隔方式,但基本思想非常简单。例如,假设您使用 NUL 字节作为分隔符(不太可能,但作为玩具示例很容易展示)。

data = b''
while True:
    buf = f.read(8192)
    if not buf:
        process(data)
        break
    data += buf
    chunks = data.split(b'\0')
    for chunk in chunks[:-1]:
        process(chunk)
    data = chunks[-1]

这种代码在网络中很常见(因为sockets不能只是“读取所有”,所以你总是必须读入缓冲区并块成消息),因此您可能会在使用类似于您的文件格式的协议的网络代码中找到一些有用的示例。


或者,您可以使用mmap

如果您的虚拟内存大小大于文件,这很简单:

with mmap.mmap(f.fileno(), access=mmap.ACCESS_READ) as m:
    process(m)

现在m 就像一个巨大的bytes 对象,就像您调用read() 将整个内容读入内存一样——但操作系统会根据需要自动将位分页进出内存。


如果您尝试读取一个太大而无法适应您的虚拟内存大小的文件(例如,使用 32 位 Python 的 4GB 文件,或使用 64 位 Python 的 20EB 文件 - 这很可能发生在2013 如果你正在读取一个稀疏或虚拟文件,比如 Linux 上另一个进程的 VM 文件),你必须一次在一个文件中实现窗口化——mmap。例如:

windowsize = 8*1024*1024
size = os.fstat(f.fileno()).st_size
for start in range(0, size, window size):
    with mmap.mmap(f.fileno(), access=mmap.ACCESS_READ, 
                   length=windowsize, offset=start) as m:
        process(m)

当然,如果您需要分隔事物,映射窗口与读取块有相同的问题,您可以通过相同的方式解决它。

但是,作为一种优化,您可以将窗口向前滑动到包含最后一条完整消息末尾的页面,而不是一次 8MB,而不是缓冲,这样就可以避免任何复制。这有点复杂,所以如果你想这样做,请搜索“滑动 mmap 窗口”之类的内容,如果遇到困难,请编写一个新问题。

【讨论】:

我赞扬您对如此广泛的问题给出如此深思熟虑的答案。说真的,+1。 谢谢!在我的情况下,出于效率原因,我希望一个块是 RAM 的大小。你能做到不试错吗? @marshall:你真的不希望它是(物理)RAM 的大小,因为你的解释器空间的其余部分、内核、其他进程、磁盘缓存等。此外,一旦获得足够大的块,就没有更多的收益了;如果您的代码尽可能接近磁盘 DMA 的完全流水线,那么更大的读取将无济于事。您可以(并且应该)自己测试它,但通常最佳位置在 4KB 到 8MB 之间,而不是接近物理内存的限制。 @marshall:同时,如果您出于某种原因确实想要物理 RAM 大小,则没有跨平台的方法可以做到这一点,但您始终可以从 @ 987654340@ 文件系统用于 linux,ctypessysctlbyname 用于大多数其他 *nix 系统,win2apiGlobalMemoryStatusEx 用于 Windows 等。 关于如何确定 8192 字节是否是一个好数字的任何指针?

以上是关于以块的形式处理比物理内存大得多的数据的主要内容,如果未能解决你的问题,请参考以下文章

Spark 如何处理比 Spark 存储大得多的数据?

即使可用堆内存比使用的大得多,也会出现堆内存不足错误

操作系统篇-浅析分页机制

python 以块的形式读取文件而不将整个文件加载到内存中。

python 在Quantopian中创建一个管道实例并以块的形式运行以避免内存过载。包括管道的常见导入。

如何在ios中将数据以块的形式写入磁盘