Skip to content

Commit 0a79d16

Browse files
authored
Merge pull request #1524 from dexhunter/perf/faster-start-count-stride
perf(utils): 2-3x faster _StartCountStride (21% on MFDataset reads)
2 parents ca6f424 + 7eeb2c8 commit 0a79d16

2 files changed

Lines changed: 125 additions & 92 deletions

File tree

‎Changelog‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
version 1.7.6 (not yet released)
2+
================================
3+
* Speed up index setup in _StartCountStride (used when indexing variables) by
4+
skipping numpy conversion for plain ints and slices and classifying each index once (PR #1524).
5+
16
version 1.7.5.1.post1 (tag v1.7.5.1.post1rel)
27
=============================================
38
* fix installation of plugins into linux wheels.

‎src/netCDF4/utils.py‎

Lines changed: 120 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,13 @@ def _StartCountStride(elem, shape, dimensions=None, grp=None, datashape=None,\
225225
e = int(e)
226226
except:
227227
raise IndexError(IndexErrorMsg)
228-
ea = np.asarray(e)
229-
# Raise error if multidimensional indexing is used.
230-
if ea.ndim > 1:
231-
raise IndexError("Index cannot be multidimensional")
228+
# plain integers, slices and ellipsis need no numpy conversion.
229+
simple = type(e) is int or type(e) is slice or e is Ellipsis
230+
if not simple:
231+
ea = np.asarray(e)
232+
# Raise error if multidimensional indexing is used.
233+
if ea.ndim > 1:
234+
raise IndexError("Index cannot be multidimensional")
232235
# set unlim to True if dimension is unlimited and put==True
233236
# (called from __setitem__)
234237
if hasunlim and put and dimensions:
@@ -239,42 +242,11 @@ def _StartCountStride(elem, shape, dimensions=None, grp=None, datashape=None,\
239242
unlim = False
240243
else:
241244
unlim = False
242-
# convert boolean index to integer array.
243-
if np.iterable(ea) and ea.dtype.kind =='b':
244-
# check that boolean array not too long
245-
if not unlim and shape[i] != len(ea):
246-
msg="""
247-
Boolean array must have the same shape as the data along this dimension."""
248-
raise IndexError(msg)
249-
ea = np.flatnonzero(ea)
250-
# an iterable (non-scalar) integer array.
251-
if np.iterable(ea) and ea.dtype.kind == 'i':
252-
# convert negative indices in 1d array to positive ones.
253-
ea = np.where(ea < 0, ea + shape[i], ea)
254-
if np.any(ea < 0):
255-
raise IndexError("integer index out of range")
256-
# if unlim, let integer index be longer than current dimension
257-
# length.
258-
if ea.shape != (0,):
259-
elen = shape[i]
260-
if unlim:
261-
elen = max(ea.max()+1,elen)
262-
if ea.max()+1 > elen:
263-
msg="integer index exceeds dimension size"
264-
raise IndexError(msg)
265-
newElem.append(ea)
266245
# integer scalar
267-
elif ea.dtype.kind == 'i':
246+
if type(e) is int:
268247
newElem.append(e)
269-
# Scalar floats retain compatibility when they represent an exact
270-
# integer, but non-integral values are invalid indices. Letting
271-
# int(e) handle both cases silently truncates values such as 0.8.
272-
elif ea.dtype.kind == 'f' and not np.iterable(ea):
273-
if not np.isfinite(e) or e != np.floor(e):
274-
raise IndexError(IndexErrorMsg)
275-
newElem.append(int(e))
276248
# slice or ellipsis object
277-
elif type(e) == slice or type(e) == type(Ellipsis):
249+
elif simple:
278250
if not use_get_vars and type(e) == slice and e.step not in [None,-1,1] and\
279251
dimensions is not None and grp is not None:
280252
# convert strided slice to integer sequence if possible
@@ -294,12 +266,60 @@ def _StartCountStride(elem, shape, dimensions=None, grp=None, datashape=None,\
294266
except:
295267
pass
296268
newElem.append(e)
297-
else: # castable to a scalar int, otherwise invalid
298-
try:
299-
e = int(e)
269+
else:
270+
# classify the converted index once; ndim is 0 or 1 here.
271+
kind = ea.dtype.kind
272+
# convert boolean index to integer array.
273+
if ea.ndim and kind =='b':
274+
# check that boolean array not too long
275+
if not unlim and shape[i] != len(ea):
276+
msg="""
277+
Boolean array must have the same shape as the data along this dimension."""
278+
raise IndexError(msg)
279+
# nonzero positions are never negative.
280+
ea = np.flatnonzero(ea)
281+
kind = None
282+
# an iterable (non-scalar) integer array.
283+
elif ea.ndim and kind == 'i':
284+
if ea.dtype == np.intp and type(shape[i]) is int and \
285+
not (ea < 0).any():
286+
# nothing to wrap; just avoid aliasing the caller's array.
287+
if ea is e:
288+
ea = ea.copy()
289+
else:
290+
# convert negative indices in 1d array to positive ones.
291+
ea = np.where(ea < 0, ea + shape[i], ea)
292+
if np.any(ea < 0):
293+
raise IndexError("integer index out of range")
294+
kind = None
295+
if kind is None:
296+
# if unlim, let integer index be longer than current dimension
297+
# length.
298+
if len(ea):
299+
elen = shape[i]
300+
emax = ea.max()+1
301+
if unlim:
302+
elen = max(emax,elen)
303+
if emax > elen:
304+
msg="integer index exceeds dimension size"
305+
raise IndexError(msg)
306+
newElem.append(ea)
307+
# integer scalar
308+
elif kind == 'i':
300309
newElem.append(e)
301-
except:
302-
raise IndexError(IndexErrorMsg)
310+
# Scalar floats retain compatibility when they represent an exact
311+
# integer, but non-integral values are invalid indices. Letting
312+
# int(e) handle both cases silently truncates values such as 0.8.
313+
elif kind == 'f' and not ea.ndim:
314+
if not np.isfinite(e) or e != np.floor(e):
315+
raise IndexError(IndexErrorMsg)
316+
newElem.append(int(e))
317+
else: # castable to a scalar int, otherwise invalid
318+
try:
319+
e = int(e)
320+
newElem.append(e)
321+
except:
322+
raise IndexError(IndexErrorMsg)
303323
if type(e)==type(Ellipsis):
304324
i+=1+nDims-len(elem)
305325
else:
@@ -309,13 +329,16 @@ def _StartCountStride(elem, shape, dimensions=None, grp=None, datashape=None,\
309329
# replace Ellipsis and integer arrays with slice objects, if possible.
310330
newElem = []
311331
for e in elem:
312-
ea = np.asarray(e)
332+
if type(e) is int or type(e) is slice:
333+
newElem.append(e)
313334
# Replace ellipsis with slices.
314-
if type(e) == type(Ellipsis):
335+
elif type(e) == type(Ellipsis):
315336
# The ellipsis stands for the missing dimensions.
316337
newElem.extend((slice(None, None, None),) * (nDims - len(elem) + 1))
338+
elif not np.iterable(e):
339+
newElem.append(e)
317340
# Replace sequence of indices with slice object if possible.
318-
elif np.iterable(e) and len(e) > 1:
341+
elif len(e) > 1:
319342
start = e[0]
320343
stop = e[-1]+1
321344
step = e[1]-e[0]
@@ -332,14 +355,12 @@ def _StartCountStride(elem, shape, dimensions=None, grp=None, datashape=None,\
332355
newElem.append(slice(start,stop,step))
333356
else:
334357
newElem.append(e)
335-
elif np.iterable(e) and len(e) == 1:
358+
elif len(e) == 1:
336359
newElem.append(slice(e[0], e[0] + 1, 1))
337-
elif np.iterable(e) and len(e) == 0:
360+
else:
338361
# Retain counts and scalar squeeze markers for the other axes.
339362
# A zero-length chunk axis would discard all of that metadata.
340363
newElem.append(slice(0, 0, 1))
341-
else:
342-
newElem.append(e)
343364
elem = newElem
344365

345366
# If slice doesn't cover all dims, assume ellipsis for rest of dims.
@@ -354,28 +375,26 @@ def _StartCountStride(elem, shape, dimensions=None, grp=None, datashape=None,\
354375
# Compute the dimensions of the start, count, stride and indices arrays.
355376
# The number of elements in the first n dimensions corresponds to the
356377
# number of times the _get method will be called.
357-
sdim = []
358-
for i, e in enumerate(elem):
359-
# at this stage e is a slice, a scalar integer, or a 1d integer array.
360-
# integer array: _get call for each True value
361-
if np.iterable(e):
362-
sdim.append(len(e))
363-
# Scalar int or slice, just a single _get call
364-
else:
365-
sdim.append(1)
378+
# at this stage e is a slice, a scalar integer, or a 1d integer array.
379+
iterable = [type(e) is not int and type(e) is not slice and np.iterable(e)
380+
for e in elem]
381+
# integer array: _get call for each True value
382+
# Scalar int or slice, just a single _get call
383+
sdim = [len(e) if it else 1 for e, it in zip(elem, iterable)]
366384

367385
# broadcast data shape when assigned to full variable (issue #919)
368-
try:
369-
fullslice = elem.count(slice(None,None,None)) == len(elem)
370-
except: # fails if elem contains a numpy array.
371-
fullslice = False
372-
if fullslice and datashape and put and not hasunlim:
373-
datashape = broadcasted_shape(shape, datashape)
386+
if datashape and put and not hasunlim:
387+
try:
388+
fullslice = elem.count(slice(None,None,None)) == len(elem)
389+
except: # fails if elem contains a numpy array.
390+
fullslice = False
391+
if fullslice:
392+
datashape = broadcasted_shape(shape, datashape)
374393

375394
# pad datashape with zeros for dimensions not being sliced (issue #906)
376395
# only used when data covers slice over subset of dimensions
377-
padding_dims = sum(1 for e in elem if type(e) == slice)
378396
if datashape and len(datashape) != len(elem):
397+
padding_dims = sum(1 for e in elem if type(e) == slice)
379398
for i, e in enumerate(elem):
380399
if type(e) != slice:
381400
continue
@@ -404,15 +423,13 @@ def _StartCountStride(elem, shape, dimensions=None, grp=None, datashape=None,\
404423
# integer index arrays into their Cartesian chunk grid if it is nonempty.
405424
sdim.append(max(nDims, 1))
406425
compact = [1] * nDims + [nDims]
407-
start = np.empty(compact, dtype=np.intp)
408-
count = np.empty(compact, dtype=np.intp)
409-
stride = np.empty(compact, dtype=np.intp)
410-
indices = np.empty(compact, dtype=object)
426+
start = []
427+
count = []
428+
stride = []
429+
indices = []
411430

412431
for i, e in enumerate(elem):
413432

414-
ea = np.asarray(e)
415-
416433
# set unlim to True if dimension is unlimited and put==True
417434
# (called from __setitem__). Note: grp and dimensions must be set.
418435
if hasunlim and put and dimensions:
@@ -451,47 +468,58 @@ def _StartCountStride(elem, shape, dimensions=None, grp=None, datashape=None,\
451468
beg, end, inc = e.indices(length)
452469
n = len(range(beg,end,inc))
453470

454-
start[...,i] = beg
455-
count[...,i] = n
456-
stride[...,i] = inc
457-
indices[...,i] = slice(None)
471+
start.append(beg)
472+
count.append(n)
473+
stride.append(inc)
474+
indices.append(slice(None))
458475

459476
# ITERABLE #
460-
elif np.iterable(e) and np.array(e).dtype.kind in 'i': # Sequence of integers
461-
start[...,i] = e[0]
462-
count[...,i] = len(e)
463-
stride[...,i] = 1
464-
indices[...,i] = 0 # preserve the distinction from a full slice
477+
elif iterable[i] and np.asarray(e).dtype.kind in 'i': # Sequence of integers
478+
start.append(e[0])
479+
count.append(len(e))
480+
stride.append(1)
481+
indices.append(0) # preserve the distinction from a full slice
465482

466483
# all that's left is SCALAR INTEGER #
467484
else:
468485
if e >= 0:
469-
start[...,i] = e
486+
start.append(e)
470487
elif e < 0 and (-e <= shape[i]) :
471-
start[...,i] = e+shape[i]
488+
start.append(e+shape[i])
472489
else:
473490
raise IndexError("Index out of range")
474491

475-
count[...,i] = 1
476-
stride[...,i] = 1
477-
indices[...,i] = -1 # Use -1 instead of 0 to indicate that
478-
# this dimension shall be squeezed.
492+
count.append(1)
493+
stride.append(1)
494+
indices.append(-1) # Use -1 instead of 0 to indicate that
495+
# this dimension shall be squeezed.
496+
497+
# Convert the per-dimension values once, rather than writing each one.
498+
empty = 0 in count
499+
start, count, stride = np.array((start, count, stride),
500+
dtype=np.intp).reshape([3] + compact)
501+
indices_ = np.empty(nDims, dtype=object)
502+
indices_[:] = indices
503+
indices = indices_.reshape(compact)
479504

480-
if 0 in count and not put:
505+
if empty and not put:
481506
# Empty reads bypass native bounds checks, including those for vlens.
482507
for i, e in enumerate(elem):
483-
if type(e) != slice and not np.iterable(e) and e >= shape[i]:
508+
if type(e) != slice and not iterable[i] and e >= shape[i]:
484509
raise IndexError("integer index exceeds dimension size")
485510

486-
if 0 not in count and sdim != compact:
511+
if not empty and sdim != compact:
487512
start = np.broadcast_to(start, sdim).copy()
488513
count = np.broadcast_to(count, sdim).copy()
489514
stride = np.broadcast_to(stride, sdim).copy()
490515
indices = np.broadcast_to(indices, sdim).copy()
491516
for i, e in enumerate(elem):
492-
if np.iterable(e):
493-
start[...,i] = np.apply_along_axis(lambda x: e*x, i, np.ones(sdim[:-1]))
494-
indices[...,i] = np.apply_along_axis(lambda x: np.arange(sdim[i])*x, i, np.ones(sdim[:-1], int))
517+
if iterable[i]:
518+
# broadcast the index values along their own chunk axis.
519+
axis = [1] * nDims
520+
axis[i] = sdim[i]
521+
start[...,i] = np.reshape(e, axis)
522+
indices[...,i] = np.arange(sdim[i]).reshape(axis)
495523
count[...,i] = 1
496524

497525
if return_selection:

0 commit comments

Comments
 (0)