Skip to content

Commit 8b5f589

Browse files
committed
Fast path for NDArray.slice of consecutive chunks
1 parent cddb5c3 commit 8b5f589

3 files changed

Lines changed: 173 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#######################################################################
2+
# Copyright (c) 2019-present, Blosc Development Team <blosc@blosc.org>
3+
# All rights reserved.
4+
#
5+
# This source code is licensed under a BSD-style license (found in the
6+
# LICENSE file in the root directory of this source tree)
7+
#######################################################################
8+
9+
# Benchmark for comparing speeds of NDArray.slice() when using
10+
# different slices containing consecutive and non-consecutive chunks.
11+
12+
import math
13+
from time import time
14+
import numpy as np
15+
import blosc2
16+
17+
# Dimensions and type properties for the arrays
18+
shape = (50, 100, 300)
19+
chunks = (5, 25, 50)
20+
blocks = (1, 5, 10)
21+
dtype = np.dtype(np.int32)
22+
23+
# Consecutive slices
24+
c_slices = [
25+
(slice(0, 50), slice(0, 100), slice(0, 300)),
26+
(slice(0, 10), slice(0, 100), slice(0, 300)),
27+
(slice(0, 5), slice(0, 25), slice(0, 300)),
28+
(slice(0, 5), slice(0, 25), slice(0, 50)),
29+
]
30+
# Non-consecutive slices
31+
nc_slices = [
32+
(slice(0, 50), slice(0, 100), slice(0, 300-1)),
33+
(slice(0, 10), slice(0, 100-1), slice(0, 300)),
34+
(slice(0, 5-1), slice(0, 25), slice(0, 300)),
35+
(slice(0, 5), slice(0, 25), slice(0, 50-1)),
36+
]
37+
38+
print("Creating array with shape:", shape)
39+
arr = blosc2.arange(math.prod(shape), dtype=dtype, shape=shape, chunks=chunks, blocks=blocks)
40+
41+
t0 = time()
42+
for s in c_slices:
43+
arr2 = arr.slice(s)
44+
# print(arr2.shape, arr[s].shape)
45+
# print(arr2.schunk.nbytes, arr[s].nbytes)
46+
# np.testing.assert_array_equal(arr2[:], arr[s])
47+
t1 = time() - t0
48+
print(f"Time to get consecutive slices: {t1:.5f}")
49+
50+
t0 = time()
51+
for s in nc_slices:
52+
arr2 = arr.slice(s)
53+
# print(arr2.schunk.nbytes, arr[s].nbytes)
54+
# np.testing.assert_array_equal(arr2[:], arr[s])
55+
t1 = time() - t0
56+
print(f"Time to get non-consecutive slices: {t1:.5f}")

src/blosc2/ndarray.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import math
1414
from collections import OrderedDict, namedtuple
1515
from functools import reduce
16+
from itertools import product
1617
from typing import TYPE_CHECKING, Any, NamedTuple
1718

1819
from numpy.exceptions import ComplexWarning
@@ -1029,6 +1030,89 @@ def extract_values(arr, indices: np.ndarray[np.int_], max_cache_size: int = 10)
10291030
return extracted_values
10301031

10311032

1033+
def detect_consecutive_chunks( # noqa: C901
1034+
key: Sequence[slice], shape: Sequence[int], chunks: Sequence[int]
1035+
) -> list[int]:
1036+
"""
1037+
Detect whether a multidimensional slice matches a sequence of consecutive chunk boundaries.
1038+
1039+
Parameters
1040+
----------
1041+
key : Sequence of slice
1042+
The multidimensional slice to check.
1043+
shape : Sequence of int
1044+
Shape of the NDArray.
1045+
chunks : Sequence of int
1046+
Chunk shape of the NDArray.
1047+
1048+
Returns
1049+
-------
1050+
list[int]
1051+
Index of the chunk (in C-order) if the slice matches exactly with a single chunk,
1052+
a list of chunk indices if the slice matches a consecutive sequence of chunks.
1053+
If it doesn't match any chunk(s) properly, return an empty list.
1054+
"""
1055+
if len(key) != len(shape):
1056+
return []
1057+
1058+
# Check that slice boundaries are exact multiple of chunk boundaries.
1059+
# We want to do that so we don't copy data, and hence, waste space,
1060+
# unnecessarily into destination.
1061+
for i, s in enumerate(key):
1062+
if s.start is not None and s.start % chunks[i] != 0:
1063+
return []
1064+
if s.stop is not None and s.stop % chunks[i] != 0:
1065+
return []
1066+
1067+
# Parse the slice boundaries and check for alignment
1068+
start_indices = []
1069+
end_indices = []
1070+
n_chunks = []
1071+
1072+
for i, s in enumerate(key):
1073+
start = s.start if s.start is not None else 0
1074+
stop = s.stop if s.stop is not None else shape[i]
1075+
1076+
if stop > shape[i]:
1077+
return []
1078+
1079+
chunk_size = chunks[i]
1080+
1081+
# Ensure alignment with chunk boundaries
1082+
if start % chunk_size != 0:
1083+
return []
1084+
1085+
start_idx = start // chunk_size
1086+
end_idx = math.ceil(stop / chunk_size)
1087+
start_indices.append(start_idx)
1088+
end_indices.append(end_idx)
1089+
n_chunks.append(math.ceil(shape[i] / chunk_size))
1090+
1091+
indices = [range(start, end) for start, end in zip(start_indices, end_indices, strict=False)]
1092+
result = []
1093+
1094+
for combination in product(*indices):
1095+
flat_index = 0
1096+
multiplier = 1
1097+
for idx, n in zip(reversed(range(len(n_chunks))), reversed(n_chunks), strict=False):
1098+
flat_index += combination[idx] * multiplier
1099+
multiplier *= n
1100+
1101+
result.append(flat_index)
1102+
1103+
if not result:
1104+
return []
1105+
1106+
# The product() of ranges might not naturally produce indices in ascending order
1107+
result.sort()
1108+
is_consecutive = builtins.all(result[i] == result[i - 1] + 1 for i in range(1, len(result)))
1109+
1110+
if not is_consecutive:
1111+
return []
1112+
1113+
return result
1114+
1115+
10321116
class NDOuterIterator:
10331117
def __init__(self, ndarray: NDArray | NDField, cache_size=1):
10341118
self.ndarray = ndarray
@@ -1846,6 +1930,29 @@ def slice(self, key: int | slice | Sequence[slice], **kwargs: Any) -> NDArray:
18461930
kwargs = _check_ndarray_kwargs(**kwargs) # sets cparams to defaults
18471931
key, mask = process_key(key, self.shape)
18481932
start, stop, step = get_ndarray_start_stop(self.ndim, key, self.shape)
1933+
1934+
# Fast path for slices made with consecutive chunks
1935+
if step == (1,) * self.ndim:
1936+
consecutive_chunks = detect_consecutive_chunks(key, self.shape, self.chunks)
1937+
if consecutive_chunks:
1938+
# Create a new ndarray for the key slice
1939+
new_shape = [
1940+
sp - st for sp, st in zip([k.stop for k in key], [k.start for k in key], strict=False)
1941+
]
1942+
newarr = blosc2.empty(
1943+
shape=new_shape,
1944+
dtype=self.dtype,
1945+
chunks=self.chunks,
1946+
blocks=self.blocks,
1947+
**kwargs,
1948+
)
1949+
# Get the chunks from the original array and update the new array
1950+
# No need for chunks to decompress and compress again
1951+
for order, nchunk in enumerate(consecutive_chunks):
1952+
chunk = self.schunk.get_chunk(nchunk)
1953+
newarr.schunk.update_chunk(order, chunk)
1954+
return newarr
1955+
18491956
key = (start, stop)
18501957
ndslice = super().get_slice(key, mask, **kwargs)
18511958

tests/ndarray/test_slice.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@
1616
([456], [258], [73], slice(0, 1), np.int32),
1717
([77, 134, 13], [31, 13, 5], [7, 8, 3], (slice(3, 7), slice(50, 100), 7), np.float64),
1818
([12, 13, 14, 15, 16], [5, 5, 5, 5, 5], [2, 2, 2, 2, 2], (slice(1, 3), ..., slice(3, 6)), np.float32),
19+
# Consecutive slices
20+
((10, 100, 300), (5, 25, 50), (1, 5, 10), (slice(0, 10), slice(0, 100), slice(0, 300)), np.int32),
21+
((10, 100, 300), (5, 25, 50), (1, 5, 10), (slice(0, 5), slice(0, 100), slice(0, 300)), np.int32),
22+
((10, 100, 300), (5, 25, 50), (1, 5, 10), (slice(0, 5), slice(0, 25), slice(0, 200)), np.int32),
23+
((10, 100, 300), (5, 25, 50), (1, 5, 10), (slice(0, 5), slice(0, 25), slice(0, 50)), np.int32),
24+
# Non-consecutive slices
25+
((10, 100, 300), (5, 25, 50), (1, 5, 10), (slice(0, 10), slice(0, 100), slice(0, 300 - 1)), np.int32),
26+
((10, 100, 300), (5, 25, 50), (1, 5, 10), (slice(0, 5), slice(0, 100 - 1), slice(0, 300)), np.int32),
27+
((10, 100, 300), (5, 25, 50), (1, 5, 10), (slice(0, 5 - 1), slice(0, 25), slice(0, 200)), np.int32),
28+
((10, 100, 300), (5, 25, 50), (1, 5, 10), (slice(0, 5), slice(0, 25), slice(0, 50 - 1)), np.int32),
1929
]
2030

2131

0 commit comments

Comments
 (0)