-
-
Notifications
You must be signed in to change notification settings - Fork 11k
ENH: Use itertools.product for ndindex to improve performance #29165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
ENH: Use itertools.product for ndindex to improve performance #29165
Conversation
print(f"Time to consume first 1000 elements: {partial_time * 1000:.3f} ms") | ||
print("--> Memory efficient: Only generates indices as needed") | ||
|
||
if __name__ == "__main__": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be more sensible to follow our usual approach to writing/running benchmarks with asv
. It has builtin harnesses for timing (time_
) and peak memory usage (peakmem_
). It will probably be easier to evaluate changes when sticking to this common approach we use for benchmarking perf/memory usage. It shouldn't be too hard to navigate around the other asv
benchmarking modules we have to get a sort of "template" for how that works.
New in version X.Y.Z. | ||
As of NumPy 2.3.0.dev0, this iterator is implemented using `itertools.product` | ||
from Python's standard library. This change provides significant improvements | ||
in both performance and and memory efficiency, particularly for large iteration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
repeated and
; we'll probably want to change the version numbering you use above since i.e., 2.3.0
has already been released
# 1. Empty Shapes and Zero Dimensions | ||
def test_ndindex_empty_and_zero_dimensions(): | ||
# Empty shape | ||
assert list(np.ndindex()) == [()] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It probably does make sense to add a few more test cases, but note that the already-existing test_ndindex
already has this particular test case for example, and may cover one or two other cases.
with pytest.raises(TypeError): | ||
list(np.ndindex([2, 3])) | ||
with pytest.raises(TypeError): | ||
list(np.ndindex((2.0, 3))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor: could use pytest.mark.parametrize
for concision and separation of case execution
@@ -724,8 +734,7 @@ def __next__(self): | |||
iteration. | |||
|
|||
""" | |||
next(self._it) | |||
return self._it.multi_index | |||
return next(self.iter) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should help with the test failures
return next(self.iter) | |
return next(self._iter) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh nice, full suite passes locally with that patch
expected_count = 20 * 30 * 40 | ||
assert_equal(count, expected_count) | ||
|
||
assert elapsed_time < 1.0, f"ndindex took {elapsed_time:.3f}s, which seems slow" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't usually do this in our testsuite--we typically guard against performance regressions using asv
benchmarks separate from our regression tests proper (see my other comment about asv
)
Summary
This PR rewrites
numpy.ndindex
to useitertools.product
instead ofnditer
, significantly improving both runtime performance and memory efficiency while preserving full backward compatibility.It addresses Issue #28921: ndindex is slower than itertools alternative.
Motivation
The current implementation of
ndindex
usesnditer
, which is flexible but adds unnecessary overhead when generating Cartesian product indices. Sinceitertools.product
is a well-optimized C implementation for this exact pattern, switching to it results in a notable performance improvement.Changes Made
ndindex.__init__
and__next__
methods innumpy/lib/_index_tricks_impl.py
to utilizeitertools.product
.numpy/lib/tests/test_index_tricks.py
to cover various scenarios (e.g., empty/zero dimensions, non-integer dimensions, iterator independence, stop iteration behavior, large dimensions, and consistency withndenumerate
).benchmarks/benchmarks/bench_ndindex.py
to provide a clear performance comparison.ndindex
in_index_tricks_impl.py
to include aNotes
section explaining the implementation change and its benefits.Benchmark Results
Benchmarks were added in
benchmarks/benchmarks/bench_ndindex.py
. Here is a summary:Memory Usage Test:
SUCCESS! Average performance improvement: 9.8x
This demonstrates excellent performance improvement.
Test Cases & Functional Correctness
The following new tests were added to ensure correctness across a variety of use cases:
Tests live in numpy/lib/tests/test_index_tricks.py.
Environment/Testing Notes:
✅ The optimized implementation was developed and tested locally in a standalone file (ndindex_optimized.py), separate from the NumPy codebase.
✅ A full set of unit tests were written and run successfully in isolation using pytest, validating correctness across all intended behaviors.
🔁 When attempting to run the full NumPy test suite inside a GitHub Codespace (editable install with pip install -e . and meson/ninja), I encountered the following persistent import error:
ImportError: cannot import name 'version' from partially initialized module 'numpy'
🔍 This appears to be a build tooling issue, not a problem with the PR's logic or correctness.
📦 Submitting this PR in hopes that GitHub Actions CI will successfully build and test the full suite, or maintainers can help identify the issue with the local dev environment
Docstring Update
A Notes section has been added to the ndindex docstring to reflect the new implementation:
✳️ Replace X.Y.Z with the release version when merging.
Related Issue
Closes: #28921
Author Note
👋 I'm a first-time contributor to NumPy and would love feedback on this PR. Thanks to @seberg and @ricardoV94 for the helpful context in the original issue!
📎 GitHub: @samruddhibaviskar11