【问题标题】:Fastest save and load options for a numpy arraynumpy 数组的最快保存和加载选项
【发布时间】:2015-07-31 12:33:30
【问题描述】:

我有一个脚本可以生成二维numpy 数组,其中dtype=float 和形状为(1e3, 1e6)。现在我正在使用np.savenp.load 对数组执行IO 操作。但是,每个数组的这些函数需要几秒钟。是否有更快的方法来保存和加载整个数组(即,不对它们的内容进行假设并减少它们)?只要数据完全保留,我愿意在保存之前将数组转换为另一种类型。

【问题讨论】:

    标签: python arrays performance numpy io


    【解决方案1】:

    对于非常大的数组,我听说过几种解决方案,它们大多是在 I/O 上懒惰:

    • NumPy.memmap,将大数组映射为二进制形式
      • 优点:
        • 除了 Numpy 没有其他依赖
        • ndarray 的透明替换(任何接受 ndarray 的类都接受 memmap
      • 缺点:
        • 阵列的块限制为 2.5G
        • 仍然受到 Numpy 吞吐量的限制
    • 对 HDF5 使用 Python 绑定,这是一种大数据就绪的文件格式,例如 PyTablesh5py

      • 优点:
        • 格式支持压缩、索引和其他超棒的功能
        • 显然是终极的 PB 大文件格式
      • 缺点:
        • 分层格式的学习曲线?
        • 必须定义您的性能需求(见下文)
    • Python's pickling 系统(退出比赛,提及 Pythonicity 而不是速度)

      • 优点:
        • 这是 Pythonic ! (哈哈)
        • 支持各种对象
      • 缺点:
        • 可能比其他慢(因为针对任何对象而不是数组)

    Numpy.memmap

    来自NumPy.memmap 的文档:

    为存储在磁盘上的二进制文件中的数组创建内存映射。

    内存映射文件用于访问磁盘上大文件的小段,无需将整个文件读入内存

    memmap 对象可以在任何接受 ndarray 的地方使用。给定任何 memmap fpisinstance(fp, numpy.ndarray) 返回 True。


    HDF5 阵列

    来自h5py doc

    让您可以存储大量数字数据,并轻松地从 NumPy 操作这些数据。例如,您可以分割成存储在磁盘上的数 TB 数据集,就好像它们是真正的 NumPy 数组一样。数以千计的数据集可以存储在一个文件中,根据需要进行分类和标记。

    该格式支持以各种方式压缩数据(为相同的 I/O 读取加载更多位),但这意味着数据变得不太容易单独查询,但在您的情况下(纯粹加载/转储数组)它可能高效

    【讨论】:

    • 你做了分析吗? h5py 怎么样?我遇到了一些麻烦,当在同一个文件中有数千个数据集时会变得相当慢......
    • 我听说 hdf5 不支持线程/处理/celery,你如何解决这个问题
    • @PirateApp Threading example from h5py 以其他方式显示?如果您需要其他具体帮助,请打开一个单独的问题
    • 根据我使用deepdish 的经验,部分加载相当慢且效率低下。最终将我的整个数据集加载到内存中。
    【解决方案2】:

    我比较了一些使用perfplot(我的项目之一)的方法。结果如下:

    写作

    对于大型数组,所有方法的速度都差不多。文件大小也相等,这是意料之中的,因为输入数组是随机双精度数,因此几乎不可压缩。

    重现情节的代码:

    import perfplot
    import pickle
    import numpy
    import h5py
    import tables
    import zarr
    
    
    def npy_write(data):
        numpy.save("npy.npy", data)
    
    
    def hdf5_write(data):
        f = h5py.File("hdf5.h5", "w")
        f.create_dataset("data", data=data)
    
    
    def pickle_write(data):
        with open("test.pkl", "wb") as f:
            pickle.dump(data, f)
    
    
    def pytables_write(data):
        f = tables.open_file("pytables.h5", mode="w")
        gcolumns = f.create_group(f.root, "columns", "data")
        f.create_array(gcolumns, "data", data, "data")
        f.close()
    
    
    def zarr_write(data):
        zarr.save("out.zarr", data)
    
    
    perfplot.save(
        "write.png",
        setup=numpy.random.rand,
        kernels=[npy_write, hdf5_write, pickle_write, pytables_write, zarr_write],
        n_range=[2 ** k for k in range(28)],
        xlabel="len(data)",
        equality_check=None,
    )
    

    阅读

    pickles、pytables 和 hdf5 的速度大致相同;对于大型数组,pickles 和 zarr 速度较慢。

    重现情节的代码:

    import perfplot
    import pickle
    import numpy
    import h5py
    import tables
    import zarr
    
    
    def setup(n):
        data = numpy.random.rand(n)
        # write all files
        #
        numpy.save("out.npy", data)
        #
        f = h5py.File("out.h5", "w")
        f.create_dataset("data", data=data)
        f.close()
        #
        with open("test.pkl", "wb") as f:
            pickle.dump(data, f)
        #
        f = tables.open_file("pytables.h5", mode="w")
        gcolumns = f.create_group(f.root, "columns", "data")
        f.create_array(gcolumns, "data", data, "data")
        f.close()
        #
        zarr.save("out.zip", data)
    
    
    def npy_read(data):
        return numpy.load("out.npy")
    
    
    def hdf5_read(data):
        f = h5py.File("out.h5", "r")
        out = f["data"][()]
        f.close()
        return out
    
    
    def pickle_read(data):
        with open("test.pkl", "rb") as f:
            out = pickle.load(f)
        return out
    
    
    def pytables_read(data):
        f = tables.open_file("pytables.h5", mode="r")
        out = f.root.columns.data[()]
        f.close()
        return out
    
    
    def zarr_read(data):
        return zarr.load("out.zip")
    
    
    b = perfplot.bench(
        setup=setup,
        kernels=[
            npy_read,
            hdf5_read,
            pickle_read,
            pytables_read,
            zarr_read,
        ],
        n_range=[2 ** k for k in range(27)],
        xlabel="len(data)",
    )
    b.save("out2.png")
    b.show()
    

    【讨论】:

    • 太棒了,你能更新python 3.8的数字吗,有什么变化吗?
    【解决方案3】:

    这是与 PyTables 的比较。

    由于内存限制,我无法到达 (int(1e3), int(1e6)。 因此,我使用了一个较小的数组:

    data = np.random.random((int(1e3), int(1e5)))
    

    NumPy save:

    %timeit np.save('array.npy', data)
    1 loops, best of 3: 4.26 s per loop
    

    NumPy load:

    %timeit data2 = np.load('array.npy')
    1 loops, best of 3: 3.43 s per loop
    

    PyTables 写作:

    %%timeit
    with tables.open_file('array.tbl', 'w') as h5_file:
        h5_file.create_array('/', 'data', data)
    
    1 loops, best of 3: 4.16 s per loop
    

    PyTables 读取:

     %%timeit
     with tables.open_file('array.tbl', 'r') as h5_file:
          data2 = h5_file.root.data.read()
    
     1 loops, best of 3: 3.51 s per loop
    

    数字非常相似。所以在这里 PyTables 并没有真正的收获。 但我们已经非常接近我的 SSD 的最大写入和读取速率了。

    写作:

    Maximum write speed: 241.6 MB/s
    PyTables write speed: 183.4 MB/s
    

    阅读:

    Maximum read speed: 250.2
    PyTables read speed: 217.4
    

    由于数据的随机性,压缩并没有真正的帮助:

    %%timeit
    FILTERS = tables.Filters(complib='blosc', complevel=5)
    with tables.open_file('array.tbl', mode='w', filters=FILTERS) as h5_file:
        h5_file.create_carray('/', 'data', obj=data)
    1 loops, best of 3: 4.08 s per loop
    

    压缩数据的读取变得有点慢:

    %%timeit
    with tables.open_file('array.tbl', 'r') as h5_file:
        data2 = h5_file.root.data.read()
    
    1 loops, best of 3: 4.01 s per loop
    

    这与常规数据不同:

     reg_data = np.ones((int(1e3), int(1e5)))
    

    写作速度明显更快:

    %%timeit
    FILTERS = tables.Filters(complib='blosc', complevel=5)
    with tables.open_file('array.tbl', mode='w', filters=FILTERS) as h5_file:
        h5_file.create_carray('/', 'reg_data', obj=reg_data)
    

    1 次循环,3 次取胜:每个循环 849 毫秒

    阅读也是如此:

    %%timeit
    with tables.open_file('array.tbl', 'r') as h5_file:
        reg_data2 = h5_file.root.reg_data.read()
    
    1 loops, best of 3: 1.7 s per loop
    

    结论:您的数据越规则,使用 PyTables 获得的速度就越快。

    【讨论】:

      【解决方案4】:

      根据我的经验,到目前为止,在硬盘和内存之间传输数据时,np.save()&np.load() 是最快的解决方案。 在我意识到这个结论之前,我非常依赖数据库和 HDFS 系统上的数据加载。 我的测试表明: 数据库数据加载(从硬盘到内存)带宽可能在 50 MBps(字节/秒)左右,但 np.load() 带宽几乎与我的硬盘最大带宽相同:2GBps(字节/秒)。两种测试环境都使用最简单的数据结构。

      而且我认为用几秒钟来加载一个形状为 (1e3, 1e6) 的数组不是问题。例如。 你的数组形状是(1000, 1000000),它的数据类型是float128,那么纯数据大小是(128/8)*1000*1,000,000=16,000,000,000=16GBytes 如果需要 4 秒, 那么你的数据加载带宽是 16GBytes/4Seconds = 4GBps。 SATA3最大带宽600MBps=0.6GBps,你的数据加载带宽已经是它的6倍了,你的数据加载性能几乎可以和DDR's maximum bandwidth媲美,你还想要什么?

      所以我最后的结论是:

      不要使用python的Pickle,不要使用任何数据库,不要使用任何大数据系统将数据存储到硬盘中,如果可以使用np.save()和np.load( )。这两个函数是迄今为止在硬盘和内存之间传输数据最快的解决方案。

      我还测试了HDF5,发现它比 np.load() 和 np.save() 慢得多,所以如果你有足够的 DDR,请使用 np.save()&np.load()您的平台中的内存。

      【讨论】:

      • 如果您使用 HDF5 无法达到存储设备的最大带宽,您通常会出错。而且有很多事情可能会出错。 (块缓存,块形状,花式索引,...)
      • 试试这个stackoverflow.com/a/48997927/4045774 有和没有压缩(压缩限制在大约 500-800 MB/s。对于可压缩的数据,你可以在 HDD 上使用 HDF 5 获得更多的吞吐量或甚至是 SATA3 SSD。但主要优势是以顺序 IO 速度沿任意轴读取或写入阵列的一部分。如果 IO 速度真的很重要,那么阵列也很可能比 RAM 大...
      • @ClockZHONG,感谢您的帖子,DataFrames 怎么样?
      • 如果你想随机访问磁盘上的数组值怎么办?我假设您必须使用 HDF5 来处理该用例?
      • @Duane 不,这是不可能的,如果你想从一个非常大的数字中随机访问一小部分数据,我们唯一的选择是数据库、HDF5 或其他可以支持我们随机访问的机制访问硬盘。我建议只有在我们有足够的DDR内存空间并且我们的数据不是很大时才使用np.load(),至少我们的数据可以放入我们的内存空间。
      【解决方案5】:

      我创建了一个基准测试工具,并使用 python 3.9 生成了各种加载/保存方法的基准测试。我在快速 NVMe 上运行它(传输速率 >6GB/s,因此这里的测量不受磁盘 I/O 限制)。测试的 numpy 数组的大小从很小到 16GB 不等。结果可见here。 该工具的 github repo 是here

      结果有所不同,并受数组大小的影响;有些方法会执行数据压缩,因此需要权衡取舍。这是 I/O 速率的一个概念(更多结果通过上面的链接):

      传奇(用于扑救): np:np.save(),npz:np.savez(),npzc:np.savez_compressed(),hdf5:h5py.File().create_dataset(),pickle:pickle.dump(),zarr_zip:zarr.save_array() w/.zip扩展,zarr_zip:@98765432 987654333@ 扩展,pytables:tables.open_file().create_array()

      【讨论】:

        【解决方案6】:

        我很惊讶看到 torch.load 和 torch.save 根据这里的基准被认为是最佳或接近最佳的,但我发现它应该做的事情相当慢。所以我试了一下,想出了一个更快的替代方案:fastnumpyio

        运行 3x64x64 浮点数组的 100000 次保存/加载迭代(计算机视觉中的常见场景)我在 numpy.save 和 numpy.load 上实现了以下加速(我认为 numpy.load 非常慢,因为它必须解析文本数据第一个?):

        Windows 11、Python 3.9.5、Numpy 1.22.0、英特尔酷睿 i7-9750H:

        numpy.save: 0:00:01.656569
        fast_numpy_save: 0:00:00.398236
        numpy.load: 0:00:16.281941
        fast_numpy_load: 0:00:00.308100
        

        Ubuntu 20.04、Python 3.9.7、Numpy 1.21.4、英特尔酷睿 i7-9750H:

        numpy.save: 0:00:01.887152
        fast_numpy_save: 0:00:00.745052
        numpy.load: 0:00:16.368871
        fast_numpy_load: 0:00:00.381135
        

        macOS 12.0.1、Python 3.9.5、Numpy 1.21.2、Apple M1:

        numpy.save: 0:00:01.268598
        fast_numpy_save: 0:00:00.449448
        numpy.load: 0:00:11.303569
        fast_numpy_load: 0:00:00.318216
        

        对于较大的数组 (3x512x512),fastnumpyio 的保存速度仍然稍快,加载速度快 2 倍。

        【讨论】:

          猜你喜欢
          • 2012-09-24
          • 2013-06-16
          • 2018-09-03
          • 2010-12-14
          • 2013-07-12
          • 1970-01-01
          • 2019-02-13
          • 2019-10-03
          • 1970-01-01
          相关资源
          最近更新 更多