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

我正在处理大型的3D数组,我经常需要用各种方法来进行各种数据分析。 一个典型的“立方体”可以是〜100GB(未来可能会变大)

似乎python中大数据集的典型推荐文件格式是使用HDF5(h5py或pytables)。 我的问题是:是否有任何速度或内存使用的好处,使用HDF5存储和分析这些立方体存储在简单的平面二进制文件? HDF5更适合表格数据,而不是像我正在使用的大型数组? 我看到HDF5可以提供很好的压缩,但是我更关心处理速度和处理内存溢出问题。

我经常只想分析立方体的一个大的子集。 pytables和h5py的一个缺点是,当我分割一个数组的时候,总是会返回一个数组,使用内存。 但是,如果我分割一个平坦的二进制文件的numpy memmap,我可以得到一个视图,它保持在磁盘上的数据。 所以,我似乎可以更容易地分析我的数据的特定部门,而不会超出我的记忆。

我已经探索了pytables和h5py,至今没有看到我的目的的好处。

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

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

HDF的另一个优点是数据集可以是固定大小的,也可以是灵活大小的。 因此,将数据附加到大型数据集很容易,而无需创build全新的副本。

另外,HDF5是一种标准化格式,几乎可以使用任何语言的库,因此在HDF之间共享磁盘数据,例如Matlab,Fortran,R,C和Python是非常容易的。 (公平地说,对于一个大的二进制数组来说也不算太难,只要你知道C和F的sorting并知道存储数组的形状,dtype等。)

大arrays的HDF优势:更快的任意片的I / O

就像TL / DR一样:对于一个〜8GB的3Darrays,沿着任何一个轴读取一个“完整”的片段需要花费大约20秒的时间,对于大块的HDF5数据集,0.3秒(最好的情况下), 超过三个小时一个相同数据的memmapped数组。

除了上面列出的内容之外,磁盘数据格式(如HDF5)还有一个很大的优势:读取任意片(强调任意)通常会更快,因为磁盘上的数据更接近于平均。

* (HDF5不一定是分块的数据格式,它支持分块,但不需要它。事实上,如果我记得正确的话,在h5py创build数据集的默认是h5py块的。

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

一个警告,如果你有一个固态硬盘,你可能不会注意到读/写速度的巨大差异。 然而,对于普通的硬盘驱动器来说,顺序读取比随机读取要快得多。 (即普通的硬盘驱动器需要很长的seek )。HDF在固态硬盘上仍然占有优势,但是由于原始速度的原因,HDF的其他function(例如元数据,组织等)更多。


首先,为了消除混淆,访问h5py数据集返回的对象的行为与numpy数组的行为相当类似,但不会将数据加载到内存中,直到它被切片。 (与memmap类似,但不完全相同。)有关更多信息,请参阅h5py介绍 。

切片数据集会将数据的一个子集加载到内存中,但是大概你想对它做些什么,在这一点上,无论如何你都需要它。

如果你想做一些核外计算,你可以很容易地用pandas或者pytables来表格化数据。 这是可能的h5py (更好的大NDarrays),但你需要下降到一个触摸较低的水平,并处理迭代自己。

然而,类似核心计算的未来是Blaze。 看看它,如果你真的想要走这条路。


“unchunked”的情况

首先,考虑写入磁盘的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个值。 这将是一个非常快的阅读! (一个寻求,一个阅读)

 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次读取,因为所有的值都在磁盘上分开。 没有一个是相邻的!

这可能看起来很小,但是当我们遇到越来越大的arrays时, seek操作的数量和大小迅速增长。 对于以这种方式存储的大数据(〜10Gb)3Darrays并通过memmap读取,即使使用现代硬件,沿着“最差”轴读取完整片也可能需要几十分钟的时间。 同时,沿着最佳轴的切片可能不到一秒钟。 为了简单起见,我只在单个轴上显示“完整”切片,但是对任何数据子集的任意切片都会发生完全相同的事情。

顺便说一下,有几种文件格式可以利用这一点,并基本上在磁盘上存储三个巨大的 3D数组副本:一个在C顺序,一个在F顺序,一个在两者之间的中间。 (一个例子是Geoprobe的D3D格式,但我不确定它在任何地方都有logging。)谁在乎最终的文件大小是4TB,存储是便宜! 关于这一点的疯狂之处在于,因为主要的用例是在每个方向上提取一个子片,所以你想要做的读取非常快。 它工作得很好!


简单的“块”情况

假设我们将3Darrays的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的区块,注意这些是chunked的前8个值:

 In [9]: arr[:2, :2, :2] Out[9]: array([[[ 0, 1], [ 6, 7]], [[36, 37], [42, 43]]]) 

要沿轴读取任何一个片,我们可以读取6或9个连续的块(数据量是我们需要的两倍),然后只保留我们想要的部分。 这是最坏的情况,最多9个寻找,最多36个寻求非分块版本。 (但是,对于memmapped数组,最好的情况仍然是6个,而对于memmapped数组,由于顺序读取速度非常快,这大大减less了将任意子集读入内存所需的时间。 再一次,这个效应随着更大的arrays而变大。

HDF5将这一步走得更远。 块不必连续存储,它们被B树索引。 此外,它们不必在磁盘上具有相同的大小,因此可以将压缩应用于每个块。


h5py块数组

默认情况下, h5py不会在磁盘上创build分块的HDF文件(相反,我认为pytables可以)。 如果在创build数据集时指定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告诉h5py自动为我们select一个块大小。 如果您对最常见的用例有更多的了解,可以通过指定一个形状元组来优化块大小/形状(2,2,2)例如上面简单示例中的(2,2,2) )。 这使您可以使特定轴的读取效率更高,或针对特定大小的读取/写入进行优化。


I / O性能比较

为了强调这一点,让我们比较分块HDF5数据集和大型(〜8GB)Fortransorting3D数组中包含相同确切数据的片段的读数。

我已经清除了每次运行之间的所有操作系统caching ,所以我们看到了“冷”的performance。

对于每种文件types,我们将testing沿第一个轴的“全”x片和沿着最后一个轴的“全”z轴的读数。 对于Fortran有序的memmapped数组,“x”slice是最糟糕的情况,“z”slice是最好的情况。

使用的代码是主要的 (包括创buildhdf文件)。 我不能轻松地共享这里使用的数据,但是可以用相同形状的零( np.uint8 621, 4991, 2600)的数组来模拟它,然后键入np.uint8

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的arrays,这不是太糟糕。 大多数时候

如果我们将它与memmapped数组的时间进行比较(它是Fortran有序的:“z-slice”是最好的情况,“x-slice”是最坏的情况):

 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.5 小时

在“x”方向切片的时间比将整个8GBarrays加载到内存并select我们想要的切片的时间长得多! (再一次,这是一个Fortran有序的数组,相反的x / z切片时序就是C有序数组的情况)。

但是,如果我们总是希望沿着最好的方向进行分割,磁盘上的大二进制数组是非常好的。 (〜0.3秒!)

使用memmapped数组,您会陷入这种I / O差异(或者各向异性可能是一个更好的术语)。 但是,对于分块的HDF数据集,您可以select块大小,以使访问等于或针对特定用例进行优化。 它给你更多的灵活性。

综上所述

希望这有助于澄清你的问题的一部分,无论如何。 HDF5与“原始”的memmaps相比有许多其他优点,但是我没有足够的空间在这里扩展它们。 压缩可以加速某些事情(我使用的数据不会从压缩中受益太多,所以我很less使用它),OS级别的caching对HDF5文件的打击效果通常比“原始”的memmaps更好。 除此之外,HDF5是一个非常棒的容器格式。 它使您在pipe理数据方面拥有很大的灵活性,并且可以或多或less地使用任何编程语言。

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