From 0b7dca3eed67d3db6ecf7ed560b61e07126a11b0 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Wed, 30 Sep 2026 21:20:28 +0200 Subject: [PATCH 1/2] perf(io): open the HDF5 file once per load_mat_style call HDF5LazyArray.load_mat_style opened the file and dataset to get the dataspace, closed them, and opened both again to read. One open and close of the file and dataset costs about 0.24 ms, against about 1.2 ms for a whole small read. The dataspace now comes from the dataset that is opened for the read. The empty-selection path returns before anything is opened, because it reads through a nested load_mat_style call. Reading an 8x3 block from an 8x15000 dataset goes from 1.29 ms to 0.89 ms per call. Co-Authored-By: Claude Opus 5.5 --- +io/+backend/+hdf5/@HDF5LazyArray/load_mat_style.m | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/+io/+backend/+hdf5/@HDF5LazyArray/load_mat_style.m b/+io/+backend/+hdf5/@HDF5LazyArray/load_mat_style.m index 7617d10c8..b9c21306a 100644 --- a/+io/+backend/+hdf5/@HDF5LazyArray/load_mat_style.m +++ b/+io/+backend/+hdf5/@HDF5LazyArray/load_mat_style.m @@ -9,7 +9,6 @@ %% Select from Space dataDimensions = obj.dims; - spaceId = obj.getSpace(); userSelection = varargin; selectionErrorId = 'NWB:DataStub:Load:InvalidSelection'; @@ -41,8 +40,15 @@ data = obj.load_mat_style(1); data = getEmptyRepresentation(data); return + end + + % The dataspace and the read use the same open dataset, so each call + % opens the file once. + fileId = H5F.open(obj.Filename); + datasetId = H5D.open(fileId, obj.DatasetPath); + spaceId = H5D.get_space(datasetId); - elseif isscalar(userSelection) && ~ischar(userSelection{1}) + if isscalar(userSelection) && ~ischar(userSelection{1}) % linear index into the fast dimension. orderedSelection = unique(userSelection{1}); @@ -81,8 +87,6 @@ H5S.close(spaceId); %% Read Data - fileId = H5F.open(obj.Filename); - datasetId = H5D.open(fileId, obj.DatasetPath); data = H5D.read(datasetId, 'H5ML_DEFAULT', memorySpaceId, readSpaceId, 'H5P_DEFAULT'); %% Retype Data From d3c3a53eefa7392af465c5e2a387da5387b793e6 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 1 Oct 2026 21:51:30 +0200 Subject: [PATCH 2/2] perf(io): read a contiguous range of a 1-D dataset as one hyperslab load_mat_style turned every single-subscript selection into a point selection with one point per element. Selecting all 211,722 elements of a 1-D dataset took 22 ms as points and 4 ms as one hyperslab. A selection that is one contiguous run of a one-dimensional dataset is now read as a hyperslab. Any other selection keeps the point selection, which is the faster one when there are gaps: for 2,500 short runs a point selection took 1 ms and a union of 2,500 hyperslabs 74 ms. Co-Authored-By: Claude Opus 5.5 --- .../+hdf5/@HDF5LazyArray/load_mat_style.m | 15 ++++++++++++--- +tests/+unit/+io/+backend/HDF5LazyArrayTest.m | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/+io/+backend/+hdf5/@HDF5LazyArray/load_mat_style.m b/+io/+backend/+hdf5/@HDF5LazyArray/load_mat_style.m index b9c21306a..bc384478f 100644 --- a/+io/+backend/+hdf5/@HDF5LazyArray/load_mat_style.m +++ b/+io/+backend/+hdf5/@HDF5LazyArray/load_mat_style.m @@ -72,11 +72,20 @@ dataDimensions = [dataDimensions, 1]; end - [points{:}] = ind2sub(dataDimensions, orderedSelection); readSpaceId = H5S.copy(spaceId); H5S.select_none(readSpaceId); - H5S.select_elements(readSpaceId, 'H5S_SELECT_SET', ... - cell2mat(flipud(points)) - 1); + isOneRun = orderedSelection(end) - orderedSelection(1) + 1 == numel(orderedSelection); + if isscalar(obj.dims) && isOneRun + % One contiguous run of a one-dimensional dataset is a single + % hyperslab, which HDF5 selects faster than one point per element. + % The selection can be of any integer class, and H5S takes doubles. + H5S.select_hyperslab(readSpaceId, 'H5S_SELECT_SET', ... + double(orderedSelection(1)) - 1, [], numel(orderedSelection), []); + else + [points{:}] = ind2sub(dataDimensions, orderedSelection); + H5S.select_elements(readSpaceId, 'H5S_SELECT_SET', ... + cell2mat(flipud(points)) - 1); + end memorySpaceId = H5S.create_simple(length(selectionDimensions), ... selectionDimensions, selectionDimensions); else diff --git a/+tests/+unit/+io/+backend/HDF5LazyArrayTest.m b/+tests/+unit/+io/+backend/HDF5LazyArrayTest.m index e40c98304..d1f100b69 100644 --- a/+tests/+unit/+io/+backend/HDF5LazyArrayTest.m +++ b/+tests/+unit/+io/+backend/HDF5LazyArrayTest.m @@ -70,6 +70,24 @@ function reorderedFirstSubscriptBeforeFoldedColon(testCase, Subscript) testCase.verifyEqual(lazyArray.load_mat_style(Subscript, ':'), data(Subscript, :)); end + + function loadMatStyleReadsOneDimensionalSelections(testCase) + % A selection that is one contiguous run is read as a hyperslab + % and any other selection as points. Both return the elements + % in the order they were asked for. + filename = "lazy-array-1d.h5"; + data = (101:120)'; + h5create(filename, "/data", numel(data)); + h5write(filename, "/data", data); + lazyArray = io.backend.hdf5.HDF5LazyArray(filename, "/data"); + + selections = {5:9, (9:-1:5)', [3 3 4], 1:20, 20, uint64(5:9), [2 5 9]}; + for iSelection = 1:numel(selections) + selection = selections{iSelection}; + testCase.verifyEqual(lazyArray.load_mat_style(selection), data(selection), ... + sprintf('Unexpected data for selection %s.', mat2str(selection))); + end + end end methods (Access = private)