从Python 2迁移到Python 3后numpy.fromfile性能大幅下降的原因及优化方案咨询
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




