在特定位置从二进制文件读取整数的性能问题
我有一个文件,里面存储了以二进制形式表示的整数,我想从特定的位置提取这些值。这个文件实际上是一个很大的序列化整数数组,我需要获取特定索引的值。我写了以下代码,但它的速度比我之前用F#写的版本慢得多。
import os, struct
def read_values(filename, indices):
# indices are sorted and unique
values = []
with open(filename, 'rb') as f:
for index in indices:
f.seek(index*4L, os.SEEK_SET)
b = f.read(4)
v = struct.unpack("@i", b)[0]
values.append(v)
return values
为了比较,这里是F#的版本:
open System
open System.IO
let readValue (reader:BinaryReader) cellIndex =
// set stream to correct location
reader.BaseStream.Position <- cellIndex*4L
match reader.ReadInt32() with
| Int32.MinValue -> None
| v -> Some(v)
let readValues fileName indices =
use reader = new BinaryReader(File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
// Use list or array to force creation of values (otherwise reader gets disposed before the values are read)
let values = List.map (readValue reader) (List.ofSeq indices)
values
有没有什么建议可以提高Python版本的性能,比如使用numpy?
更新
使用Hdf5效果很好(在我的测试文件中,从5秒缩短到0.8秒):
import tables
def read_values_hdf5(filename, indices):
values = []
with tables.open_file(filename) as f:
dset = f.root.raster
return dset[indices]
更新2
我选择了np.memmap,因为它的性能和hdf5相似,而且我已经在生产环境中使用numpy了。
2 个回答
这个索引列表是排好序的吗?我觉得如果这个列表是有序的,性能会更好,因为这样你就可以减少很多次磁盘查找。
根据你的索引文件大小,你可能想把它完全读入一个numpy数组。如果文件不大,顺序读取可能比频繁跳转位置要快。
一个关于跳转操作的问题是,Python使用的是缓冲输入。如果程序是用一些低级语言写的,使用无缓冲的输入输出会是个好主意,因为你只需要几个值。
import numpy as np
# read the complete index into memory
index_array = np.fromfile("my_index", dtype=np.uint32)
# look up the indices you need (indices being a list of indices)
return index_array[indices]
如果你几乎要读取所有页面(也就是说你的索引是随机的,频率是1/1000或更高),这样做可能会更快。另一方面,如果你的索引文件很大,而你只想挑几个索引,这样就不太快了。
还有一种可能性——可能是最快的——就是使用Python的mmap
模块。这样文件就会被映射到内存中,只有真正需要的页面会被访问。
应该像这样:
import mmap
with open("my_index", "rb") as f:
memory_map = mmap.mmap(mmap.mmap(f.fileno(), 0)
for i in indices:
# the index at position i:
idx_value = struct.unpack('I', memory_map[4*i:4*i+4])
(注意,我实际上没有测试这个,所以可能有打字错误。此外,我没有考虑字节序,所以请检查一下是否正确。)
幸运的是,这些可以通过使用numpy.memmap
结合起来。这样可以把你的数组保存在磁盘上,但仍然能使用numpy的索引方式。应该像这样简单:
import numpy as np
index_arr = np.memmap(filename, dtype='uint32', mode='rb')
return index_arr[indices]
我认为这应该是最简单和最快的选择。不过,如果“快”很重要,请务必测试和分析性能。
编辑:由于mmap
解决方案似乎越来越受欢迎,我想多说几句关于内存映射文件的内容。
什么是mmap?
内存映射文件并不是Python特有的,因为内存映射是POSIX标准中定义的一种方式。内存映射是一种将设备或文件当作内存中的区域来使用的方法。
文件内存映射是一种非常高效的随机访问固定长度数据文件的方法。它使用的技术与虚拟内存相同。读取和写入都是普通的内存操作。如果它们指向的内存位置不在物理内存中(会发生“页面错误”),所需的文件块(页面)会被读入内存。
随机文件访问的延迟主要是由于磁盘的物理旋转(SSD是另一个故事)。平均来说,你需要的块距离你大约有半个旋转的距离;对于典型的硬盘驱动器,这个延迟大约是5毫秒,加上任何数据处理的延迟。与这种延迟相比,使用Python而不是编译语言带来的开销几乎可以忽略不计。
如果文件是顺序读取的,操作系统通常会使用预读缓存,在你甚至还没意识到需要它之前就把文件缓存起来。对于随机访问的大文件,这一点完全没有帮助。内存映射提供了一种非常高效的方法,因为所有块在你需要的时候才会被加载,并且会保留在缓存中以供进一步使用。(原则上,这也可以通过fseek
实现,因为它可能在后台使用相同的技术。然而,没有保证,而且在调用过程中会有一些开销。)
mmap
也可以用来写文件。它非常灵活,因为一个内存映射文件可以被多个进程共享。在某些情况下,这可能非常有用和高效,mmap
也可以用于进程间通信。在这种情况下,通常不会为mmap
指定文件,而是创建一个没有文件的内存映射。
尽管mmap
非常有用且相对容易使用,但它并不是很知名。不过,它有一个重要的“陷阱”。文件大小必须保持不变。如果在mmap
期间发生变化,可能会出现奇怪的情况。