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 仍然具有优势,但更多的是由于其其他功能(例如元数据、组织等)而不是原始速度。
首先,为了消除混乱,访问h5py
dataset 返回一个行为与 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 是一种非常棒的容器格式。它为您管理数据提供了很大的灵活性,并且可以或多或少地从任何编程语言中使用。
总的来说,尝试一下,看看它是否适合您的用例。我想你可能会感到惊讶。