Ответ 1
HDF5 Преимущества: Организация, гибкость, интероперабельность
Некоторые из основных преимуществ HDF5 - его иерархическая структура (аналогичная папкам/файлам), необязательные произвольные метаданные, хранящиеся в каждом элементе, и его гибкость (например, сжатие). Эта организационная структура и хранилище метаданных могут казаться тривиальными, но это очень полезно на практике.
Другим преимуществом HDF является то, что наборы данных могут быть как фиксированного, так и гибкого размера. Таким образом, легко добавлять данные в большой набор данных без необходимости создавать целую новую копию.
Кроме того, HDF5 является стандартизованным форматом с библиотеками, доступными практически для любого языка, поэтому совместное использование данных на диске между, скажем, Matlab, Fortran, R, C и Python очень просто с HDF. (Чтобы быть справедливым, это не слишком сложно с большим двоичным массивом, если вы знаете порядок C и F и знаете форму, dtype и т.д. Сохраненного массива.)
Преимущества HDF для большого массива: более быстрый ввод/вывод произвольного среза
Точно так же, как TL/DR:. Для 3D-массива размером ~ 8 ГБ чтение "полного" среза вдоль любой оси заняло ~ 20 секунд с набором данных HDF5 и 0,3 секунды (в лучшем случае ) до более чем трех часов (наихудший случай) для memmapped массива одних и тех же данных.
Помимо вышеперечисленных вещей, существует еще одно большое преимущество в формате "chunked" * on-disk data, таком как HDF5: чтение произвольного фрагмента (выделение на произвольное), как правило, будет намного быстрее, поскольку данные на диске более сопредельных в среднем.
*
(HDF5 не обязательно должен быть записанным форматом данных. Он поддерживает chunking, но не требует его. Фактически, значение по умолчанию для создания набора данных в h5py
не является фрагментом, если я правильно помните.)
В принципе, ваша максимальная скорость чтения на диске и скорость чтения диска с наименьшим регистром для данного фрагмента вашего набора данных будут достаточно близки к набору данных HDF (если вы выбрали разумный размер фрагмента или позвольте библиотеке выбрать один для вы). С простым двоичным массивом наилучший вариант быстрее, но худший вариант намного хуже.
Одно предупреждение, если у вас SSD, вы, вероятно, не заметите огромной разницы в скорости чтения/записи. Однако с помощью обычного жесткого диска последовательные чтения намного, намного быстрее, чем случайные чтения. (т.е. обычный жесткий диск имеет длительное время seek
). HDF по-прежнему имеет преимущество на SSD, но больше благодаря другим функциям (например, метаданные, организации и т.д.), чем из-за необработанной скорости.
Прежде всего, чтобы устранить путаницу, доступ к набору данных h5py
возвращает объект, который ведет себя аналогично массиву numpy, но не загружает данные в память, пока не нарезается. (Подобно memmap, но не идентичны.) Для получения дополнительной информации см. h5py
.
Нарезка набора данных будет загружать подмножество данных в память, но, предположительно, вы хотите что-то с ней сделать, и в этом случае вам все равно понадобится это в памяти.
Если вы хотите выполнять внекорпоративные вычисления, вы можете довольно легко использовать табличные данные с помощью pandas
или pytables
. Это возможно при h5py
(лучше для больших массивов N-D), но вам нужно опуститься на нижний уровень касания и самостоятельно обработать итерацию.
Тем не менее, будущее многопользовательских вычислений - Blaze. Посмотрите на него, если вы действительно хотите пройти этот маршрут.
"unchunked" case
Сначала рассмотрим 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
быстро растут. Для трехмерного массива большого размера (~ 10 Гб), сохраненного таким образом и считанного через memmap
, чтение полного фрагмента вдоль "худшей" оси может легко занять десятки минут даже при использовании современного оборудования. В то же время срез вдоль наилучшей оси может занимать менее секунды. Для простоты я показываю только "полные" фрагменты вдоль одной оси, но то же самое происходит с произвольными срезами любого подмножества данных.
Кстати, есть несколько форматов файлов, которые используют это и в основном хранят на диске три копии огромных 3D-массивов на диске: один в C-порядке, один в F-порядке и один в промежуточном между ними. (Примером этого является формат Geoprobe D3D, хотя я не уверен, что он где-то документирован.) Кому все равно, если размер финального файла составляет 4 ТБ, хранилище дешево! Сумасшедшая вещь в том, что, поскольку основной вариант использования - извлечение одного суб-среза в каждом направлении, чтение, которое вы хотите сделать, очень и очень быстро. Он работает очень хорошо!
Простой "случайный" случай
Скажем, мы храним 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 запросов для не-chunked версии. (Но лучший вариант - 6 просмотров vs 1 для memmapped array.) Поскольку последовательные чтения очень быстрые по сравнению с запросами, это значительно сокращает время, затрачиваемое на чтение произвольного подмножества в память. И снова этот эффект становится больше при больших массивах.
HDF5 делает это на несколько шагов дальше. Куски не должны храниться смежно, и они индексируются B-Tree. Кроме того, они не должны быть одного размера на диске, поэтому сжатие может быть применено к каждому фрагменту.
Разбитые массивы с 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 и большой (~ 8 ГБ), Fortran-упорядоченный 3D-массив, содержащий те же точные данные.
очистил все кэши OS между каждым прогоном, поэтому мы видим "холодную" производительность.
Для каждого типа файла мы проверим чтение в "полном" x-срезе вдоль первой оси и "полный" z-slize вдоль последней оси. Для упорядоченного по Fortran memmapped массиву "x" срез является наихудшим случаем, а срез "z" - лучший случай.
Используемый код в сущности (включая создание файла hdf
). Я не могу легко использовать данные, используемые здесь, но вы могли бы имитировать массив с нулями нужной формы (621, 4991, 2600)
и type 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 секунд). Учитывая, что это массив размером 8 ГБ, это не так уж плохо. Большую часть времени
И если мы сравним это с временами 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" намного длиннее времени, необходимого для загрузки всего массива 8 ГБ в память и выбора фрагмента, который мы хотели! (Опять же, это упорядоченный по Фортрану массив. Простой момент времени среза x/z будет иметь место для C-упорядоченного массива.)
Однако, если мы всегда хотим взять срез в лучшем случае, большой двоичный массив на диске очень хорош. (~ 0,3 с!)
С memmapped array вы застряли в этом несоответствии ввода/вывода (или, возможно, анизотропия - лучший термин). Однако, с набором данных HDF, вы можете выбрать chunksize, чтобы доступ был либо равным, либо оптимизирован для конкретного прецедента. Это дает вам большую гибкость.
В заключение
Надеюсь, это поможет, во всяком случае, решить одну часть вашего вопроса. HDF5 имеет много других преимуществ по сравнению с "сырыми" memmaps, но у меня нет возможности расширять их все здесь. Сжатие может ускорить некоторые вещи (данные, с которыми я работаю, не сильно выигрывают от сжатия, поэтому я редко их использую), а кэширование на уровне ОС часто играет лучше с файлами HDF5, чем с "сырыми" mem-картами. Кроме того, HDF5 - действительно фантастический формат контейнера. Это дает вам большую гибкость в управлении вашими данными и может использоваться из более или менее любого языка программирования.
В целом, попробуйте и посмотрите, хорошо ли это подходит для вашего случая использования. Я думаю, вы можете быть удивлены.