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

Specialize TrustedLen for Iterator::unzip() #123253

Merged
merged 1 commit into from
Jul 7, 2024

Conversation

ChayimFriedman2
Copy link
Contributor

Don't check the capacity every time (and also for Extend for tuples, as this is how unzip() is implemented).

I did this with an unsafe method on Extend that doesn't check for growth (extend_one_unchecked()). I've marked it as perma-unstable currently, although we may want to expose it in the future so collections outside of std can benefit from it. Then specialize Extend for (A, B) for TrustedLen to call it.

An alternative way of implementing this is to have a semi-public trait (#[doc(hidden)] public, so collections outside of core can implement it) for extend() inside tuples, and specialize it from collections. However:

  1. This looks more complex to me.
  2. This prohibits the option of exposing this somewhen to collections outside of std, as we never expose specializations.

A concern that may arise with the current approach is that implementing extend_one_unchecked() correctly must also incur implementing extend_reserve(), otherwise you can have UB. This is a somewhat non-local safety invariant. However, I believe this is fine, since to have actual UB you must have unsafe code inside your extend_one_unchecked() that makes incorrect assumption, and not implement extend_reserve(). I've also documented this requirement.

Benchmark:

Code:

#[bench]
fn unzip(b: &mut Bencher) {
    b.iter(|| {
        for _ in 0..10_000 {
            let v: (Vec<_>, VecDeque<_>) =
                (black_box(0u32)..black_box(1_000)).map(|i| (i, i * 2)).unzip();
            black_box(v);
        }
    });
}

Before:

unzip::unzip 14.17ms/iter  +/- 374.85µs

After:

unzip::unzip 5.33ms/iter  +/- 164.54µs

@rustbot
Copy link
Collaborator

rustbot commented Mar 31, 2024

r? @scottmcm

rustbot has assigned @scottmcm.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Mar 31, 2024
@scottmcm
Copy link
Member

scottmcm commented Apr 3, 2024

This feels like a
r? @the8472
kind of PR :)

@rustbot rustbot assigned the8472 and unassigned scottmcm Apr 3, 2024
library/core/src/iter/traits/collect.rs Outdated Show resolved Hide resolved
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Apr 8, 2024
ChayimFriedman2 added a commit to ChayimFriedman2/rust that referenced this pull request Apr 10, 2024
Don't check the capacity every time (and also for `Extend` for tuples, as this is how `unzip()` is implemented).

I did this with a semi-public (`#[doc(hidden)]` public, so collections outside of core can implement it) specialization trait that has a method (`extend_one_unchecked()`) that doesn't check for growth. Then specialize `Extend for (A, B)` for `TrustedLen` to call it, and specialize it for collections (only `Vec` and `VecDequq` from the collection in the standard library could benefit from this)

An alternative way of implementing this is to have this method on `Extend` directly, but this was rejected by @the8472 in a review (rust-lang#123253 (comment)).

I didn't mark the trait as unsafe; a concern that may arise from that is that implementing `extend_one_unchecked()` correctly must also incur implementing `extend_reserve()`, otherwise you can have UB. This is a somewhat non-local safety invariant. However, I believe this is fine, since to have actual UB you must have unsafe code inside your `extend_one_unchecked()` that makes incorrect assumption, *and* not implement `extend_reserve()`. I've also documented this requirement.
ChayimFriedman2 added a commit to ChayimFriedman2/rust that referenced this pull request Apr 10, 2024
Don't check the capacity every time (and also for `Extend` for tuples, as this is how `unzip()` is implemented).

I did this with a semi-public (`#[doc(hidden)]` public, so collections outside of core can implement it) specialization trait that has a method (`extend_one_unchecked()`) that doesn't check for growth. Then specialize `Extend for (A, B)` for `TrustedLen` to call it, and specialize it for collections (only `Vec` and `VecDequq` from the collection in the standard library could benefit from this)

An alternative way of implementing this is to have this method on `Extend` directly, but this was rejected by @the8472 in a review (rust-lang#123253 (comment)).

I didn't mark the trait as unsafe; a concern that may arise from that is that implementing `extend_one_unchecked()` correctly must also incur implementing `extend_reserve()`, otherwise you can have UB. This is a somewhat non-local safety invariant. However, I believe this is fine, since to have actual UB you must have unsafe code inside your `extend_one_unchecked()` that makes incorrect assumption, *and* not implement `extend_reserve()`. I've also documented this requirement.
@Dylan-DPC Dylan-DPC added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels May 5, 2024
Copy link
Member

@the8472 the8472 left a comment

Choose a reason for hiding this comment

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

Mostly looks fine to me and I can't think of a better approach given the current specialization limitations.

Just a few minor points

library/core/src/iter/traits/collect.rs Show resolved Hide resolved
Comment on lines 528 to 571
if lower_bound > 0 {
a.extend_reserve(lower_bound);
b.extend_reserve(lower_bound);
}

iter.fold((), extend(a, b));
Copy link
Member

Choose a reason for hiding this comment

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

This does essentially the same as the specialized impl, the only difference is the case where the upper bound exceeds usize.

They could be unified (just doing the panic separately) or the default impl could be more like Vec's extend_desugared, but that doesn't have to happen in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't see a reason to unify them. It will take more work and LOC that the few duplicated LOC.

library/core/src/iter/traits/collect.rs Outdated Show resolved Hide resolved
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 1, 2024
Don't check the capacity every time (and also for `Extend` for tuples, as this is how `unzip()` is implemented).

I did this with an unsafe method on `Extend` that doesn't check for growth (`extend_one_unchecked()`). I've marked it as perma-unstable currently, although we may want to expose it in the future so collections outside of std can benefit from it. Then specialize `Extend for (A, B)` for `TrustedLen` to call it.

It may seem that an alternative way of implementing this is to have a semi-public trait (`#[doc(hidden)]` public, so collections outside of core can implement it) for `extend()` inside tuples, and specialize it from collections. However, it is impossible due to limitations of `min_specialization`.

A concern that may arise with the current approach is that implementing `extend_one_unchecked()` correctly must also incur implementing `extend_reserve()`, otherwise you can have UB. This is a somewhat non-local safety invariant. However, I believe this is fine, since to have actual UB you must have unsafe code inside your `extend_one_unchecked()` that makes incorrect assumption, *and* not implement `extend_reserve()`. I've also documented this requirement.
@ChayimFriedman2
Copy link
Contributor Author

@rustbot ready

Did the requested changes.

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jul 7, 2024
@the8472
Copy link
Member

the8472 commented Jul 7, 2024

unzip is used in a few places in the compiler, so it might affect perf results

@bors r+ rollup=never

@bors
Copy link
Contributor

bors commented Jul 7, 2024

📌 Commit 54556f4 has been approved by the8472

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 7, 2024
@bors
Copy link
Contributor

bors commented Jul 7, 2024

⌛ Testing commit 54556f4 with merge 382148d...

@bors
Copy link
Contributor

bors commented Jul 7, 2024

☀️ Test successful - checks-actions
Approved by: the8472
Pushing 382148d to master...

@bors bors added the merged-by-bors This PR was explicitly merged by bors. label Jul 7, 2024
@bors bors merged commit 382148d into rust-lang:master Jul 7, 2024
7 checks passed
@rustbot rustbot added this to the 1.81.0 milestone Jul 7, 2024
@ChayimFriedman2 ChayimFriedman2 deleted the extend-trusted branch July 7, 2024 14:07
@rust-timer
Copy link
Collaborator

Finished benchmarking commit (382148d): comparison URL.

Overall result: no relevant changes - no action needed

@rustbot label: -perf-regression

Instruction count

This benchmark run did not return any relevant results for this metric.

Max RSS (memory usage)

Results (primary 2.0%, secondary 3.4%)

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
2.0% [1.4%, 2.5%] 4
Regressions ❌
(secondary)
3.4% [2.8%, 3.9%] 3
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) 2.0% [1.4%, 2.5%] 4

Cycles

Results (secondary -3.3%)

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-3.3% [-3.6%, -3.0%] 2
All ❌✅ (primary) - - 0

Binary size

This benchmark run did not return any relevant results for this metric.

Bootstrap: 699.867s -> 699.903s (0.01%)
Artifact size: 328.42 MiB -> 328.38 MiB (-0.01%)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merged-by-bors This PR was explicitly merged by bors. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants