You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

从Python 2迁移到Python 3后numpy.fromfile性能大幅下降的原因及优化方案咨询

Python 3下可变长度二进制文件高效读取方案及numpy.fromfile性能暴跌原因

嘿,针对你的问题——从Python2迁移到3后numpy.fromfile性能暴跌,以及如何找回高性能读取可变长度二进制文件的方法,结合你的测试场景和代码,我整理了下面的内容:

一、Python 3里实现接近(甚至超过)Python2的读取性能

你的测试已经发现numpy.frombuffer在Python3下性能下降幅度很小,这是个很好的起点,在此基础上还有几个优化方向:

  • numpy.frombuffer配合批量大块读取:频繁的小IO操作是性能杀手,你可以一次性读取大块数据到内存缓冲区,再在缓冲区里解析记录,减少IO次数。比如试试这个优化后的读取函数:

    def read_binary_buffered(filename, buffer_size=1024*1024*64):  # 64MB缓冲块,可根据内存调整
        checksum = 0.0
        with open(filename, 'rb') as f:
            buffer = b''
            while True:
                chunk = f.read(buffer_size)
                if not chunk:
                    break
                buffer += chunk
                # 循环处理缓冲区内的完整记录
                while len(buffer) >= np.dtype('i4').itemsize:
                    # 先读记录长度
                    record_len = np.frombuffer(buffer[:4], dtype='i4')[0]
                    required_bytes = 4 + record_len * 8  # i4是4字节,d是8字节
                    if len(buffer) < required_bytes:
                        break  # 数据不够,等下一块
                    # 读取数据段
                    x = np.frombuffer(buffer[4:required_bytes], dtype='d', count=record_len)
                    checksum += x.sum()
                    # 截断缓冲区,去掉已处理的部分
                    buffer = buffer[required_bytes:]
            # 处理最后剩余的缓冲数据
            while len(buffer) >= 4:
                record_len = np.frombuffer(buffer[:4], dtype='i4')[0]
                required_bytes = 4 + record_len *8
                if len(buffer) < required_bytes:
                    break
                x = np.frombuffer(buffer[4:required_bytes], dtype='d', count=record_len)
                checksum += x.sum()
                buffer = buffer[required_bytes:]
        assert(np.abs(checksum) < 1e-6)
    

    这种方式把多次小读合并成大块读取,能显著减少IO层面的开销,甚至可能比Python2下的numpy.fromfile更快。

  • 用内存映射(mmap)处理超大文件:你的文件在0.5-20GB之间,用mmap把文件直接映射到内存,numpy可以直接操作映射区域,完全绕开Python的IO层,性能拉满:

    import mmap
    
    def read_binary_mmap(filename):
        checksum = 0.0
        with open(filename, 'rb') as f:
            # 映射整个文件到内存,无需一次性加载
            with mmap.mmap(f.fileno(), length=0, access=mmap.ACCESS_READ) as mm:
                offset = 0
                file_len = len(mm)
                while offset +4 <= file_len:
                    # 从映射内存读记录长度
                    record_len = np.frombuffer(mm[offset:offset+4], dtype='i4')[0]
                    offset +=4
                    # 读对应的数据段
                    x = np.frombuffer(mm[offset:offset+record_len*8], dtype='d', count=record_len)
                    checksum += x.sum()
                    offset += record_len*8
        assert(np.abs(checksum) <1e-6)
    

    内存映射对超大文件特别友好,它不会把整个文件塞进内存,而是按需从磁盘加载,同时numpy操作映射内存的速度几乎和操作原生数组一样。

  • 如果场景允许,直接用numpy.fromfile读文件名(而非文件对象):注意哦,numpy.fromfile如果直接传文件名,在Python3里性能依然很高——因为它会直接用C层打开文件读取,跳过Python的IO封装。但这种方式只适合你知道要读多少数据的情况,没法像你现在这样动态处理可变长度记录,所以只适合特定场景。

二、为什么Python3里numpy.fromfile性能暴跌一个数量级?

这个锅得甩给Python2和Python3文件对象的底层实现差异:

  • Python2的文件对象是直接绑定C的FILE*指针numpy.fromfile可以直接调用C的fread函数读取数据,完全绕开Python层的开销,所以速度飞快。
  • Python3的文件对象是封装后的io.BufferedReader,为了安全和兼容性,它加了很多Python层的逻辑。当你把这个Python文件对象传给numpy.fromfile时,numpy没法直接访问底层的C文件指针,只能通过Python的read()方法来一点点拿数据——这就引入了大量的Python级别的开销:GIL锁竞争、Python函数调用、缓冲区复制等等,每一次小读取都会触发这些开销,累积起来就导致性能暴跌9倍多。
  • numpy.frombuffer是直接操作已经读到内存里的字节串,完全避开了Python IO层的额外消耗,所以性能下降幅度很小,只有10%左右。

顺便说一句,numpy文档里说numpy.fromfile是“高效方式”,其实是针对直接传文件名或者Python2的文件对象的情况,在Python3传文件对象的场景下,这个描述已经过时了。


内容的提问来源于stack exchange,提问作者Dmytro Oliinychenko

火山引擎 最新活动