-
Notifications
You must be signed in to change notification settings - Fork 861
Optimize nth and nth_back for BoundListIterator #4810
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
Optimize nth and nth_back for BoundListIterator #4810
Conversation
Awesome, thanks! Will seek to review soon. I guess we can do the same thing for tuple iteration? |
Yup - I'll file a PR to optimise tuple iterator after the holiday =) |
src/types/list.rs
Outdated
let length = self.length.min(self.list.len()); | ||
let target_index = self.index + n; | ||
if self.index + n < length { | ||
let item = unsafe { self.get_item(target_index) }; |
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.
I wonder, is there a time-of-check to time-of-use bug here on the length? Not one for this PR, but a follow up I will try not to forget...
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.
Yup I think that's a potential TOCTOU bug here. This particular implementation assumes the user ensures proper synchronization if they intend to use the iterator in a multi-threaded or mutable environment.
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.
I think the current implementation of next
also has a potential TOCTOU bug:
https://github.com/PyO3/pyo3/blob/main/src/types/list.rs#L495-L498
Thanks for working on this! Hopefully my PR should be merged in the next few days and then you can rebase this. |
It might also make sense to implement advance_by on nightly. |
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.
See inline comments, I think there are some issues in how this is set up for the limited API. I also think we should define nth
and nth_back
for all non-nightly builds. I also don't think it's necessary to call get_item
in advance_by
, per my reading of the advance_by
docs.
@ngoldbaum Can I seek your review again ? I've adopted your suggested changes above the CI |
GitHub's interface doesn't help by putting a big red x next to it, but this is fine and we can merge PRs as long as the "conclusion" job finishes. Right now it's broken because of an upstream bug in clippy, so it will probably stay that way until the beta channel gets the fix. I'll try to give this PR another once-over today but failing that will get to it next week. |
src/types/list.rs
Outdated
@@ -750,6 +849,32 @@ impl<'py> Iterator for BoundListIterator<'py> { | |||
None | |||
}) | |||
} | |||
|
|||
#[inline] | |||
#[cfg(all(not(Py_LIMITED_API), feature = "nightly"))] |
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.
#[cfg(all(not(Py_LIMITED_API), feature = "nightly"))] | |
#[cfg(feature = "nightly")] |
Unless I'm missing something...
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.
I feel we need the flag not(Py_LIMITED_API
. Otherwise we will compile the code without access to with_critical_section
https://github.com/PyO3/pyo3/actions/runs/12769998628/job/35593851381?pr=4810
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.
I think you can delete the #[cfg(not(Py_LIMITED_API))]
on BoundListIterator::with_critical_section
, it was only there to avoid a clippy lint about unused code. pyo3::sync::with_critical_section
is unconditionally exposed in the PyO3 API, it's just a no-op on GIL-enabled builds. We wanted to allow people to write code that avoids conditional compilation, where possible.
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.
Thanks. I deleted #[cfg(not(Py_LIMITED_API))]
but was hitting the unused code lint warning, so I put in a allow lint dead_code. Lemme know your thoughts.
I also pushed a few commits and implemented advance_back_by
.
src/types/list.rs
Outdated
@@ -547,6 +547,46 @@ impl<'py> BoundListIterator<'py> { | |||
} | |||
} | |||
|
|||
#[inline] | |||
#[cfg(all(not(Py_LIMITED_API), feature = "nightly"))] |
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.
I'm confused - shouldn't this only be defined on not(feature = "nightly")
? Because on nightly this is defined in terms of advance_by
in the standard library implementation, so we don't need to override nth
. It'll just do the right thing on nightly by calling advance_by
and then next()
. On not nightly you need this override because there's no way to override advance_by
.
Ditto for all the other BoundListIterator
methods.
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.
Indeed - I spent hours reading the stdlib again to figure out that on stable toolchain, we should override nth
, and override advance_by
on nightly. Thanks for spotting this. I've made the changes to compile it on stable
src/types/list.rs
Outdated
|
||
#[inline] | ||
#[cfg(all(Py_LIMITED_API, feature = "nightly"))] | ||
#[deny(unsafe_op_in_unsafe_fn)] |
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.
#[deny(unsafe_op_in_unsafe_fn)] |
there's no unsafe code in this function so this is unnecessary
src/types/list.rs
Outdated
) -> Option<Bound<'py, PyAny>> { | ||
let length = length.0.min(list.len()); | ||
let target_index = index.0 + n; | ||
if index.0 + n < length { |
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.
if index.0 + n < length { | |
if target_index < length { |
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.
Thanks & revised
src/types/list.rs
Outdated
) -> Option<Bound<'py, PyAny>> { | ||
let length = length.0.min(list.len()); | ||
let target_index = index.0 + n; | ||
if index.0 + n < length { |
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.
if index.0 + n < length { | |
if target_index < length { |
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.
Thanks & revised
src/types/list.rs
Outdated
@@ -625,6 +704,26 @@ impl<'py> Iterator for BoundListIterator<'py> { | |||
} | |||
} | |||
|
|||
#[inline] | |||
#[cfg(feature = "nightly")] |
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.
#[cfg(feature = "nightly")] | |
#[cfg(not(feature = "nightly"))] |
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.
Thanks & revised
src/types/list.rs
Outdated
#[cfg(not(Py_LIMITED_API))] | ||
{ | ||
self.with_critical_section(|index, length, list| unsafe { | ||
Self::nth_unchecked(index, length, list, n) | ||
}) | ||
} | ||
#[cfg(Py_LIMITED_API)] | ||
{ | ||
let Self { | ||
index, | ||
length, | ||
list, | ||
} = self; | ||
Self::nth(index, length, list, n) | ||
} |
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.
#[cfg(not(Py_LIMITED_API))] | |
{ | |
self.with_critical_section(|index, length, list| unsafe { | |
Self::nth_unchecked(index, length, list, n) | |
}) | |
} | |
#[cfg(Py_LIMITED_API)] | |
{ | |
let Self { | |
index, | |
length, | |
list, | |
} = self; | |
Self::nth(index, length, list, n) | |
} | |
self.with_critical_section(|index, length, list| unsafe { | |
Self::nth(index, length, list, n) | |
}) |
If you implement my suggestion to only have BoundListIterator::nth
then you can simplify this a lot.
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.
and you can do a similar refactor for the nth_back
implementation for the impl DoubleEndedIterator for BoundListIterator
block below
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.
Thanks. I've adopted this idea for nth
and nth_back
src/types/list.rs
Outdated
) -> Option<Bound<'py, PyAny>> { | ||
let length_size = length.0.min(list.len()); | ||
if index.0 + n < length_size { | ||
let target_index = length_size - n - 1; |
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.
Personally this logic feels a little backwards to me, and I would probably prefer to do this using signed integers and testing if the index is less than zero. That said, this is totally equivalent and if it makes sense to you as-is then no need to change it.
… for proper compilation
Just realize that I forgot to implement |
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.
Nice, much simpler now!
I'm not going to merge immediately because I'd like someone with a little bit more experience maintaining PyO3 to give this a once-over.
newsfragments/4810.added.md
Outdated
@@ -0,0 +1 @@ | |||
Optimizes `nth` and `nth_back` for `BoundListIterator` |
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.
maybe also mention that advance_by
and advance_by_back
are implemented on nightly
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.
Thanks, this looks great to me! Sorry for the slow review cycles 😬
* Optimize nth and nth_back for BoundListIterator. Add unit test and benchmarks * Fix fmt and newsfragment CI * Fix clippy and changelog CI * Revise Impl of nth and nth_back. Impl advance_by * Fix failing fmt * Fix failing ruff test * branch out nth, nth_unchecked, nth_back, nth_back_unchecked. * Fix fmt * Revise advance_by impl. add advance_by unittest. * Fix fmt * Fix clippy unused function warning * Set appropriate Py_LIMITED_API flag * Rewrite nth & nth_back using conditional compilation. Rearrange flags for proper compilation * fix fmt * fix failing CI * Impl advance_back_by. Remove cfg flag for with_critical_section * refactor advance_by and advance_back_by. Add back cfg for with_critical_section * Put allow deadcode for with_critical_section * Remove use of get_item. Revise changelog
See #4787
This PR optimizes
nth
andnth_back
forBoundListIterator
, and added unittest & benchmarks for these 2 APIs. Here are the benchmark of the optimizednth
andnth_back
:On Stable toolchain
With Optimization
The default
nth
andnth_back
implementationOn nightly toolchain
With Optimization
The default
nth
andnth_back
implementation