Skip to content

Precompute little-endian sample dtype once per stream#170

Merged
cbrnr merged 2 commits into
xdf-modules:mainfrom
sappelhoff:perf/precompute-le-dtype
Jun 15, 2026
Merged

Precompute little-endian sample dtype once per stream#170
cbrnr merged 2 commits into
xdf-modules:mainfrom
sappelhoff:perf/precompute-le-dtype

Conversation

@sappelhoff

@sappelhoff sappelhoff commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

NOTE I generated this PR with the help of Claude as part of a general memory/speed profiling I am doing for a private package of mine.

What

_read_chunk3 reinterprets each numeric sample buffer as little-endian via np.frombuffer(raw, dtype=s.dtype.newbyteorder("<"), count=s.nchns). s.dtype.newbyteorder("<") returns the same dtype object every time but was being recomputed once per sample. This moves it to StreamData.__init__ (s.dtype_le, alongside the existing dtype/samplebytes pre-calcs) and reuses it.

Why it's behaviour-preserving

  • XDF sample data is little-endian, and the original code already hardcoded "<" regardless of platform — s.dtype_le = s.dtype.newbyteorder("<") is the identical value, so output is byte-identical (including on big-endian hosts).
  • dtype_le is set under the same if self.fmt != "string": guard that already gates self.dtype, and its only use is the numeric branch of _read_chunk3. String streams never set or read it. Empty streams never call _read_chunk3.
  • newbyteorder is dtype-generic (no type-specific branch), so all numeric formats behave identically.

The .pxd Cython declaration is updated to keep StreamData in sync.

Verification

  • Full suite: 372 passed, 1 xfailed (with example-files/ cloned), covering int16/int32/float32/double64/string/empty streams — every sample-reading path.
  • Byte-identical per-stream time_series/time_stamps md5 on a real 224 MB recording (99-ch EEG @ 500 Hz + marker/logging/notes/misc streams), baseline vs this branch.
  • ~8% faster load_xdf on that recording (median 1.014 s → 0.935 s, 6 reps), as the per-sample newbyteorder (553k calls) is eliminated.

The little-endian dtype view used by np.frombuffer in _read_chunk3 was
recomputed once per sample via s.dtype.newbyteorder('<'). Compute it once
in StreamData.__init__ (s.dtype_le) and reuse it. XDF sample data is
little-endian, so the result is byte-identical; loading a 553k-sample,
99-channel EEG recording is ~8% faster.
@cbrnr cbrnr merged commit 9aa0f3d into xdf-modules:main Jun 15, 2026
6 checks passed
@cbrnr

cbrnr commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Very nice, thanks @sappelhoff!

@sappelhoff sappelhoff deleted the perf/precompute-le-dtype branch June 15, 2026 08:28
@sappelhoff

Copy link
Copy Markdown
Contributor Author

Sure! Could we perhaps get a release with this? 🙏 the speedup is modest but noticeable :)

@cbrnr

cbrnr commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Yes, I will try to do it today or tomorrow at the latest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants