Ưu điểm của HDF5: Tổ chức, linh hoạt, khả năng tương tác
Một số ưu điểm chính của HDF5 là cấu trúc phân cấp (tương tự như thư mục / tệp), siêu dữ liệu tùy ý tùy chọn được lưu trữ với từng mục và tính linh hoạt của nó (ví dụ: nén). Cấu trúc tổ chức và lưu trữ siêu dữ liệu này nghe có vẻ tầm thường, nhưng nó rất hữu ích trong thực tế.
Một ưu điểm khác của HDF là tập dữ liệu có thể có kích thước cố định hoặc kích thước linh hoạt. Do đó, thật dễ dàng để nối dữ liệu vào một tập dữ liệu lớn mà không cần phải tạo toàn bộ bản sao mới.
Ngoài ra, HDF5 là một định dạng chuẩn hóa với các thư viện có sẵn cho hầu hết mọi ngôn ngữ, vì vậy việc chia sẻ dữ liệu trên đĩa của bạn giữa Matlab, Fortran, R, C và Python rất dễ dàng với HDF. (Công bằng mà nói, nó cũng không quá khó với một mảng nhị phân lớn, miễn là bạn biết thứ tự C so với F và biết hình dạng, loại, v.v. của mảng được lưu trữ.)
Ưu điểm của HDF cho một mảng lớn: I / O nhanh hơn của một lát tùy ý
Cũng giống như TL / DR: Đối với mảng 3D dung lượng ~ 8GB, việc đọc một lát "đầy đủ" dọc theo bất kỳ trục nào mất ~ 20 giây với tập dữ liệu HDF5 phân khúc và 0,3 giây (trường hợp tốt nhất) đến hơn ba giờ (trường hợp xấu nhất) cho một mảng được ghi nhớ của cùng một dữ liệu.
Ngoài những thứ được liệt kê ở trên, có một lợi thế lớn khác đối với định dạng dữ liệu trên đĩa "chunked" * chẳng hạn như HDF5: Đọc một lát tùy ý (nhấn mạnh vào tùy ý) thường sẽ nhanh hơn nhiều, vì dữ liệu trên đĩa liền kề hơn Trung bình cộng.
*
(HDF5 không nhất thiết phải là một định dạng dữ liệu phân đoạn. Nó hỗ trợ phân đoạn, nhưng không yêu cầu. Trên thực tế, mặc định để tạo tập dữ liệu h5py
không phải là phân đoạn, nếu tôi nhớ chính xác.)
Về cơ bản, tốc độ đọc đĩa trong trường hợp tốt nhất của bạn và tốc độ đọc đĩa trong trường hợp xấu nhất của bạn đối với một phần nhất định của tập dữ liệu của bạn sẽ khá gần với tập dữ liệu HDF phân khúc (giả sử bạn đã chọn kích thước chunk hợp lý hoặc để thư viện chọn cho bạn). Với một mảng nhị phân đơn giản, trường hợp tốt nhất sẽ nhanh hơn, nhưng trường hợp xấu nhất thì tệ hơn nhiều .
Một lưu ý, nếu bạn có SSD, bạn có thể sẽ không nhận thấy sự khác biệt lớn về tốc độ đọc / ghi. Tuy nhiên, với một ổ cứng thông thường, việc đọc tuần tự nhanh hơn rất nhiều so với việc đọc ngẫu nhiên. (ví dụ: Ổ cứng thông thường có thời seek
gian sử dụng lâu .) HDF vẫn có lợi thế hơn trên SSD, nhưng đó là do các tính năng khác của nó (ví dụ siêu dữ liệu, tổ chức, v.v.) hơn là do tốc độ thô.
Trước hết, để giải quyết sự nhầm lẫn, việc truy cập một h5py
tập dữ liệu trả về một đối tượng hoạt động khá giống với một mảng numpy, nhưng không tải dữ liệu vào bộ nhớ cho đến khi nó bị cắt. (Tương tự như memmap, nhưng không giống nhau.) Hãy xem phần h5py
giới thiệu để biết thêm thông tin.
Cắt tập dữ liệu sẽ tải một tập hợp con dữ liệu vào bộ nhớ, nhưng có lẽ bạn muốn làm gì đó với nó, lúc đó bạn vẫn cần nó trong bộ nhớ.
Nếu bạn muốn thực hiện các tính toán ngoài lõi, bạn có thể khá dễ dàng đối với dữ liệu dạng bảng với pandas
hoặc pytables
. Điều đó có thể xảy ra với h5py
(đẹp hơn đối với các mảng ND lớn), nhưng bạn cần phải thả xuống mức thấp hơn và tự xử lý việc lặp lại.
Tuy nhiên, tương lai của các tính toán ngoài lõi giống như numpy là Blaze. Hãy xem nó nếu bạn thực sự muốn đi theo con đường đó.
Trường hợp "unchunked"
Trước hết, hãy xem xét một mảng 3D có thứ tự C được ghi vào đĩa (tôi sẽ mô phỏng nó bằng cách gọi arr.ravel()
và in kết quả, để làm cho mọi thứ rõ ràng hơn):
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]]])
Các giá trị sẽ được lưu trữ tuần tự trên đĩa như thể hiện ở dòng 4 bên dưới. (Hãy bỏ qua chi tiết và phân mảnh hệ thống tệp vào lúc này.)
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])
Trong trường hợp tốt nhất, hãy lấy một lát dọc theo trục đầu tiên. Lưu ý rằng đây chỉ là 36 giá trị đầu tiên của mảng. Đây sẽ là một bài đọc rất nhanh! (một lần tìm kiếm, một lần đọc)
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]])
Tương tự, lát cắt tiếp theo dọc theo trục đầu tiên sẽ chỉ là 36 giá trị tiếp theo. Để đọc một lát hoàn chỉnh dọc theo trục này, chúng ta chỉ cần một seek
thao tác. Nếu tất cả những gì chúng ta sẽ đọc là các lát cắt khác nhau dọc theo trục này, thì đây là cấu trúc tệp hoàn hảo.
Tuy nhiên, hãy xem xét trường hợp xấu nhất: Một lát dọc theo trục cuối cùng.
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]])
Để đọc phần này, chúng ta cần 36 lần tìm và 36 lần đọc, vì tất cả các giá trị đều được phân tách trên đĩa. Không ai trong số họ liền kề!
Điều này có vẻ khá nhỏ, nhưng khi chúng ta tiến tới các mảng ngày càng lớn, số lượng và kích thước của các seek
hoạt động sẽ tăng lên nhanh chóng. Đối với một mảng 3D lớn (~ 10Gb) được lưu trữ theo cách này và đọc qua memmap
, việc đọc toàn bộ phần dọc theo trục "tồi tệ nhất" có thể dễ dàng mất hàng chục phút, ngay cả với phần cứng hiện đại. Đồng thời, một lát dọc theo trục tốt nhất có thể mất ít hơn một giây. Để đơn giản, tôi chỉ hiển thị các lát "đầy đủ" dọc theo một trục duy nhất, nhưng điều tương tự cũng xảy ra với các lát tùy ý của bất kỳ tập con dữ liệu nào.
Ngẫu nhiên, có một số định dạng tệp tận dụng lợi thế này và về cơ bản lưu trữ ba bản sao của mảng 3D khổng lồ trên đĩa: một theo thứ tự C, một theo thứ tự F và một ở trung gian giữa hai. (Một ví dụ về điều này là định dạng D3D của Geoprobe, mặc dù tôi không chắc nó được ghi lại ở bất kỳ đâu.) Ai quan tâm nếu kích thước tệp cuối cùng là 4TB, dung lượng lưu trữ rất rẻ! Điều điên rồ về điều đó là bởi vì trường hợp sử dụng chính là trích xuất một lát phụ duy nhất theo mỗi hướng, các lần đọc bạn muốn thực hiện rất rất nhanh. Nó hoạt động rất tốt!
Trường hợp "chunked" đơn giản
Giả sử chúng tôi lưu trữ các "khối" 2x2x2 của mảng 3D dưới dạng các khối liền kề trên đĩa. Nói cách khác, một cái gì đó như:
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])
Vì vậy, dữ liệu trên đĩa sẽ giống như chunked
sau:
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])
Và chỉ để cho thấy rằng chúng là các khối 2x2x2 arr
, hãy lưu ý rằng đây là 8 giá trị đầu tiên của chunked
:
In [9]: arr[:2, :2, :2]
Out[9]:
array([[[ 0, 1],
[ 6, 7]],
[[36, 37],
[42, 43]]])
Để đọc trong bất kỳ phần nào dọc theo một trục, chúng tôi sẽ đọc trong 6 hoặc 9 phần liền kề (gấp đôi dữ liệu mà chúng tôi cần) và sau đó chỉ giữ lại phần mà chúng tôi muốn. Đó là trường hợp xấu nhất tối đa là 9 lần tìm kiếm so với tối đa 36 lần tìm kiếm cho phiên bản không phân khúc. (Nhưng trường hợp tốt nhất vẫn là 6 lần tìm kiếm so với 1 lần tìm kiếm đối với mảng được ánh xạ.) Bởi vì đọc tuần tự rất nhanh so với lần tìm kiếm, điều này làm giảm đáng kể lượng thời gian cần thiết để đọc một tập hợp con tùy ý vào bộ nhớ. Một lần nữa, hiệu ứng này trở nên lớn hơn với các mảng lớn hơn.
HDF5 tiến xa hơn một vài bước. Các phần không cần phải được lưu trữ liền kề và chúng được lập chỉ mục bởi B-Tree. Hơn nữa, chúng không nhất thiết phải có cùng kích thước trên đĩa, do đó, nén có thể được áp dụng cho từng đoạn.
Mảng chunked với h5py
Theo mặc định, h5py
không tạo các tệp HDF phân khối trên đĩa ( pytables
ngược lại, tôi nghĩ là có). chunks=True
Tuy nhiên, nếu bạn chỉ định khi tạo tập dữ liệu, bạn sẽ nhận được một mảng phân đoạn trên đĩa.
Như một ví dụ nhanh, tối thiểu:
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?'
Lưu ý rằng chunks=True
yêu h5py
cầu tự động chọn kích thước phân đoạn cho chúng tôi. Nếu bạn biết thêm về trường hợp sử dụng phổ biến nhất của mình, bạn có thể tối ưu hóa kích thước / hình dạng phân đoạn bằng cách chỉ định một bộ hình dạng (ví dụ: (2,2,2)
trong ví dụ đơn giản ở trên). Điều này cho phép bạn thực hiện đọc dọc theo một trục cụ thể hiệu quả hơn hoặc tối ưu hóa cho việc đọc / ghi ở một kích thước nhất định.
So sánh hiệu suất I / O
Chỉ để nhấn mạnh vấn đề, chúng ta hãy so sánh việc đọc theo từng lát từ một tập dữ liệu HDF5 được chia nhỏ và một mảng 3D lớn (~ 8GB), theo thứ tự của Fortran chứa cùng một dữ liệu chính xác.
Tôi đã xóa tất cả bộ nhớ cache của hệ điều hành giữa mỗi lần chạy, vì vậy chúng tôi đang thấy hiệu suất "lạnh".
Đối với mỗi loại tệp, chúng tôi sẽ kiểm tra việc đọc trong một lát cắt x "đầy đủ" dọc theo trục đầu tiên và một lát cắt z "đầy đủ" dọc theo trục cuối cùng. Đối với mảng được ánh xạ theo thứ tự Fortran, lát cắt "x" là trường hợp xấu nhất và lát "z" là trường hợp tốt nhất.
Mã được sử dụng là một ý chính (bao gồm cả việc tạo hdf
tệp). Tôi không thể dễ dàng chia sẻ dữ liệu được sử dụng ở đây, nhưng bạn có thể mô phỏng nó bằng một mảng các số không có cùng hình dạng ( 621, 4991, 2600)
và kiểu np.uint8
.
Các chunked_hdf.py
ngoại hình như thế này:
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
tương tự, nhưng phức tạp hơn để đảm bảo các lát cắt thực sự được tải vào bộ nhớ (theo mặc định, một memmapped
mảng khác sẽ được trả về, không phải là so sánh giữa táo với táo).
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()
Trước tiên, hãy xem hiệu suất 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
Một lát x "đầy đủ" và một lát z "đầy đủ" mất khoảng thời gian như nhau (~ 20 giây). Xem xét đây là một mảng 8GB, điều đó không quá tệ. Hầu hết thời gian
Và nếu chúng ta so sánh điều này với thời gian của mảng được ghi nhớ (đó là theo thứ tự Fortran: "z-slice" là trường hợp tốt nhất và "x-slice" là trường hợp xấu nhất.):
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
Bạn đã đọc đúng. 0,3 giây cho một hướng lát và ~ 3,5 giờ cho hướng kia.
Thời gian để lát trong "x" chỉ đạo là xa dài hơn khoảng thời gian nó sẽ mất để tải toàn bộ mảng 8GB vào bộ nhớ và chọn slice chúng tôi muốn! (Một lần nữa, đây là một mảng được sắp xếp theo thứ tự Fortran. Thời gian lát cắt x / z ngược lại sẽ là trường hợp cho một mảng có thứ tự C.)
Tuy nhiên, nếu chúng ta luôn muốn đi theo hướng trường hợp tốt nhất, thì mảng nhị phân lớn trên đĩa là rất tốt. (~ 0,3 giây!)
Với một mảng được ghi nhớ, bạn sẽ mắc kẹt với sự khác biệt I / O này (hoặc có lẽ dị hướng là một thuật ngữ tốt hơn). Tuy nhiên, với tập dữ liệu HDF được phân loại, bạn có thể chọn kích thước khối sao cho quyền truy cập bằng nhau hoặc được tối ưu hóa cho một trường hợp sử dụng cụ thể. Nó mang lại cho bạn sự linh hoạt hơn rất nhiều.
Tóm tắt
Hy vọng rằng điều đó sẽ giúp làm sáng tỏ một phần câu hỏi của bạn, ở bất kỳ mức độ nào. HDF5 có nhiều lợi thế khác so với các bản ghi nhớ "thô", nhưng tôi không có chỗ để mở rộng tất cả chúng ở đây. Nén có thể tăng tốc một số thứ (dữ liệu tôi làm việc với không được lợi nhiều từ việc nén, vì vậy tôi hiếm khi sử dụng nó) và bộ nhớ đệm cấp hệ điều hành thường chơi với các tệp HDF5 hơn là với các bản ghi nhớ "thô". Ngoài ra, HDF5 là một định dạng container thực sự tuyệt vời. Nó mang lại cho bạn sự linh hoạt trong việc quản lý dữ liệu của mình và có thể được sử dụng ít nhiều từ bất kỳ ngôn ngữ lập trình nào.
Nhìn chung, hãy thử nó và xem nó có hoạt động tốt cho trường hợp sử dụng của bạn hay không. Tôi nghĩ bạn có thể ngạc nhiên.
h5py
phù hợp hơn với các tập dữ liệu như của bạn hơnpytables
. Ngoài ra,h5py
không không trả về một mảng NumPy trong bộ nhớ. Thay vào đó, nó trả về một cái gì đó hoạt động giống như một cái, nhưng không được tải vào bộ nhớ (tương tự như mộtmemmapped
mảng). Tôi đang viết một câu trả lời đầy đủ hơn (có thể không kết thúc nó), nhưng hy vọng nhận xét này sẽ giúp một chút trong thời gian chờ đợi.