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

Simplify lightweight clones, including into closures and async blocks #3680

Open
wants to merge 36 commits into
base: master
Choose a base branch
from

Conversation

joshtriplett
Copy link
Member

@joshtriplett joshtriplett commented Aug 20, 2024

Provide a feature to simplify performing lightweight clones (such as of
Arc/Rc), particularly cloning them into closures or async blocks, while
still keeping such cloning visible and explicit.

A very common source of friction in asynchronous or multithreaded Rust
programming is having to clone various Arc<T> reference-counted objects into
an async block or task. This is particularly common when spawning a closure as
a thread, or spawning an async block as a task. Common patterns for doing so
include:

// Use new names throughout the block
let new_x = x.clone();
let new_y = y.clone();
spawn(async move {
    func1(new_x).await;
    func2(new_y).await;
});

// Introduce a scope to perform the clones in
{
    let x = x.clone();
    let y = y.clone();
    spawn(async move {
        func1(x).await;
        func2(y).await;
    });
}

// Introduce a scope to perform the clones in, inside the call
spawn({
    let x = x.clone();
    let y = y.clone();
    async move {
        func1(x).await;
        func2(y).await;
    }
});

All of these patterns introduce noise every time the program wants to spawn a
thread or task, or otherwise clone an object into a closure or async block.
Feedback on Rust regularly brings up this friction, seeking a simpler solution.

This RFC proposes solutions to minimize the syntactic weight of
lightweight-cloning objects, particularly cloning objects into a closure or
async block, while still keeping an indication of this operation.


This RFC is part of the "Ergonomic ref-counting" project goal, owned by
@jkelleyrtp. Thanks to @jkelleyrtp and @nikomatsakis for reviewing. Thanks to
@nikomatsakis for key insights in this RFC, including the idea to use use.

Rendered

@joshtriplett joshtriplett added T-lang Relevant to the language team, which will review and decide on the RFC. I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. labels Aug 20, 2024
@5225225
Copy link

5225225 commented Aug 20, 2024

Personally, I don't feel that the non-closure/block use cases of this are really strong enough to warrant adding this, and the closure/block use case can be fixed with clone blocks.

The example

let obj: Arc<LargeComplexObject> = new_large_complex_object();
some_function(obj.use); // Pass a separate use of the object to `some_function`
obj.method(); // The object is still owned afterwards

could just be written as some_function(obj.clone()) with the only downsides being "that will still compile even if obj is expensive to clone" (Which is likely more easily solvable through a lint rather than a language feature), and not being able to remove redundant clones.

Which can presumably be solved either by making LLVM smarter about atomics for the specific case of Arc, or having an attribute on a clone impl that gives it the semantics use is being given here (Which would benefit all code that uses that type, not just new code that has been written to use .use)

The ergonomics of needing to clone in a block are annoying though, I agree, but that's a smaller feature by being able to do:

spawn(async clone {
    func1(x).await;
    func2(y).await;
});

and similarly for closures.

@joshtriplett
Copy link
Member Author

joshtriplett commented Aug 20, 2024

the closure/block use case can be fixed with clone blocks

The problem with a clone block/closure is that it would perform both cheap and expensive clones. A use block/closure will only perform cheap clones (e.g. Arc::clone), never expensive ones (e.g. Vec::clone).

Even without the .use syntax, async use blocks and use || closures provide motivation for this.

or having an attribute on a clone impl that gives it the semantics use is being given here (Which would benefit all code that uses that type, not just new code that has been written to use .use)

I think there's potential value there (and I've captured this in the RFC); for instance, we could omit a clone of a String if the original is statically known to be dead. I'd be concerned about changing existing semantics, though, particularly if we don't add a way for users to bypass that elision (which would add more complexity).

@Diggsey
Copy link
Contributor

Diggsey commented Aug 20, 2024

I'm afraid I'm pretty negative about this RFC.

Use trait

I don't like the Use concept as applied here: I don't think it makes sense to tie the concept of a "lightweight clone" to the syntax sugar for cloning values into a closure. Why can't I clone heavy-weight objects into a closure? It seems like an arbitrary distinction imposed by the compiler, when the compiler cannot possibly know what the performance requirements of my code are.

I could imagine there might be scenarios where knowing if a clone is light-weight is useful, but I don't think this is one of them.

.use keyword

I think the suffix .use form is unnecessary when you can already chain .clone(), and it's confusing for new users that .clone() requires brackets, whilst .use does not. Consistency is important. .use does not do anything that couldn't be done via a method, so it should be method - in general the least powerful language construct should be chose, in the same way that you wouldn't use a macro where a function would suffice.

However, x.use does not always invoke Clone::clone(x); in some cases the compiler can optimize away a use.

I don't like the "can" in this statement. Optimizations should fall into one of two camps:

  1. Automatic. These optimizations are based on the "as if" principle - ie. the program executes just as if the optimization was not applied, it just runs faster.
  2. Opt-in. This covers things like guaranteed tail calls, where the programmer says "I want this to be a tail-call" and the compiler returns an error if it can't do it.

Giving the compiler implementation a choice which has program-visible side-effects, and then specifying a complicated set of rules for when it should apply the optimization is just asking for trouble (eg. see C++'s automatic copy elision...) and I don't want to work in a language where different compilers might make the exact same code execute in significantly different ways.

use closure

If any object referenced within the closure or block does not implement Use (including generic types whose bounds do not require Use), the closure or block will attempt to borrow that object instead

I think this fallback is dangerous, as it means that implementing Use for existing types can have far-reaching implications for downstream code, making it a backwards compatibility hazard.

Motivation

Getting back to the original motivation: making reference counting more seamless, I think simply adding a syntax or standard library macro for cloning values into a closure or async block would go a long way to solving the issue... Potentially even all the way.

If a more significant change is needed, then I think this should be a type of variable binding (eg. let auto mut x = ...) where such variables are automatically cloned as necessary, but I hope such a significant change is not needed.

@joshtriplett
Copy link
Member Author

joshtriplett commented Aug 20, 2024

I've added a new paragraph in the "Rationale and alternatives" section explaining why async clone/clone || would not suffice:

Rather than specifically supporting lightweight clones, we could add a syntax
for closures and async blocks to perform any clones (e.g. async clone /
clone ||). This would additionally allow expensive clones (such as
String/Vec). However, we've had many requests to distinguish between
expensive and lightweight clones, as well as ecosystem conventions attempting
to make such distinctions (e.g. past guidance to write Arc::clone/Rc::clone
explicitly). Having a syntax that only permits lightweight clones would allow
users to confidently use that syntax without worrying about an unexpectedly
expensive operation. We can then provide ways to perform the expensive clones
explicitly, such as the use(x = x.clone()) syntax suggested in
[future possibilities][future-possibilities].

@joshtriplett
Copy link
Member Author

joshtriplett commented Aug 20, 2024

@Diggsey wrote:

I don't think it makes sense to tie the concept of a "lightweight clone" to the syntax sugar for cloning values into a closure. Why can't I clone heavy-weight objects into a closure?

You can; I'm not suggesting that we couldn't provide a syntax for that, too. However, people have asked for the ability to distinguish between expensive and lightweight clones. And a lightweight clone is less of a big deal, making it safer to have a lighter-weight syntax and let users mostly not worry about it. We could additionally provide syntax for performing expensive clones; I've mentioned one such syntax in the future work section, but we could consider others as well if that's a common use case.

I think the suffix .use form is unnecessary when you can already chain .clone()

That assumes that users want to call .clone(), rather than calling something that is always lightweight. If we separate out that consideration, then the question of whether this should be .use or a separate (new) trait method is covered in the alternatives section. I think it'd be more unusual to have the elision semantics and attach them to what otherwise looks like an ordinary trait method, but we could do that.

.use does not do anything that couldn't be done via a method, so it should be method

This is only true if we omitted the proposed elision behavior, or if we decide that it's acceptable for methods to have elision semantics attached to them. I agree that in either of those cases there's no particular reason to use a special syntax rather than a method.

I don't like the "can" in this statement. [...] Giving the compiler implementation a choice which has program-visible side-effects, and then specifying a complicated set of rules for when it should apply the optimization is just asking for trouble

This is a reasonable point. I personally don't think this would cause problems, but at a minimum I'll capture this in the alternatives section, and we could consider changing the elision behavior to make it required. The annoying thing about making it required is that we then have to implement it before shipping the feature and we can never make it better after shipping the feature. I don't think that's a good tradeoff.

Ultimately, though, I think the elisions aren't the most important part of this feature, and this feature is well worth shipping without the elisions, so if the elisions fail to reach consensus we can potentially ship the feature without the elisions. (Omitting the elisions entirely is already called out as an alternative.)

Getting back to the original motivation: making reference counting more seamless, I think simply adding a syntax or standard library macro for cloning values into a closure or async block would go a long way to solving the issue... Potentially even all the way.

See the previous points about people wanting to distinguish lightweight clones specifically. This is a load-bearing point: I can absolutely understand that if you disagree with the motivation of distinguishing lightweight clones, the remainder of the RFC then does not follow. The RFC is based on the premise that people do in fact want to distinguish lightweight clones specifically.

If a more significant change is needed, then I think this should be a type of variable binding (eg. let auto mut x = ...) where such variables are automatically cloned as necessary

I've added this as an alternative, but I don't think that would be nearly as usable.

@joshtriplett
Copy link
Member Author

joshtriplett commented Aug 20, 2024

@Diggsey wrote:

use closure

If any object referenced within the closure or block does not implement Use (including generic types whose bounds do not require Use), the closure or block will attempt to borrow that object instead

I think this fallback is dangerous, as it means that implementing Use for existing types can have far-reaching implications for downstream code, making it a backwards compatibility hazard.

While I don't think this is dangerous, I do think it's not the ideal solution, and I'd love to find a better way to specify this. The goal is to use the things that need to be used, and borrow the things for which a borrow suffices. For the moment, I've removed this fallback, and added an unresolved question.

@davidhewitt
Copy link
Contributor

Thank you for working on this RFC! PyO3 necessarily makes heavy use of Python reference counting so users working on Rust + Python projects may benefit significantly from making this more ergonomic. The possibility to elide operations where unnecessary is also very interesting; while it's a new idea to me, performance optimizations are always great!

I have some questions:

  • The name Use for the trait was quite surprising to me. Reading the general description of the trait and the comments in this thread, it seems like "lightweight cloning" or "shallow cloning" is generally the property we're aiming for. Why not call the trait LightweightClone or ShallowClone? (Maybe can note this in rejected alternatives?)

  • The RFC text doesn't make it clear to me why use & move on blocks / closures need to be mutually exclusive. In particular what if I want to use an Arc<T> and move a Vec<Arc<T>> at the same time; if I'm not allowed the move keyword then I guess I have to fall back to something like let arc2 = arc.use; and then moving both values? Seems like this is potentially confusing / adds complexity.

  • I would like to see further justification why the rejection of having Use provide automatic cloning for these types. I could only find one short justification in the text: "Rust has long attempted to keep user-provided code visible, such as by not providing copy constructors."

    • We already have user-provided code running in Deref operations for most (all?) of the types for which Use would be beneficial. Is it really so bad to make these types a bit more special, if it's extremely ergonomic and makes room for optimizations of eliding .clone() where the compiler can see it?
    • Further, I think Clone is already special: for types which implement Copy, a subtrait of Clone, we already ascribe special semantics. Why could we not just add ShallowClone as another subtrait of Clone which allows similar language semantics (but doesn't go as far as just a bit copy, which would be incorrect for these types)?

@kennytm
Copy link
Member

kennytm commented Aug 21, 2024

Since "Precise capturing" #3617 also abuses the use keyword this may be confusing to teach about the 3 or 4 unrelated usage of the keyword (use item; / impl Trait + use<'captured> / use || closure & async use { block } / rc.use).

@burdges
Copy link

burdges commented Aug 21, 2024

We should really not overload the usage of the keyword use so much, but ignoring the keyword..

Isn't it easier to understand if we've some macro for the multiple clones that run before the code that consumes them, but still inside some distinguished scope?

{
    same_clones!(x,y,z);
    spawn(async move { ... });
}

In this, the same_clones! macro expands to

let x = x.clone();
let y = y.clone();
let z = z.clone();

We use this multi-clones pattern outside async code too, so this non-async specific approach benefits everyone.

@ssokolow
Copy link

ssokolow commented Jan 16, 2025

The point @xkr47 is making is one of my biggest concerns with this idea, added complexity to be taught aside.

One of the reasons I use Rust is that, from the perspective of managing memory consumption, it has fewer footguns than a garbage collected language. (And I've run into stuff on more than one occasion where a "memory leak" turned out to be some piece of JavaScript forgetting to unhook an HTML DOM event handler, leading to stale stuff being spuriously held alive by some closure.)

If cloning Rc<T> and Arc<T> becomes completely automatic, then I'll have to start to consider forbidding them (ideally with Clippy lints rather than some kind of slapdash grep-based CI script) and replacing direct use of them with wrappers which block the auto-clone behaviour. (Actually, now that I think about it, this is starting to sound like the dynamic between memory-unsafe APIs, unsafe, and safety-enforcing abstractions.)

@Nadrieril
Copy link
Member

For the cases where you do want to see every Rc clone, you can just deny(implicit_clone) or however we call that.

@ssokolow
Copy link

ssokolow commented Jan 17, 2025

Better than nothing... though my much-earlier-mentioned concerns about knock-on effects still apply.

(That the ecosystem as a whole will become less memory efficient due to a more "laissez-faire by default" approach toward having the compiler surface these concerns. As C and C++ demonstrated very clearly, it's not as if it's practical for you to take responsibility for bringing your entire transitive dependency tree up to whatever standard you want to hold to. Rust has let mut instead of let const and unsafe instead of safe for a reason, after all.)

@nebneb0703
Copy link

This RFC uses as motivation the ergonomics of cloning instead of moving into closures and async blocks. It solves this by creating use || closures and async use blocks, where types that implement a Use trait may be cloned instead of moved.

The RFC also proposes new syntax .use which is not motivated and does not contribute to the ergonomics of cloning into closures or async blocks, as that is already solved without this operator. Aside from the future possibilities of the syntax, the main rationale for this syntax is to enable optimising redundant clones in the same scope (which is already solved by existing lints), or by allowing owned reference types to be "moved back" into the caller scope after calling a function that moves the reference. This behaviour strongly depends on the semantics of the type and its Drop implementation. A type becoming Use will be a SemVer breaking change as some functions/scopes can no longer depend on the fact that an owned type will have its Drop called, and this can be UB if not implemented on ref-like types. In particular, a function may check the strong count of an Arc to verify that it is the sole owner of the Arc. The description of the Use trait only specifies that an example implementation is a reference counted type, but does not specify that this optimisation may be invalid for a lot of other types. This trait is safe to implement, but may cause UB when combined with certain Drop implementations and functions.

Some of these optimisations may be desired, and could be implemented for .clone()s of Use types. Many negative responses to this RFC have been reassured that the new syntax and behaviour is optional, but I would like ref-like optimisations to also apply to code which opts out of these features. If a compiler implemented such features, what would be the difficulty in adding special cases to the .clone() calls, compared to a new .use operator?


What is the current status of this RFC? There remain unresolved review comments, and no FCP has started, but PRs have popped up in nightly and syn to begin implementation

@piegamesde
Copy link

As I already said in more words a while back (#3680 (comment)), this RFC mixes up two distinct issues which should be handled separately. I'd appreciate if it could be closed and superseded by two separate RFCs to each deal with one issue more fully.

@ColonelThirtyTwo
Copy link

ColonelThirtyTwo commented Jan 24, 2025

Coming across this for the first time, I do agree that cloning things into closures is unergonomic, but this RFC is the wrong solution.

The entire "cheaply cloneable" concept is a solution to a footgun that is only introduced by the solution of automatically cloning closure variables. If, for example, you consider a closure syntax where you explicitly mention the variables to clone, the footgun goes away.
It's an XY problem.

And I'd honestly prefer a clone(foo, bar, baz) || { ... } style syntax over implicit cloning - not only is it more explicit, but you don't have to artificially limit yourself to "cheaply cloneable" types for no reason.

The name "Use" is awful. The word "Use" can mean so many different things, and aliasing it with "clone" is extremely unclear. If a non-Rust programmer comes across foo.clone(), it is immediately clear that the value is being cloned, but what the heck does foo.use mean? Considering how much of these clones are going to be of Arc<RwLock<T>> or other locks, I would not put it past a newbie to think foo.use may mean "acquire a lock".

Related, I don't like the addition of the foo.use syntax at all. It's just foo.clone() but with strings attached. The only upside is that the RFC is written in a way that may omit a final clone for the last value, but since this is explicitly for "cheaply cloneable" types anyway, it's not going to matter.

@traviscross traviscross added I-lang-radar Items that are on lang's radar and will need eventual work or consideration. and removed I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. labels Jan 26, 2025
@nikomatsakis
Copy link
Contributor

nikomatsakis commented Jan 30, 2025

Hi folks,

I just took an hour to read every comment on this thread. I wanted to summarize the various bits of feedback to try and capture the main points that were made. I would break things into three buckets. I'd love to hear if you think there is any major point that I missed here.

The Good (points of general agreement)

capturing ref counted values in closures and async blocks is a pain point

Many folks (though not all) agreed that the state of capture in today's closures is indeed a pain point. David Hewitt from PyO3 also highlighted that "PyO3 necessarily makes heavy use of Python reference counting so users working on Rust + Python projects may benefit significantly from making this more ergonomic".

The Bad (points of disagreement with the RFC)

"Can't serve two masters"

The gist of this concern is that, although the RFC declares a motivation of making things easier for new users, adding a new syntax like x.use and hence a category of types is, in fact, going to work against that goal. Diggsey I think put it well:

Having to navigate a codebase filled with this strange .use keyword, which is hard to explain (it copies the value.. except when it doesn't, and it can be used on Use types, but not Clone types, but Use is not Copy, it's different… When is something Use? What makes something lightweight? Uh… read this essay. Why do I need to make this closure use but this one doesn't need to be? Ahahaha, yeah this is going to take a while to explain…) is more of a blocker than clone-into-closure ever was.

Similar comments were made by boats and matthieu-m. In David Hewitt's comment, he also requested further justification for why one needs to write x.use and not just x, as there is already precedent for invisibly invoking user-provided methods via auto-deref.

"Cheap is in the eye of the beholder"

Several people pointed out that it is difficult to define what is "cheap". Rc and Arc have different execution costs, for example, and for many applications, even deep-cloning a map may not be prohibitive.

This led Dirbaio to propose "use-site" declarations like #![autoclone(Rc, Arc)], where crates or modules would declare the types that they believe should be autocloned.

#3680 (comment)

Another alternative (floated by Mark-Simulacrum and cynecx) was to provide some kind of "inline syntax" like use { x.clone() } or x.clone().super to indicate an "inline" expression in the closure that would actually execute at closure creation time.

"Optional" optimizations are creepy

There was some disagreement about whether the idea of optimizing x.use to potentially skip the clone was a good idea. The RFC did not say precisely when such calls would be elided, which means that users would be able to observe distinct behavior depending on the compiler they were using. On the other hand, as Josh pointed out, specifying a precise set of optimizations would mean that we are locking into those specific semantics forever.

and the Ugly (bikeshed-like concerns)

The name use is heavily overloaded

Several people pointed out that the use keyword is fairly generic and already has two meanings in Rust, use foo::bar and the recently introduce "precise capture" rules for impl Trait.

We could have a syntax for cloning into a closure

Given that closures are the main pain point, several people proposed having some form of syntax for cloning into a closure. There are a number of variations on this, e.g., clone || x or clone(x) || fooormove(x.clone()) foo` (as I once proposed in a draft RFC long ago) and so forth.

@ChayimFriedman2

This comment was marked as resolved.

@nikomatsakis

This comment was marked as resolved.

@ssokolow
Copy link

ssokolow commented Feb 1, 2025

Re: "We could have a syntax for cloning into a closure", I'm not sure it's fair to call it a "bikeshed-like concern" when talking about whether Rust should have a new hole in its preference for making behaviours explicit at the call site.

It's bad enough that we have panic! without a maintained cargo-geiger analogue and, as I mentioned, if anything, the examples with heavy-to-Copy arrays speak to #[derive(Copy)] being an under-cooked idea.

Comment on lines +188 to +195
If `x` is not statically known to be dead, but *all* of the following
conditions are met, the compiler *may* elide an `x.use` and use `&x` instead:
- The compiler can statically see that `x` outlives the result of `x.use`,
- The compiler can statically see that the result of `x.use` is only accessed
via shared reference (including methods with `&self`, dereferences via
`Deref`, other invocations of `.use`, `use ||` closures, or `async use`
blocks). Effectively, these conditions mean that the user could theoretically
have refactored the code to use `&x` rather than `x.use`.
Copy link
Member

Choose a reason for hiding this comment

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

This talks about the compiler, but in std we'd likely also apply specializations to elide clones that are Use. Such specializations tend to have a somewhat broader scope than what the compiler can prove.

So I think trying to precisely define what the compiler will do could be misleading to users when additional optimizations elide more than what's mentioned here.

Copy link
Member

Choose a reason for hiding this comment

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

Well, I guess technically it won't be Use itself because it can't be a specialization trait if it inherits from Clone (😥) but yet another internal specialization trait quite like TrivialClone (though that one implies bit-copyability).

Copy link
Member

Choose a reason for hiding this comment

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

Actually, if the compiler wants to use the presence of this trait for optimizations, won't it also have problems with lifetime-dependent implementations just like specialization?

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Mar 7, 2025
…1, r=nikomatsakis

Ergonomic ref counting

This is an experimental first version of ergonomic ref counting.

This first version implements most of the RFC but doesn't implement any of the optimizations. This was left for following iterations.

RFC: rust-lang/rfcs#3680
Tracking issue: rust-lang#132290
Project goal: rust-lang/rust-project-goals#107

r? `@nikomatsakis`
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Mar 7, 2025
…1, r=nikomatsakis

Ergonomic ref counting

This is an experimental first version of ergonomic ref counting.

This first version implements most of the RFC but doesn't implement any of the optimizations. This was left for following iterations.

RFC: rust-lang/rfcs#3680
Tracking issue: rust-lang#132290
Project goal: rust-lang/rust-project-goals#107

r? ``@nikomatsakis``
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Mar 7, 2025
…1, r=nikomatsakis

Ergonomic ref counting

This is an experimental first version of ergonomic ref counting.

This first version implements most of the RFC but doesn't implement any of the optimizations. This was left for following iterations.

RFC: rust-lang/rfcs#3680
Tracking issue: rust-lang#132290
Project goal: rust-lang/rust-project-goals#107

r? ```@nikomatsakis```
rust-timer added a commit to rust-lang-ci/rust that referenced this pull request Mar 7, 2025
Rollup merge of rust-lang#134797 - spastorino:ergonomic-ref-counting-1, r=nikomatsakis

Ergonomic ref counting

This is an experimental first version of ergonomic ref counting.

This first version implements most of the RFC but doesn't implement any of the optimizations. This was left for following iterations.

RFC: rust-lang/rfcs#3680
Tracking issue: rust-lang#132290
Project goal: rust-lang/rust-project-goals#107

r? ```@nikomatsakis```
github-actions bot pushed a commit to model-checking/verify-rust-std that referenced this pull request Mar 14, 2025
…1, r=nikomatsakis

Ergonomic ref counting

This is an experimental first version of ergonomic ref counting.

This first version implements most of the RFC but doesn't implement any of the optimizations. This was left for following iterations.

RFC: rust-lang/rfcs#3680
Tracking issue: rust-lang#132290
Project goal: rust-lang/rust-project-goals#107

r? ```@nikomatsakis```
Comment on lines +393 to +394
Should we allow *any* `x.clone()` call to be elided if a type is `Use` (or `Use
+ Copy`)? If we did this, that would not give us a way to explicitly avoid
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Should we allow *any* `x.clone()` call to be elided if a type is `Use` (or `Use
+ Copy`)? If we did this, that would not give us a way to explicitly avoid
Should we allow *any* `x.clone()` call to be elided if a type is `Use` (or
`Use + Copy`)? If we did this, that would not give us a way to explicitly avoid

mohe2015 pushed a commit to tucant/rustfmt that referenced this pull request Mar 16, 2025
…matsakis

Ergonomic ref counting

This is an experimental first version of ergonomic ref counting.

This first version implements most of the RFC but doesn't implement any of the optimizations. This was left for following iterations.

RFC: rust-lang/rfcs#3680
Tracking issue: rust-lang/rust#132290
Project goal: rust-lang/rust-project-goals#107

r? ```@nikomatsakis```
flip1995 pushed a commit to flip1995/rust-clippy that referenced this pull request Mar 20, 2025
…matsakis

Ergonomic ref counting

This is an experimental first version of ergonomic ref counting.

This first version implements most of the RFC but doesn't implement any of the optimizations. This was left for following iterations.

RFC: rust-lang/rfcs#3680
Tracking issue: rust-lang/rust#132290
Project goal: rust-lang/rust-project-goals#107

r? ```@nikomatsakis```
use(x = x.method(), y) || { ... }

// `..` additionally captures everything that a plain `use` would
async use(x = &obj, move y, ..)

Choose a reason for hiding this comment

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

I really like this idea. If someone in the project wanted complete transparency, an option could be added to Clippy to prevent the use of use || { ... } without explicitly specifying the variables that should be cloned (or moved).

`use(ref x)`).

This potentially makes `use` closures/blocks almost completely supersede `move`
closures; we could consider in the future whether we want to deprecate them.

Choose a reason for hiding this comment

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

A very ambitious idea. I like it :) Once this approach is implemented, I would start using use instead of move in my projects because it would offer much greater possibilities and make the code more explicit.

@theypsilon
Copy link

theypsilon commented Mar 31, 2025

As has been said before, a way to explicitly annotate the closure capture kind (move/clone) of a variable (or all variables) sounds like a fine solution for the "reducing friction" part of the RFC.

This syntax would feel like a natural expansion to the current move-capturing one: clone || {...}, move(foo) clone(bar) || {...} which corresponds to C++ lambda capturing syntax: [=]() {...}, [foo = std::move(foo), bar] () {...}

After this, there could be a conversation about adding another capture kind for light clones in the future.

bors added a commit to rust-lang-ci/rust that referenced this pull request Apr 1, 2025
… r=<try>

Ergonomic ref counting: optimize away clones when possible

This PR build on top of rust-lang#134797. It optimizes codegen of ergonomic ref-counting when the type being `use`d is only known to be copy after monomorphization. We avoid codening a clone and generate bitwise copy instead.

RFC: rust-lang/rfcs#3680
Tracking issue: rust-lang#132290
Project goal: rust-lang/rust-project-goals#107

r? `@nikomatsakis`

This PR could better sit on top of rust-lang#131650 but as it did not land yet I've decided to just do minimal changes. It may be the case that doing what I'm doing regress the performance and we may need to go the full route of rust-lang#131650.
cc `@saethlin` in this regard.
@Ddystopia
Copy link

I think it's worth mentioning that the Forget trait might be an alternative, because it would mostly eliminate the need to clone Arcs into closures to spawn them. There is a recent RFC for it.

@CheaterCodes
Copy link

I just re-read this entire conversation again:

People seem unsure about the .use proposal, but since there is an experiment for it, maybe we'll know more about its implications soon.

There seems to be (almost) a consensus that an explicit/precise capture syntax would be welcome, and maybe even make further ergonomics unnecessary. Yet, I don't think there are any active proposals for this. I think this should probably be split into a separate RFC, since I believe there's a good chance to get that accepted faster.

I'd be happy to write an RFC for precise closure capures if there is interest?

bors added a commit to rust-lang-ci/rust that referenced this pull request Apr 7, 2025
… r=<try>

Ergonomic ref counting: optimize away clones when possible

This PR build on top of rust-lang#134797. It optimizes codegen of ergonomic ref-counting when the type being `use`d is only known to be copy after monomorphization. We avoid codening a clone and generate bitwise copy instead.

RFC: rust-lang/rfcs#3680
Tracking issue: rust-lang#132290
Project goal: rust-lang/rust-project-goals#107

r? `@nikomatsakis`

This PR could better sit on top of rust-lang#131650 but as it did not land yet I've decided to just do minimal changes. It may be the case that doing what I'm doing regress the performance and we may need to go the full route of rust-lang#131650.
cc `@saethlin` in this regard.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I-lang-radar Items that are on lang's radar and will need eventual work or consideration. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.