使用 HDF5 进行大型数组存储(而不是平面二进制文件)是否具有分析速度或内存使用优势?

2024-02-21

我正在处理大型 3D 数组,我经常需要以各种方式对其进行切片以进行各种数据分析。典型的“立方体”可以约为 100GB(并且将来可能会变得更大)

看来Python中大型数据集的典型推荐文件格式是使用HDF5(h5py或pytables)。我的问题是:使用 HDF5 存储和分析这些多维数据集与将它们存储在简单的平面二进制文件中相比,在速度或内存使用方面是否有任何优势?与我正在使用的大型数组相比,HDF5 是否更适合表格数据?我发现 HDF5 可以提供很好的压缩,但我更感兴趣的是处理速度和处理内存溢出。

我经常只想分析多维数据集的一大子集。 pytables 和 h5py 的一个缺点似乎是,当我获取数组的一部分时,我总是得到一个 numpy 数组,从而耗尽内存。但是,如果我对平面二进制文件的 numpy memmap 进行切片,我可以获得一个视图,它将数据保留在磁盘上。因此,我似乎可以更轻松地分析数据的特定部分,而不会超出我的内存。

我已经探索了 pytables 和 h5py,但到目前为止还没有看到它们对我的目的有什么好处。


HDF5 优点:组织、灵活性、互操作性

HDF5 的一些主要优点是其分层结构(类似于文件夹/文件)、与每个项目一起存储的可选任意元数据及其灵活性(例如压缩)。这种组织结构和元数据存储可能听起来微不足道,但在实践中非常有用。

HDF 的另一个优点是数据集可以是固定大小的or大小灵活。因此,可以轻松地将数据附加到大型数据集,而无需创建整个新副本。

此外,HDF5 是一种标准化格式,具有适用于几乎所有语言的库,因此使用 HDF 在 Matlab、Fortran、R、C 和 Python 之间共享磁盘数据非常容易。 (公平地说,只要您了解 C 与 F 的顺序并知道存储数组的形状、数据类型等,使用大型二进制数组也不是太难。)

HDF 对于大型阵列的优势:任意切片的 I/O 速度更快

正如 TL/DR 所示:对于约 8GB 的​​ 3D 阵列,使用分块 HDF5 数据集读取沿任何轴的“完整”切片大约需要 20 秒,而读取分块 HDF5 数据集则需要 0.3 秒(最佳情况)。三个多小时(最坏的情况)对于相同数据的内存映射数组。

除了上面列出的内容之外,“分块”* 磁盘数据格式(例如 HDF5)还有另一个很大的优点:读取任意切片(重点是任意)通常会更快,因为磁盘数据在磁盘上更连续。平均的。

*(HDF5 不一定是分块数据格式。它支持分块,但不要求它。事实上,在中创建数据集的默认设置h5py如果我没记错的话,不是分块。)

基本上,对于给定的数据集切片,您的最佳情况磁盘读取速度和最坏情况磁盘读取速度将与分块 HDF 数据集相当接近(假设您选择了合理的块大小或让库为您选择一个)。对于简单的二进制数组,最好的情况更快,但最坏的情况是much worse.

需要注意的是,如果您有 SSD,您可能不会注意到读/写速度的巨大差异。然而,对于常规硬盘驱动器,顺序读取比随机读取快得多。 (即普通硬盘驱动器具有很长的seek时间。)HDF 相对于 SSD 仍然具有优势,但更多的是由于其其他功能(例如元数据、组织等)而不是原始速度。


首先,为了消除混乱,访问h5pydataset 返回一个行为与 numpy 数组非常相似的对象,但在切片之前不会将数据加载到内存中。 (类似于 memmap,但不完全相同。)看看h5py介绍 http://docs.h5py.org/en/latest/high/dataset.html#dataset了解更多信息。

对数据集进行切片会将数据的子集加载到内存中,但大概您想用它做一些事情,此时您无论如何都需要将它存储在内存中。

如果您确实想要进行核外计算,您可以相当轻松地使用以下命令来获取表格数据pandas or pytables。这是可能的h5py(对于大型 A-N-D 数组更好),但是您需要下降到较低的级别并自己处理迭代。

然而,类似 numpy 的核外计算的未来是 Blaze。看看它 http://blaze.readthedocs.io/en/latest/index.html#如果你真的想走那条路。


“未分块”的案例

首先,考虑一个写入磁盘的 3D C 有序数组(我将通过调用来模拟它arr.ravel()并打印结果,使事情更加明显):

In [1]: import numpy as np

In [2]: arr = np.arange(4*6*6).reshape(4,6,6)

In [3]: arr
Out[3]:
array([[[  0,   1,   2,   3,   4,   5],
        [  6,   7,   8,   9,  10,  11],
        [ 12,  13,  14,  15,  16,  17],
        [ 18,  19,  20,  21,  22,  23],
        [ 24,  25,  26,  27,  28,  29],
        [ 30,  31,  32,  33,  34,  35]],

       [[ 36,  37,  38,  39,  40,  41],
        [ 42,  43,  44,  45,  46,  47],
        [ 48,  49,  50,  51,  52,  53],
        [ 54,  55,  56,  57,  58,  59],
        [ 60,  61,  62,  63,  64,  65],
        [ 66,  67,  68,  69,  70,  71]],

       [[ 72,  73,  74,  75,  76,  77],
        [ 78,  79,  80,  81,  82,  83],
        [ 84,  85,  86,  87,  88,  89],
        [ 90,  91,  92,  93,  94,  95],
        [ 96,  97,  98,  99, 100, 101],
        [102, 103, 104, 105, 106, 107]],

       [[108, 109, 110, 111, 112, 113],
        [114, 115, 116, 117, 118, 119],
        [120, 121, 122, 123, 124, 125],
        [126, 127, 128, 129, 130, 131],
        [132, 133, 134, 135, 136, 137],
        [138, 139, 140, 141, 142, 143]]])

这些值将按顺序存储在磁盘上,如下面第 4 行所示。 (我们暂时忽略文件系统细节和碎片。)

In [4]: arr.ravel(order='C')
Out[4]:
array([  0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,
        13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25,
        26,  27,  28,  29,  30,  31,  32,  33,  34,  35,  36,  37,  38,
        39,  40,  41,  42,  43,  44,  45,  46,  47,  48,  49,  50,  51,
        52,  53,  54,  55,  56,  57,  58,  59,  60,  61,  62,  63,  64,
        65,  66,  67,  68,  69,  70,  71,  72,  73,  74,  75,  76,  77,
        78,  79,  80,  81,  82,  83,  84,  85,  86,  87,  88,  89,  90,
        91,  92,  93,  94,  95,  96,  97,  98,  99, 100, 101, 102, 103,
       104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
       117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
       130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143])

在最好的情况下,让我们沿着第一个轴进行切片。请注意,这些只是数组的前 36 个值。这将是一个very快读! (一寻一读)

In [5]: arr[0,:,:]
Out[5]:
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17],
       [18, 19, 20, 21, 22, 23],
       [24, 25, 26, 27, 28, 29],
       [30, 31, 32, 33, 34, 35]])

同样,沿第一个轴的下一个切片将只是接下来的 36 个值。要沿着该轴读取完整的切片,我们只需要一个seek手术。如果我们要读取的只是沿该轴的各个切片,那么这就是完美的文件结构。

但是,让我们考虑最坏的情况:沿最后一个轴的切片。

In [6]: arr[:,:,0]
Out[6]:
array([[  0,   6,  12,  18,  24,  30],
       [ 36,  42,  48,  54,  60,  66],
       [ 72,  78,  84,  90,  96, 102],
       [108, 114, 120, 126, 132, 138]])

要读取此切片,我们需要 36 次查找和 36 次读取,因为所有值在磁盘上都是分开的。它们都不相邻!

这可能看起来很小,但是当我们使用越来越大的数组时,数组的数量和大小seek业务增长迅速。对于以这种方式存储并通过读取的大型 (~10Gb) 3D 数组memmap,即使使用现代硬件,沿着“最差”轴读取完整切片也很容易花费数十分钟。同时,沿最佳轴进行切片只需不到一秒的时间。为简单起见,我仅显示沿单个轴的“完整”切片,但对于数据的任何子集的任意切片也会发生完全相同的情况。

顺便说一句,有几种文件格式利用了这一点,基本上存储了三个副本huge磁盘上的 3D 阵列:一个是 C 阶,一个是 F 阶,一个是两者之间的中间阵列。 (这方面的一个例子是 Geoprobe 的 D3D 格式,尽管我不确定它是否在任何地方都有记录。)谁在乎最终文件大小是否为 4TB,存储很便宜!令人疯狂的是,因为主要用例是在每个方向提取单个子切片,所以您想要进行的读取非常非常快。效果非常好!


简单的“分块”案例

假设我们将 3D 数组的 2x2x2“块”存储为磁盘上的连续块。换句话说,类似:

nx, ny, nz = arr.shape
slices = []
for i in range(0, nx, 2):
    for j in range(0, ny, 2):
        for k in range(0, nz, 2):
            slices.append((slice(i, i+2), slice(j, j+2), slice(k, k+2)))

chunked = np.hstack([arr[chunk].ravel() for chunk in slices])

所以磁盘上的数据看起来像chunked:

array([  0,   1,   6,   7,  36,  37,  42,  43,   2,   3,   8,   9,  38,
        39,  44,  45,   4,   5,  10,  11,  40,  41,  46,  47,  12,  13,
        18,  19,  48,  49,  54,  55,  14,  15,  20,  21,  50,  51,  56,
        57,  16,  17,  22,  23,  52,  53,  58,  59,  24,  25,  30,  31,
        60,  61,  66,  67,  26,  27,  32,  33,  62,  63,  68,  69,  28,
        29,  34,  35,  64,  65,  70,  71,  72,  73,  78,  79, 108, 109,
       114, 115,  74,  75,  80,  81, 110, 111, 116, 117,  76,  77,  82,
        83, 112, 113, 118, 119,  84,  85,  90,  91, 120, 121, 126, 127,
        86,  87,  92,  93, 122, 123, 128, 129,  88,  89,  94,  95, 124,
       125, 130, 131,  96,  97, 102, 103, 132, 133, 138, 139,  98,  99,
       104, 105, 134, 135, 140, 141, 100, 101, 106, 107, 136, 137, 142, 143])

只是为了表明它们是 2x2x2 的块arr,请注意,这些是前 8 个值chunked:

In [9]: arr[:2, :2, :2]
Out[9]:
array([[[ 0,  1],
        [ 6,  7]],

       [[36, 37],
        [42, 43]]])

要读取沿轴的任何切片,我们需要读取 6 或 9 个连续块(我们需要的数据量的两倍),然后只保留我们想要的部分。最坏情况下的最大搜索次数为 9 次,而非分块版本的最大搜索次数为 36 次。 (但最好的情况仍然是 6 次查找,而内存映射数组为 1 次。)由于顺序读取与查找相比非常快,因此这显着减少了将任意子集读入内存所需的时间。同样,随着阵列的增大,这种效应也会变得更大。

HDF5 在这方面更进了几步。这些块不必连续存储,并且它们由 B 树索引。此外,它们在磁盘上的大小不必相同,因此可以对每个块应用压缩。


分块数组h5py

默认情况下,h5py不会在磁盘上创建分块 HDF 文件(我认为pytables相比之下,确实如此)。如果您指定chunks=True但是,在创建数据集时,您将在磁盘上获得一个分块数组。

作为一个快速、最小的例子:

import numpy as np
import h5py

data = np.random.random((100, 100, 100))

with h5py.File('test.hdf', 'w') as outfile:
    dset = outfile.create_dataset('a_descriptive_name', data=data, chunks=True)
    dset.attrs['some key'] = 'Did you want some metadata?'

注意chunks=True tells h5py自动为我们选择块大小。如果您更了解最常见的用例,则可以通过指定形状元组(例如(2,2,2)在上面的简单示例中)。这使您可以更有效地沿特定轴进行读取或优化特定大小的读/写。


I/O性能比较

为了强调这一点,让我们比较从分块 HDF5 数据集和包含相同精确数据的大型 (~8GB) Fortran 排序 3D 数组中读取切片的情况。

I've 清除所有操作系统缓存 https://unix.stackexchange.com/a/87909/4100每次运行之间,所以我们看到的是“冷”性能。

对于每种文件类型,我们将测试沿第一个轴的“完整”x 切片和沿最后一个轴的“完整”z 切片的读取。对于 Fortran 有序内存映射数组,“x”切片是最坏情况,“z”切片是最好情况。

使用的代码是要点 https://gist.github.com/joferkington/77edf001b8c699a14e06(包括创建hdf文件)。我无法轻松共享此处使用的数据,但您可以通过相同形状的零数组来模拟它(621, 4991, 2600)并输入np.uint8.

The chunked_hdf.py看起来像这样:

import sys
import h5py

def main():
    data = read()

    if sys.argv[1] == 'x':
        x_slice(data)
    elif sys.argv[1] == 'z':
        z_slice(data)

def read():
    f = h5py.File('/tmp/test.hdf5', 'r')
    return f['seismic_volume']

def z_slice(data):
    return data[:,:,0]

def x_slice(data):
    return data[0,:,:]

main()

memmapped_array.py类似,但更复杂,以确保切片实际加载到内存中(默认情况下,另一个memmapped将返回数组,这不会是同类比较)。

import numpy as np
import sys

def main():
    data = read()

    if sys.argv[1] == 'x':
        x_slice(data)
    elif sys.argv[1] == 'z':
        z_slice(data)

def read():
    big_binary_filename = '/data/nankai/data/Volumes/kumdep01_flipY.3dv.vol'
    shape = 621, 4991, 2600
    header_len = 3072

    data = np.memmap(filename=big_binary_filename, mode='r', offset=header_len,
                     order='F', shape=shape, dtype=np.uint8)
    return data

def z_slice(data):
    dat = np.empty(data.shape[:2], dtype=data.dtype)
    dat[:] = data[:,:,0]
    return dat

def x_slice(data):
    dat = np.empty(data.shape[1:], dtype=data.dtype)
    dat[:] = data[0,:,:]
    return dat

main()

我们先看一下HDF的性能:

jofer at cornbread in ~ 
$ sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python chunked_hdf.py z
python chunked_hdf.py z  0.64s user 0.28s system 3% cpu 23.800 total

jofer at cornbread in ~ 
$ sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python chunked_hdf.py x
python chunked_hdf.py x  0.12s user 0.30s system 1% cpu 21.856 total

“完整”x 切片和“完整”z 切片花费的时间大约相同(约 20 秒)。考虑到这是一个 8GB 阵列,这还算不错。大多数时候

如果我们将其与内存映射数组时间进行比较(它是 Fortran 排序的:“z 切片”是最好的情况,“x 切片”是最坏的情况。):

jofer at cornbread in ~ 
$ sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python memmapped_array.py z
python memmapped_array.py z  0.07s user 0.04s system 28% cpu 0.385 total

jofer at cornbread in ~ 
$ sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python memmapped_array.py x
python memmapped_array.py x  2.46s user 37.24s system 0% cpu 3:35:26.85 total

是的,你没有看错。一个切片方向 0.3 秒,~3.5hours对于另一个。

在“x”方向上切片的时间是far比将整个 8GB 数组加载到内存并选择我们想要的切片所需的时间还要长! (同样,这是一个 Fortran 有序数组。相反的 x/z 切片时序将是 C 有序数组的情况。)

然而,如果我们总是想沿着最好情况的方向进行切片,那么磁盘上的大二进制数组非常好。 (~0.3 秒!)

使用内存映射数组时,您会陷入这种 I/O 差异(或者也许各向异性是一个更好的术语)。但是,对于分块 HDF 数据集,您可以选择块大小,以便访问相等或针对特定用例进行优化。它为您提供了更多的灵活性。

总之

无论如何,希望这有助于澄清您的问题的一部分。与“原始”内存映射相比,HDF5 具有许多其他优点,但我没有空间在这里详细介绍所有这些优点。压缩可以加快某些事情的速度(我处理的数据并没有从压缩中受益太多,所以我很少使用它),并且操作系统级缓存通常在 HDF5 文件中比在“原始”memmap 中表现得更好。除此之外,HDF5 是一种非常棒的容器格式。它为您管理数据提供了很大的灵活性,并且可以或多或少地从任何编程语言中使用。

总的来说,尝试一下,看看它是否适合您的用例。我想你可能会感到惊讶。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 HDF5 进行大型数组存储(而不是平面二进制文件)是否具有分析速度或内存使用优势? 的相关文章

随机推荐

  • 如何从标准输入中读取一行,并将其余行传递给子进程?

    If you readline from sys stdin 将其余部分传递给子进程似乎不起作用 import subprocess import sys header sys stdin buffer readline print hea
  • 如何在servlet中获取客户端的远程地址?

    有什么办法可以获取到服务器的客户端的原始IP地址吗 我可以用request getRemoteAddr 但我似乎总是获得代理或网络服务器的IP 我想知道客户端用于连接到我的 IP 地址 无论如何 我能得到它吗 尝试这个 public sta
  • Visual Studio 2012 是否利用所有可用的 CPU 内核?

    我计划在 Visual Studio 2012 和 Windows 7 64 位下构建一台新的非常快的开发计算机 我正在购买所有快速组件 例如 SSD 和 16G RAM 我想知道是否视觉工作室2012旨在利用所有可用的 CPU 内核 我正
  • 每次出现错误时使用 prometheus 创建警报

    我是普罗米修斯和警报系统的新手 我开发了一个微服务并添加了指标代码 以便在出现错误时获取增量总数 现在我正在尝试创建一个警报 以便每当错误增加时 它应该标记出来并发送邮件 但我无法针对这种情况形成正确的查询 我使用了诸如 error tot
  • CoreMotion 陀螺仪苹果手表

    我正在尝试访问苹果手表的陀螺仪 据我所知 它可以在 watchos 3 中使用 不幸的是我无法让它工作 它不断返回 陀螺仪不可用 因此 MotionManager isGyroAvailable 始终为 false 这是我的代码 任何帮助
  • Kotlin 本地函数必须在使用前声明

    在这个简单的代码示例中 fun testLocalFunctions aLocalFun compiler error unresolved reference at aLocalFun fun aLocalFun aLocalFun no
  • 如何保持领域数据与输入的顺序相同

    我有一个应用程序 需要将数据按照输入的顺序保存 数据被输入到 List 属性中 一切都很顺利 直到我必须删除一个项目 当删除发生时 列表中的最后一项将取代被删除的一项 UITableView 显示正确的项目数 但与领域列表不同步 一个例子是
  • R中的顺序混淆矩阵

    我根据 3 个类别的观察结果及其预测创建了一个混淆矩阵 classes c Underweight Normal Overweight 当我计算混淆矩阵时 它会按字母顺序组织表中的类 这是我的代码 Confusion matrix Obse
  • 为什么此解释中没有使用密钥?

    我期望这个查询使用密钥 mysql gt DESCRIBE TABLE Foo Field Type Null Key Default Extra id bigint 20 NO PRI NULL auto increment name v
  • 如何正确编写 Swift UI Toggle 的 UI 测试

    有谁知道如何正确编写 Toggle 的 UI 测试 即使在一个全新的项目中 整个 UI 中只有一个切换而没有其他内容 我仍然会收到此类错误 Failed to get matching snapshot Multiple matching
  • OpenCV detectorMultiScale() 参数的推荐值

    推荐的参数是什么CascadeClassifier detectMultiScale http docs opencv org modules objdetect doc cascade classification html cascad
  • 如何在 Node.js 中将 JPG 图像转换为 WEBP 格式?

    我正在尝试使用react Js上传图像并使用multer中间件将该图像保存在node Js中 这是完美的 但现在我想使用 webp converter 将该图像转换为 WEBP 格式 反之亦然 但我收到此错误 Error Command f
  • JavaScript 数字到单词

    例如 我正在尝试将数字转换为英语单词1234会成为 一千二百三十四 我的策略是这样的 将数字分成三位并将它们放入数组 finlOutPut 从右到左 转换每个组 每个单元格中finlOutPut数组 的三个数字到一个单词 这就是triCon
  • 使用 PHP、活动目录以及 IE/Firefox 对 ldap 进行身份验证

    下面的代码根据 ldap 检查用户的凭据
  • Tensorflow 不会在 CUDA 支持下构建

    我已经尝试从源代码构建张量流 如安装指南 https www tensorflow org install install sources 我已经成功地使用仅 cpu 支持和 SIMD 指令集构建了它 但在尝试使用 CUDA 支持构建时遇到
  • 在 ARM/Thumb (IOS) 上解码 BLX 指令

    我已经阅读了大部分 ARM 文档 但在解码 BLX 指令时仍然遇到问题 下面是两个例子 text 0000347C 02 F0 B2 ED BLX objc msgSend text 0000469C 01 F0 A2 EC BLX obj
  • 允许机器人访问 Telegram Group 消息[重复]

    这个问题在这里已经有答案了 比之前更进了一步如何将机器人添加到 Telegram 群组 https stackoverflow com questions 37338101 如何以管理员身份将机器人添加到 Telegram 群组 我已将我的
  • “strcpy”和“strcpy_s”之间的区别?

    当我尝试使用时strcpy复制字符串给了我一个编译错误 error C4996 strcpy This function or variable may be unsafe Consider using strcpy s instead T
  • 将数组的元素向左旋转

    我正在尝试解决来自的 JavaScript 挑战杰希罗网 挑战是这样的 编写一个旋转函数来旋转数组的元素 全部 元素应向左移动一位 第 0 个元素 应该放在数组的末尾 旋转后的数组应该是 回 旋转 a b c 应该返回 b c a 我能想到
  • 使用 HDF5 进行大型数组存储(而不是平面二进制文件)是否具有分析速度或内存使用优势?

    我正在处理大型 3D 数组 我经常需要以各种方式对其进行切片以进行各种数据分析 典型的 立方体 可以约为 100GB 并且将来可能会变得更大 看来Python中大型数据集的典型推荐文件格式是使用HDF5 h5py或pytables 我的问题