Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,15 @@ changelog entry instead of a plain name link.
# (BSD-compatible).
```
If the license of a snippet cannot be determined, do not adapt it.

- Benchmarking performance changes: only interleaved A/B runs against a
pristine snapshot built from the exact upstream base are trustworthy;
whole-suite back-to-back runs drift ±20–100 %. Verify which installed mne a
benchmark actually imports (`print(mne.__file__)`) before trusting numbers,
and keep fixture data out of commits.
- Changelog fragments (`doc/changes/dev/<PR#>.<type>.rst`): pick `<type>` by
intent — performance improvements are `newfeature`, not `bugfix`. Keep the
entry to one short sentence ending with the contributor name link, e.g.
"Speed up X by optimizing Y, by `Jane Doe`_", and make sure the name anchors
in `doc/changes/names.inc` (add it if missing). Read an existing fragment
or two before writing yours.
1 change: 1 addition & 0 deletions doc/changes/dev/14212.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Speed up reading by optimizing small read paths, by `Bruno Aristimunha`_
15 changes: 15 additions & 0 deletions mne/_fiff/pick.py
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,21 @@ def _picks_to_idx(
)
raise TypeError(msg)
del extra_repr
# Fast path: an integer ndarray with all values already in range needs no
# copy or further checks. This matters for callers resolving picks on
# every access (e.g., Raw.get_data in deep-learning training loops).
Comment thread
bruAristimunha marked this conversation as resolved.
if picks.dtype.kind == "i" and len(picks):
sorted_picks = np.unique(picks)
if (
len(sorted_picks) == len(picks)
and sorted_picks[0] >= 0
and sorted_picks[-1] < n_chan
):
# Benchmark (64 ch EDF, picks=None per call): ~65 -> ~25 us saved
# per resolve; scales with n_channels.
if return_kind:
return picks, picked_ch_type_or_generic
return picks
picks = picks.astype(int)

#
Expand Down
12 changes: 8 additions & 4 deletions mne/_fiff/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,26 @@ def _find_channels(ch_names, ch_type="EOG"):

def _mult_cal_one(data_view, one, idx, cals, mult):
"""Take a chunk of raw data, multiply by mult or cals, and store."""
one = np.asarray(one, dtype=data_view.dtype)
assert data_view.shape[1] == one.shape[1], (
data_view.shape[1],
one.shape[1],
) # noqa: E501
if mult is not None:
one = np.asarray(one, dtype=data_view.dtype)
assert mult.ndim == one.ndim == 2
data_view[:] = mult @ one[idx]
else:
assert cals is not None
if isinstance(idx, slice):
data_view[:] = one[idx]
# Hot path: gather + type-cast + calibration in a single pass
# (was three passes plus a full float64 temporary).
# Benchmark (128 ch x 1024 samples): ~85 -> ~30 us per call
# on BrainVision/FIF window reads.
np.multiply(one[idx], cals.reshape(-1, 1), out=data_view, casting="unsafe")
else:
# faster than doing one = one[idx]
one = np.asarray(one, dtype=data_view.dtype)
np.take(one, idx, axis=0, out=data_view)
data_view *= cals
data_view *= cals


def _blk_read_lims(start, stop, buf_len):
Expand Down
8 changes: 7 additions & 1 deletion mne/io/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,13 @@ def get_data(
stop, types=("int-like", None), item_name="stop", type_name="int, None"
)

picks = _picks_to_idx(self.info, picks, "all", exclude=())
if picks is None:
# Fast lane: picks=None resolves to arange directly.
# Benchmark (300 s recording): stops a 600 KB time-axis
# allocation and ~40 us of name resolution on every call.
picks = np.arange(self.info["nchan"])
else:
picks = _picks_to_idx(self.info, picks, "all", exclude=())

# Get channel factors for conversion into specified unit
# (vector of ones if no conversion needed)
Expand Down
9 changes: 7 additions & 2 deletions mne/utils/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,8 +575,13 @@ def _handle_tmin_tmax(self, tmin, tmax):
type_name="int, float, None",
)

# handle tmin/tmax as start and stop indices into data array
n_times = self.times.size
# handle tmin/tmax as start and stop indices into data array.
# Prefer an integer n_times (available on Raw); falling back to
# times.size there would materialize the full time vector on every
# call, which dominates the cost of many small get_data() reads.
n_times = getattr(self, "n_times", None)
if n_times is None:
n_times = self.times.size
start = 0 if tmin is None else self.time_as_index(tmin)[0]
stop = n_times if tmax is None else self.time_as_index(tmax)[0]

Expand Down
Loading