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

Option::as_(mut_)slice and ::into_slice #150

Open
llogiq opened this issue Dec 18, 2022 · 16 comments
Open

Option::as_(mut_)slice and ::into_slice #150

llogiq opened this issue Dec 18, 2022 · 16 comments
Labels
api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api

Comments

@llogiq
Copy link

llogiq commented Dec 18, 2022

Proposal

Problem statement

Currently when one flat_maps over a mixture of say Vecs and Options, there is a hard to find method to create a slice from the latter, that generates suboptimal code.

Motivation, use-cases

Consider the following code that is similar to what I came up to when a colleague had the problem of iterating over either an Option or a Vec reference (the colleague, not knowing about slice::from_ref, turned to itertools::Either):

fn all_tasks(task_containers: &[TaskContainer]) -> impl Iterator<Task> + '_ {
    task_containers
        .iter()
        .flat_map(|tc| match tc.tasks() {
            UpToOne(Some(o)) => std::slice::from_ref(o),
            UpToOne(None) => &[],
            Many(v) => v.as_slice(),
        })
}

Not only is the std::slice::from_ref function hard to find, in this case the match introduces a needless branch.

Solution sketches

The solution is to add an as_slice and as_slice_mut method to Option<T>, which can create a slice by copying the data. The methods follow standard API conventions and are negative-cost abstractions (because they are faster than what a normal user would have written). For the sake of completeness, another into_slice method on both Option<&T> and Option<&mut T> is advised.

Note that removing the branch compared to match + slice::from_ref requires knowing the offset of the value of a Some(_) variant. Currently we have two cases: Either the offset is zero because of niche optimization, or we can get the offset via an Option<MaybeUninit<T>>, because they are equal with respect to memory layout (I have checked and confirmed that the current layout randomization implementation only works on each variant in isolation and then prepends the discriminant, so this property should hold, and if not, the tests will catch it).

Should this change and we get another case, where neither the layout of Option<T> is equal to that of Option<MaybeUninit<T>> nor the niche optimization applies, we would temporarily revert to the zero-cost abstraction using a match + from_ref until we have an offset_of! macro (suggested in this RFC proposal) that also covers enum variants. It is advisable to change the implementation to offset_of! once available anyway.

Links and related work

PR #105871 has a suggested implementation and links to zulip discussion

@llogiq llogiq added api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api labels Dec 18, 2022
@scottmcm
Copy link
Member

scottmcm commented Dec 19, 2022

As another way of thinking of this: Option<T> is iterable, thus it can be thought of as a container. It's, essentially, an ArrayVec<T, 1>, just with domain-specific method names (like take instead of pop). And thus it makes sense for it to have the same as_slice and as_mut_slice methods as Vec.

@llogiq llogiq changed the title Option::as_slice(_mut) and ::into_slice Option::as_(mut_)slice and ::into_slice Dec 19, 2022
@llogiq
Copy link
Author

llogiq commented Dec 19, 2022

One open question is whether to split the into_slice methods by mutability, having the mutable one named into_mut_slice. While this would slightly broaden the API surface, it would reduce the ambiguity for type inference which will arise when using None.

@llogiq llogiq changed the title Option::as_(mut_)slice and ::into_slice Option::as_(mut_)slice and ::into_slice Jan 8, 2023
@scottmcm
Copy link
Member

For another motivating example, I happened to notice this in the compiler today:

https://github.com/rust-lang/rust/blob/65d2f2a5f9c323c88d1068e8e90d0b47a20d491c/compiler/rustc_hir_typeck/src/op.rs#L748-L749

which I'm pretty sure is the Option<&T>&[T] under discussion here.

@llogiq
Copy link
Author

llogiq commented Jan 19, 2023

@scottmcm
Copy link
Member

I'd suggest leaving it alone for now. PRs are easier if they're just library, or just compiler, etc.

One can always make a follow-up PR later.

@llogiq
Copy link
Author

llogiq commented Feb 16, 2023

cc @Amanieu

@Amanieu
Copy link
Member

Amanieu commented Feb 19, 2023

I think this is a good API to add. Seconded.

@Amanieu Amanieu added the initial-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. label Feb 19, 2023
@llogiq
Copy link
Author

llogiq commented Feb 21, 2023

Thanks @Amanieu. Do you have an opinion on the open question? Should we have into_mut_slice instead of into_slice on Option<&mut T> to avoid inference failures on code like foo.into_slice() where foo's type is not completely specified?

@Amanieu
Copy link
Member

Amanieu commented Feb 21, 2023

Hmm I'm actually less certain about the into_slice* methods:

  • we can't really optimize those beyond the naive implementation.
  • they aren't as generally useful as the as_slice* methods.
  • the name of the method is really non-obvious.

Perhaps the into_slice* methods should be removed entirely?

@cuviper
Copy link
Member

cuviper commented Feb 21, 2023

For reference, here's where the discussion started for into_slice:
https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/Option.3A.3Aas_slice/near/316374731

I still feel it's well motivated, since you can't go from Option<&T> to &Option<T> to get as_slice methods -- but the optimization only works on the latter. So into_slice is just for completeness / convenience.

@llogiq
Copy link
Author

llogiq commented Feb 22, 2023

I agree there's no perf benefit to having them, and .map_or(&[], slice::from_ref()) (or from_mut, respectively) isn't too bad. Adding some verbiage to the documentation to steer people towards that solution should be perfectly usable even for beginners without needlessly extending our API surface. I'll strike the into_slice part from the description above and remove it from the implementation PR.

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Mar 1, 2023
Add `Option::as_`(`mut_`)`slice`

This adds the following functions:

* `Option<T>::as_slice(&self) -> &[T]`
* `Option<T>::as_mut_slice(&mut self) -> &[T]`

The `as_slice` and `as_mut_slice_mut` functions benefit from an optimization that makes them completely branch-free. ~~Unfortunately, this optimization is not available on by-value Options, therefore the `into_slice` implementations use the plain `match` + `slice::from_ref` approach.~~

Note that the optimization's soundness hinges on the fact that either the niche optimization makes the offset of the `Some(_)` contents zero or the mempory layout of `Option<T>` is equal to that of `Option<MaybeUninit<T>>`.

The idea has been discussed on [Zulip](https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/Option.3A.3Aas_slice). Notably the idea for the `as_slice_mut` and `into_slice´ methods came from `@cuviper` and `@Sp00ph` hardened the optimization against niche-optimized Options.

The [rust playground](https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=74f8e4239a19f454c183aaf7b4a969e0) shows that the generated assembly of the optimized method is basically only a copy while the naive method generates code containing a `test dx, dx` on x86_64.

---

EDIT from reviewer: ACP is rust-lang/libs-team#150
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Mar 1, 2023
Add `Option::as_`(`mut_`)`slice`

This adds the following functions:

* `Option<T>::as_slice(&self) -> &[T]`
* `Option<T>::as_mut_slice(&mut self) -> &[T]`

The `as_slice` and `as_mut_slice_mut` functions benefit from an optimization that makes them completely branch-free. ~~Unfortunately, this optimization is not available on by-value Options, therefore the `into_slice` implementations use the plain `match` + `slice::from_ref` approach.~~

Note that the optimization's soundness hinges on the fact that either the niche optimization makes the offset of the `Some(_)` contents zero or the mempory layout of `Option<T>` is equal to that of `Option<MaybeUninit<T>>`.

The idea has been discussed on [Zulip](https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/Option.3A.3Aas_slice). Notably the idea for the `as_slice_mut` and `into_slice´ methods came from ``@cuviper`` and ``@Sp00ph`` hardened the optimization against niche-optimized Options.

The [rust playground](https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=74f8e4239a19f454c183aaf7b4a969e0) shows that the generated assembly of the optimized method is basically only a copy while the naive method generates code containing a `test dx, dx` on x86_64.

---

EDIT from reviewer: ACP is rust-lang/libs-team#150
bors added a commit to rust-lang-ci/rust that referenced this issue Mar 1, 2023
Add `Option::as_`(`mut_`)`slice`

This adds the following functions:

* `Option<T>::as_slice(&self) -> &[T]`
* `Option<T>::as_mut_slice(&mut self) -> &[T]`

The `as_slice` and `as_mut_slice_mut` functions benefit from an optimization that makes them completely branch-free. ~~Unfortunately, this optimization is not available on by-value Options, therefore the `into_slice` implementations use the plain `match` + `slice::from_ref` approach.~~

Note that the optimization's soundness hinges on the fact that either the niche optimization makes the offset of the `Some(_)` contents zero or the mempory layout of `Option<T>` is equal to that of `Option<MaybeUninit<T>>`.

The idea has been discussed on [Zulip](https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/Option.3A.3Aas_slice). Notably the idea for the `as_slice_mut` and `into_slice´ methods came from `@cuviper` and `@Sp00ph` hardened the optimization against niche-optimized Options.

The [rust playground](https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=74f8e4239a19f454c183aaf7b4a969e0) shows that the generated assembly of the optimized method is basically only a copy while the naive method generates code containing a `test dx, dx` on x86_64.

---

EDIT from reviewer: ACP is rust-lang/libs-team#150
@scottmcm
Copy link
Member

scottmcm commented Mar 1, 2023

Hmm, a bigger survey might help for deciding on into_slice.

Of the 4 examples mentioned upthread, it appears that at least 3 of them are going through as_ref, and thus could plausibly work using as_slice:

https://github.com/rust-lang/rust/blob/65d2f2a5f9c323c88d1068e8e90d0b47a20d491c/compiler/rustc_hir_typeck/src/fn_ctxt/checks.rs#L1432-L1440

https://github.com/rust-lang/rust/blob/65d2f2a5f9c323c88d1068e8e90d0b47a20d491c/src/bootstrap/builder.rs#L865

https://github.com/rust-lang/rust/blob/65d2f2a5f9c323c88d1068e8e90d0b47a20d491c/compiler/rustc_hir_typeck/src/op.rs#L748-L749

I don't know if there's also a bunch of places that want what into_slice would be doing.

saethlin pushed a commit to saethlin/miri that referenced this issue Mar 5, 2023
Add `Option::as_`(`mut_`)`slice`

This adds the following functions:

* `Option<T>::as_slice(&self) -> &[T]`
* `Option<T>::as_mut_slice(&mut self) -> &[T]`

The `as_slice` and `as_mut_slice_mut` functions benefit from an optimization that makes them completely branch-free. ~~Unfortunately, this optimization is not available on by-value Options, therefore the `into_slice` implementations use the plain `match` + `slice::from_ref` approach.~~

Note that the optimization's soundness hinges on the fact that either the niche optimization makes the offset of the `Some(_)` contents zero or the mempory layout of `Option<T>` is equal to that of `Option<MaybeUninit<T>>`.

The idea has been discussed on [Zulip](https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/Option.3A.3Aas_slice). Notably the idea for the `as_slice_mut` and `into_slice´ methods came from `@cuviper` and `@Sp00ph` hardened the optimization against niche-optimized Options.

The [rust playground](https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=74f8e4239a19f454c183aaf7b4a969e0) shows that the generated assembly of the optimized method is basically only a copy while the naive method generates code containing a `test dx, dx` on x86_64.

---

EDIT from reviewer: ACP is rust-lang/libs-team#150
@llogiq
Copy link
Author

llogiq commented Mar 5, 2023

I could write a lint for the various as_slice/into_slice patterns and run lintcheck on various crates to see whether there are more cases in the wild.

@llogiq
Copy link
Author

llogiq commented Mar 10, 2023

In the meantime I tried github code search to find a few more motivating examples. Here's what I found in a few minutes of searching:

repo file line replace with
maekawatoshiki/sericum src/ir/opcode.rs 373 as_slice
maekawatoshiki/sericum src/ir/opcode.rs 387 as_mut_slice
avr-rust/rust-legacy-fork src/librustc/mir/mod.rs 1333 as_slice
vercel/turbo crates/turborepo-scm/src/git.rs 215 as_slice

None of those would fit into_slice though. So perhaps our bid for completeness is actually not that useful.

@joshtriplett joshtriplett removed the initial-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. label May 20, 2023
thomcc pushed a commit to tcdi/postgrestd that referenced this issue May 31, 2023
Add `Option::as_`(`mut_`)`slice`

This adds the following functions:

* `Option<T>::as_slice(&self) -> &[T]`
* `Option<T>::as_mut_slice(&mut self) -> &[T]`

The `as_slice` and `as_mut_slice_mut` functions benefit from an optimization that makes them completely branch-free. ~~Unfortunately, this optimization is not available on by-value Options, therefore the `into_slice` implementations use the plain `match` + `slice::from_ref` approach.~~

Note that the optimization's soundness hinges on the fact that either the niche optimization makes the offset of the `Some(_)` contents zero or the mempory layout of `Option<T>` is equal to that of `Option<MaybeUninit<T>>`.

The idea has been discussed on [Zulip](https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/Option.3A.3Aas_slice). Notably the idea for the `as_slice_mut` and `into_slice´ methods came from `@cuviper` and `@Sp00ph` hardened the optimization against niche-optimized Options.

The [rust playground](https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=74f8e4239a19f454c183aaf7b4a969e0) shows that the generated assembly of the optimized method is basically only a copy while the naive method generates code containing a `test dx, dx` on x86_64.

---

EDIT from reviewer: ACP is rust-lang/libs-team#150
@scottmcm
Copy link
Member

as_slice and as_mut_slice stabilized in 1.75 🎉

Are you still interested in having into_slice, @llogiq ?

@llogiq
Copy link
Author

llogiq commented Apr 17, 2024

No, I'm fine with what we have now.

RalfJung pushed a commit to RalfJung/rust-analyzer that referenced this issue Apr 20, 2024
Add `Option::as_`(`mut_`)`slice`

This adds the following functions:

* `Option<T>::as_slice(&self) -> &[T]`
* `Option<T>::as_mut_slice(&mut self) -> &[T]`

The `as_slice` and `as_mut_slice_mut` functions benefit from an optimization that makes them completely branch-free. ~~Unfortunately, this optimization is not available on by-value Options, therefore the `into_slice` implementations use the plain `match` + `slice::from_ref` approach.~~

Note that the optimization's soundness hinges on the fact that either the niche optimization makes the offset of the `Some(_)` contents zero or the mempory layout of `Option<T>` is equal to that of `Option<MaybeUninit<T>>`.

The idea has been discussed on [Zulip](https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/Option.3A.3Aas_slice). Notably the idea for the `as_slice_mut` and `into_slice´ methods came from `@cuviper` and `@Sp00ph` hardened the optimization against niche-optimized Options.

The [rust playground](https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=74f8e4239a19f454c183aaf7b4a969e0) shows that the generated assembly of the optimized method is basically only a copy while the naive method generates code containing a `test dx, dx` on x86_64.

---

EDIT from reviewer: ACP is rust-lang/libs-team#150
RalfJung pushed a commit to RalfJung/rust-analyzer that referenced this issue Apr 27, 2024
Add `Option::as_`(`mut_`)`slice`

This adds the following functions:

* `Option<T>::as_slice(&self) -> &[T]`
* `Option<T>::as_mut_slice(&mut self) -> &[T]`

The `as_slice` and `as_mut_slice_mut` functions benefit from an optimization that makes them completely branch-free. ~~Unfortunately, this optimization is not available on by-value Options, therefore the `into_slice` implementations use the plain `match` + `slice::from_ref` approach.~~

Note that the optimization's soundness hinges on the fact that either the niche optimization makes the offset of the `Some(_)` contents zero or the mempory layout of `Option<T>` is equal to that of `Option<MaybeUninit<T>>`.

The idea has been discussed on [Zulip](https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/Option.3A.3Aas_slice). Notably the idea for the `as_slice_mut` and `into_slice´ methods came from `@cuviper` and `@Sp00ph` hardened the optimization against niche-optimized Options.

The [rust playground](https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=74f8e4239a19f454c183aaf7b4a969e0) shows that the generated assembly of the optimized method is basically only a copy while the naive method generates code containing a `test dx, dx` on x86_64.

---

EDIT from reviewer: ACP is rust-lang/libs-team#150
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api
Projects
None yet
Development

No branches or pull requests

5 participants