Skip to content
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

Implement a specialized version std::iter::Enumerate for TrustedLen #77822

Closed
wants to merge 3 commits into from
Closed
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
168 changes: 152 additions & 16 deletions library/core/src/iter/adapters/enumerate.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::intrinsics;
use crate::iter::adapters::{zip::try_get_unchecked, SourceIter, TrustedRandomAccess};
use crate::iter::{FusedIterator, InPlaceIterable, TrustedLen};
use crate::ops::{Add, AddAssign, Try};
Expand All @@ -15,10 +16,155 @@ use crate::ops::{Add, AddAssign, Try};
pub struct Enumerate<I> {
iter: I,
count: usize,
len: usize,
}
impl<I> Enumerate<I> {
impl<I: Iterator> Enumerate<I> {
m-ou-se marked this conversation as resolved.
Show resolved Hide resolved
pub(in crate::iter) fn new(iter: I) -> Enumerate<I> {
Enumerate { iter, count: 0 }
EnumerateImpl::new(iter)
}
}

/// Enumerate specialization trait
///
/// This exists to work around https://bugs.llvm.org/show_bug.cgi?id=48965. It can be removed again
/// once this is solved in LLVM and the implementation of the trait functions can be folded again
/// into the corresponding functions on `Enumerate` based on the default implementation.
///
/// The trait is implemented via specialization on any iterator that implements `TrustedRandomAccess`
/// to provide the information about the maximum value this iterator can return to the optimizer.
/// Specifically, for slices this allows the optimizer to know that the returned values are never
/// bigger than the size of the slice.
///
/// The only difference between the default and specialized implementation is the use of
/// `intrinsics::assume()` on the to be returned values, and both implementations must be kept in
/// sync.
#[doc(hidden)]
trait EnumerateImpl<I> {
type Item;
fn new(iter: I) -> Self;
fn next(&mut self) -> Option<Self::Item>;
unsafe fn __iterator_get_unchecked(&mut self, idx: usize) -> Self::Item
where
Self: TrustedRandomAccess;
fn next_back(&mut self) -> Option<Self::Item>
where
I: ExactSizeIterator + DoubleEndedIterator;
}

impl<I> EnumerateImpl<I> for Enumerate<I>
where
I: Iterator,
{
type Item = (usize, I::Item);

default fn new(iter: I) -> Self {
Enumerate {
iter,
count: 0,
len: 0, // unused
}
}

#[inline]
default fn next(&mut self) -> Option<Self::Item> {
let a = self.iter.next()?;
let i = self.count;
// Possible undefined overflow. By directly calling the trait method instead of using the
// `+=` operator the decision about overflow checking is delayed to the crate that does code
// generation, even if overflow checks are disabled for the current crate. This is
// especially useful because overflow checks are usually disabled for the standard library.
AddAssign::add_assign(&mut self.count, 1);
Some((i, a))
}

#[inline]
default unsafe fn __iterator_get_unchecked(&mut self, idx: usize) -> Self::Item
where
Self: TrustedRandomAccess,
{
// SAFETY: the caller must uphold the contract for
// `Iterator::__iterator_get_unchecked`.
let value = unsafe { try_get_unchecked(&mut self.iter, idx) };
// See comment in `next()` for the reason why `Add::add()` is used here instead of `+`.
(Add::add(self.count, idx), value)
}

#[inline]
default fn next_back(&mut self) -> Option<Self::Item>
where
I: ExactSizeIterator + DoubleEndedIterator,
{
let a = self.iter.next_back()?;
let len = self.iter.len();
// Can safely add, `ExactSizeIterator` promises that the number of
// elements fits into a `usize`.
Some((self.count + len, a))
}
}

// This is the same code as above but using `intrinsics::assume()` to hint at the compiler
// that the returned index is smaller than the length of the underlying iterator.
//
// This could be bound to `TrustedLen + ExactSizeIterator` or `TrustedRandomAccess` to guarantee
// that the number of elements fits into an `usize` and that the returned length is actually the
// real length. `TrustedRandomAccess` was selected because specialization on `ExactSizeIterator` is
// not possible (yet?).
Comment on lines +108 to +111
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthewjasper Could you comment on the possibilities here? Specializing on ExactSizeIterator would be useful here, but is not accepted. I see #[rustc_specialization_trait] and #[rustc_unsafe_specialization_marker] being used in some places to work around this, but I don't know in which cases those can be used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering that TrustedLen is unsafe + unstable + rustc_unsafe_specialization_marker it is more or less implied that any type that implements it is some iterator owned by the standard library. Which means there are no lifetime shenanigans that would make ExactSizeIterator unsafe for min_specialization.
Using that justification a rustc_unsafe_specialization_marker helper trait inheriting from both should be fine. Probably.

On the other hand TrustedRandomAccess is extending its tendrils into ever more places, so soon it should be almost equivalent in power to TrustedLen + ExactSizeIterator.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TrustedRandomAccess seems like it would always be more specific than TrustedLen+ExactSizeIterator. Implementing the former on things like BTreeMap would probably be not a good idea, while TrustedLen+ExactSizeIterator would be easy.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then yeah, I think that should be possible. But it might be good to get a second opinion from someone who has better understanding of the min_specialization rules.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering the progress that happened in the LLVM bug report and all the issues found recently involving TrustedRandomAccess, maybe let's just wait this out until LLVM handles it correctly?

What do you think @m-ou-se ?

impl<I> EnumerateImpl<I> for Enumerate<I>
where
I: TrustedRandomAccess + Iterator,
{
fn new(iter: I) -> Self {
let len = iter.size();

Enumerate { iter, count: 0, len }
}

#[inline]
fn next(&mut self) -> Option<Self::Item> {
let a = self.iter.next()?;
// SAFETY: There must be fewer than `self.len` items because of `TrustedLen`'s API contract
unsafe {
intrinsics::assume(self.count < self.len);
}
let i = self.count;
// See comment in `next()` of the default implementation for the reason why
// `AddAssign::add_assign()` is used here instead of `+=`.
AddAssign::add_assign(&mut self.count, 1);
Some((i, a))
}

#[inline]
unsafe fn __iterator_get_unchecked(&mut self, idx: usize) -> Self::Item
where
Self: TrustedRandomAccess,
{
// SAFETY: the caller must uphold the contract for
// `Iterator::__iterator_get_unchecked`.
let value = unsafe { try_get_unchecked(&mut self.iter, idx) };
// See comment in `next()` for the reason why `Add::add()` is used here instead of `+`.
let idx = Add::add(self.count, idx);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this use Add::add instead of +? If this is necessary, a comment explaining why would be useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's how it was originally and is everywhere else in this file. I don't know why and was wondering the same, but kept it because there presumably was a reason

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Add::add delays decision about overflow check mode to the crate that does the code generation, even if overflow checks are disabled in the current crate, which they usually are for the standard library.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to #81721, we should be using #[rustc_inherit_overflow_checks] with a regular + operator here instead of the Add:add trick.

// SAFETY: There must be fewer than `self.len` items because of `TrustedLen`'s API contract
unsafe {
intrinsics::assume(idx < self.len);
}
(idx, value)
}

#[inline]
fn next_back(&mut self) -> Option<Self::Item>
where
I: ExactSizeIterator + DoubleEndedIterator,
{
let a = self.iter.next_back()?;
let len = self.iter.len();
// Can safely add, `ExactSizeIterator` promises that the number of
// elements fits into a `usize`.
let idx = self.count + len;
// SAFETY: There must be fewer than `self.len` items because of `TrustedLen`'s API contract
unsafe {
intrinsics::assume(idx < self.len);
}
Some((idx, a))
}
}

Expand All @@ -40,11 +186,7 @@ where
/// Might panic if the index of the element overflows a `usize`.
#[inline]
fn next(&mut self) -> Option<(usize, <I as Iterator>::Item)> {
let a = self.iter.next()?;
let i = self.count;
// Possible undefined overflow.
AddAssign::add_assign(&mut self.count, 1);
Some((i, a))
EnumerateImpl::next(self)
}

#[inline]
Expand Down Expand Up @@ -114,10 +256,8 @@ where
where
Self: TrustedRandomAccess,
{
// SAFETY: the caller must uphold the contract for
// `Iterator::__iterator_get_unchecked`.
let value = unsafe { try_get_unchecked(&mut self.iter, idx) };
(Add::add(self.count, idx), value)
// SAFETY: Just forwarding to the actual implementation.
unsafe { EnumerateImpl::__iterator_get_unchecked(self, idx) }
}
}

Expand All @@ -128,11 +268,7 @@ where
{
#[inline]
fn next_back(&mut self) -> Option<(usize, <I as Iterator>::Item)> {
let a = self.iter.next_back()?;
let len = self.iter.len();
// Can safely add, `ExactSizeIterator` promises that the number of
// elements fits into a `usize`.
Some((self.count + len, a))
EnumerateImpl::next_back(self)
}

#[inline]
Expand Down
3 changes: 2 additions & 1 deletion library/core/src/iter/adapters/zip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,8 @@ impl<A: Debug + TrustedRandomAccess, B: Debug + TrustedRandomAccess> ZipFmt<A, B
///
/// # Safety
///
/// The iterator's `size_hint` must be exact and cheap to call.
/// The iterator's `size_hint` must be exact and cheap to call, which also
/// means that the number of items must fit into an `usize`.
///
/// `size` may not be overridden.
///
Expand Down