Есть ли преимущество в скорости анализа или использовании памяти для использования HDF5 для хранения больших массивов (вместо плоских двоичных файлов)?

я обрабатываю большие 3D-массивы,которые мне часто нужно нарезать различными способами для анализа данных. Типичный " куб " может быть ~100GB (и, вероятно, станет больше в будущем)

похоже, что типичным рекомендуемым форматом файлов для больших наборов данных в python является использование HDF5 (h5py или pytables). Мой вопрос: есть ли какая-либо скорость или использование памяти для использования HDF5 для хранения и анализа этих кубов над их хранением в простых плоских двоичных файлах? Это HDF5 более подходит для табличных данных, в отличие от больших массивов, с которыми я работаю? Я вижу, что HDF5 может обеспечить хорошее сжатие, но меня больше интересует скорость обработки и борьбы с переполнением памяти.

Я часто хочу проанализировать только одно большое подмножество Куба. Одним из недостатков как pytables, так и h5py является то, что когда я беру срез массива, я всегда получаю массив numpy обратно, используя память. Однако, если я срежу numpy memmap плоского двоичного файла, Я могу получить представление, которое хранит данные на диске. Таким образом, кажется, что я могу легче анализировать определенные сектора моих данных, не переполняя мою память.

Я исследовал как pytables, так и h5py и до сих пор не видел пользы ни от того, ни от другого для моей цели.

1 ответов


преимущества HDF5: организация, гибкость, интероперабельность

некоторые из основных преимуществ HDF5 являются его иерархической структурой (аналогично папкам/файлам), необязательными произвольными метаданными, хранящимися с каждым элементом, и его гибкостью (например, сжатие). Такая организационная структура и хранилище метаданных могут показаться тривиальными, но на практике они очень полезны.

Другим преимуществом HDF является то, что наборы данных могут быть либо фиксированного размера или гибкий размер. Таким образом, легко добавлять данные в большой набор данных без необходимости создания новой копии.

кроме того, HDF5-это стандартизированный формат с библиотеками, доступными практически для любого языка, поэтому обмен данными на диске между, скажем, Matlab, Fortran, R, C и Python очень прост с HDF. (Честно говоря, это не слишком сложно с большим двоичным массивом, если вы знаете о порядке C vs. F И знаете форму, dtype и т. д. сохраненного матрица.)

преимущества HDF для большого массива: более быстрый ввод-вывод произвольного среза

так же, как TL / DR: для 3D-массива ~8GB чтение" полного " среза вдоль любой оси заняло ~20 секунд с фрагментированным набором данных HDF5 и 0,3 секунды (в лучшем случае) до через три часа (в худшем случае) для memmapped массива тех же данных.

помимо перечисленных выше вещей, есть еще одно большое преимущество для формата данных "chunked" * на диске, такого как HDF5: чтение произвольного среза (акцент на произвольном), как правило, будет намного быстрее, так как данные на диске в среднем более непрерывны.

*(HDF5 не обязательно должен быть разделенным форматом данных. Он поддерживает chunking, но не требует его. Фактически, по умолчанию для создания набора данных в h5py - это не кусок, если я правильно помню.)

в основном, ваша лучшая скорость чтения диска и ваша худшая скорость чтения диска для данного фрагмента вашего набора данных будут будьте достаточно близки с фрагментированным набором данных HDF (при условии, что вы выбрали разумный размер фрагмента или позволили библиотеке выбрать его для вас). С простым двоичным массивом лучший случай быстрее, но худший -много хуже.

одно предостережение, если у вас есть SSD, вы, вероятно, не заметите огромной разницы в скорости чтения/записи. С обычным жестким диском, однако, последовательные чтения намного быстрее, чем случайные чтения. (т. е. обычный жесткий диск имеет long seek времени.) плита HDF по-прежнему имеет преимущество на SSD, но это больше из-за его других функций (например, метаданных, организации и т. д.), чем из-за скорости raw.


во-первых, чтобы прояснить путаницу, доступ к h5py dataset возвращает объект, который ведет себя аналогично массиву numpy, но не загружает данные в память, пока он не будет разрезан. (Похоже на memmap, но не идентично.) Посмотрите на h5py введение для получения дополнительной информации.

нарезки набор данных загрузит подмножество данных в память, но, предположительно, вы хотите что-то с ним сделать, и в этот момент он вам все равно понадобится в памяти.

если вы хотите делать вычисления вне ядра, вы можете довольно легко для табличных данных с pandas или pytables. Это возможно с h5py (лучше для больших n-D массивов), но вам нужно опуститься до сенсорного нижнего уровня и обрабатывать итерацию самостоятельно.

тем не менее, будущее numpy-как из ядра расчеты-это Блейз. посмотреть если вы действительно хотите пойти по этому пути.


случай "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 читает, так как все значения разделены на диске. Ни один из них не прилегает!

это может показаться довольно незначительным, но по мере того, как мы добираемся до все больших и больших массивов, количество и размер seek операции быстро растут. Для большого массива (~10Gb) 3D, хранящегося таким образом и читать через memmap, чтение полного среза вдоль" худшей " оси может легко занять десятки минут, даже с современным оборудованием. В то же время срез вдоль лучшей оси может занять менее секунды. Для простоты я показываю только" полные " срезы вдоль одной оси, но то же самое происходит с произвольными срезами любого подмножества данных.

кстати, есть несколько форматов файлов, которые используют это и в основном хранят три копии огромный 3D-массивы на диске: один в C-порядке, один в F-порядке и один в промежуточном между ними. (Примером этого является формат D3D Geoprobe, хотя я не уверен, что он документирован где-либо.) Кого волнует, если конечный размер файла 4 ТБ, хранение дешево! Сумасшедшая вещь в этом заключается в том, что, поскольку основным вариантом использования является извлечение одного суб-среза в каждом направлении, чтения, которые вы хотите сделать, очень, очень быстрые. Это работает очень хорошо!


простой "chunked" case

предположим, мы храним 2x2x2 "куски" 3D-массива как непрерывные блоки на диске. Другими словами, что-то типа:

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 ищет vs 1 для массива memmapped.) Поскольку последовательные чтения очень быстры по сравнению с поиском, это значительно сокращает время, необходимое для чтения произвольного подмножества в память. Еще раз, этот эффект становится больше с большими массивами.

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 говорит h5py автоматически выбрать размер куска для нас. Если вы знаете больше о своем наиболее распространенном случае использования, вы можете оптимизировать размер/форму куска, указав кортеж формы (например,(2,2,2) в примере выше). Это позволяет сделать чтение вдоль определенной оси более эффективным и оптимизировать для чтения/записи определенного размера.


сравнение производительности ввода/вывода

просто чтобы подчеркнуть точка, давайте сравним чтение в срезах из фрагментированного набора данных HDF5 и большого (~8GB), упорядоченного Fortran 3D-массива, содержащего те же самые точные данные.

я очистить все кэши ОС между каждым запуском, поэтому мы видим" холодное " представление.

для каждого типа файлов мы проверим чтение в" полном "X-срезе вдоль первой оси и" полном " z-срезе вдоль последней оси. Для массива memmapped, упорядоченного Fortran, срез" x "является худшим случаем, а" z" slice-лучший вариант.

используемый код в сущности (включая создание ). Я не могу легко обмениваться данными, используемыми здесь, но вы можете имитировать его массивом нулей той же формы (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, это не так уж плохо. Большую часть времени

и если мы сравним это с временами массива memmapped (это Fortran-ordered:" 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" -далеко дольше, чем время, необходимое для загрузки всего массива 8GB в память и выбора нужного фрагмента! (Опять же, это массив, упорядоченный Fortran. Противоположное время среза x/z будет иметь место для C-упорядоченного массива.)

, если мы всегда хотим взять срез по наилучшему направлению, большой двоичный массив на диске очень хорош. (~0.3 сек!)

с массивом memmapped вы застряли с этим несоответствием ввода-вывода (или, возможно, анизотропия-лучший термин). Однако с помощью набора данных HDF можно выбрать размер chunksize таким образом, чтобы доступ был равен или оптимизирован для конкретного случая использования. Это дает вам гораздо больше гибкости.

в резюме

надеюсь, это поможет во всяком случае, проясните одну часть вашего вопроса. HDF5 имеет много других преимуществ перед "сырыми" memmaps, но у меня нет места, чтобы расширить их все здесь. Сжатие может ускорить некоторые вещи (данные, с которыми я работаю, не очень выигрывают от сжатия, поэтому я редко его использую), и кэширование на уровне ОС часто играет более хорошо с файлами HDF5, чем с "сырыми" memmaps. Кроме того, HDF5-это действительно фантастический формат контейнера. Это дает вам большую гибкость в управлении вашими данными и может быть использовано из более или менее любой язык программирования.

в целом, попробуйте и посмотрите, хорошо ли он работает для вашего случая использования. Думаю, ты удивишься.