处理比物理内存大得多的数据块。

11

我需要处理的数据比内存大几百倍。 我想读入一个大块,处理它,保存结果,释放内存并重复执行。 有没有办法在Python中使这个过程更加高效?


2
可能是重复的问题:https://dev59.com/KnRB5IYBdhLWcg3wxZ7Y - David Cain
3
请查看使用Python的pandas和pytables / hdf或hadoop流处理。如果您使用Linux,可以使用dumbo来简化与hadoop的Python交互。Python拥有强大而活跃的数据分析社区,在Google上搜索很容易找到相关内容。 - agconti
还可以参考为什么Python的mmap不能处理大文件?。虽然不是直接相关,但其中有一些关于滑动mmap窗口、以及mmapread在底层实现上的区别等有用的讨论。 - abarnert
如果考虑到大多数人写 C 语言的难度,那么用 Python 会更有效率。我想我们都知道彼此的意思。 - 2rs2ts
@2rs2ts:我不认为我真的知道你的意思。(如果我知道,那么你就是错的,而且我不认为是这样。)在Python中,以8KB为单位读取或以8MB为单位映射有什么比C语言更低效的地方吗? - abarnert
显示剩余3条评论
1个回答

25

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

如果你只是处理文本文件,这很简单: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]

这种代码在网络编程中非常常见(因为套接字不能只是“读取全部”,所以您总是需要读取到缓冲区并将其分成消息),因此您可能会在使用类似于您的文件格式的协议的网络代码中找到一些有用的示例。


或者,您可以使用mmap

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

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

现在m像一个巨大的bytes对象,就好像你调用了read()将整个文件读入内存一样——但是操作系统会根据需要自动分页内存中的位。


如果您尝试读取太大以至于无法适应虚拟内存大小的文件(例如使用32位Python的4GB文件,或者使用64位Python的20EB文件——只有当您读取像Linux上另一个进程的VM文件这样的稀疏或虚拟文件时才可能发生这种情况),您必须实现窗口化——每次映射一部分文件。例如:

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
我赞扬你对如此广泛的问题给出了一个经过深思熟虑的答案。真的,+1。 - 2rs2ts
谢谢!对于我的情况,由于效率原因,我希望一个块的大小等于RAM的大小。你能否在不进行试验和错误的情况下做到这一点? - marshall
@marshall:你实际上并不希望将它设置为(物理)内存的大小,因为其他进程、内核、磁盘缓存等都需要一些内存。此外,一旦你获得了足够大的块,就没有更多的收益了;如果你的代码与磁盘DMAs尽可能接近完全流水线,则更大的读取量无法帮助提高性能。你可以(也应该)自己进行测试,但通常最佳点将在4KB到8MB之间,而不是接近物理内存限制的任何地方。 - abarnert
@marshall:同时,如果您因某种原因确实需要物理RAM大小,那么没有跨平台的方法来实现它,但是您可以从Linux的/proc文件系统中读取,对于大多数其他的*nix系统,可以使用ctypessysctlbyname,对于Windows,则可以使用win2apiGlobalMemoryStatusEx等方式。 - abarnert
有没有关于如何决定8192字节是否是一个好的数字的指导? - Nemo
@Nemo 用8192进行测试,然后用4096和16384进行测试,看看哪个更快?真的没有“一刀切”的值,任何你读到的东西都可能在不同的系统或几年后是错误的。 - abarnert

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