[Pitch] Formally defining consuming and nonconsuming argument type modifiers

That’s what I’m thinking: we should define these modifiers such that a hypothetical future compiler has room to overrule them.

Without commenting on the larger point, isn’t this exactly what @usableFromInline does?

There are many situations where the compiler could be able to switch between the two, most notably inlining and specialization.

As for module boundaries, cross-module optimization (which may or may not be stable right now, it’s frustratingly unclear) allows the compiler to ignore them entirely in favor of optimizing everything it can. Ideally, the compiler should be able to produce a binary that is literally impossible to improve upon (Pareto optimal) at that point. You know, eventually.

That sounds like we're putting the cart before the horse. To allow the caller to decide the calling convention even for libraries in binary forms, we would require a certain level of dynamism. I don't think these small ARC optimization would outweigh the cost of such dynamism.


Yes, but that just pushes these boundaries a little deeper into the module, not eliminating them. Compilers can see @inlinable functions and optimize ARC around them, but it still needs to follow the calling convention around @usableFromInline functions, which would only be available in binary forms.

If you want to eliminate such boundaries, you'd need to distribute the library as source code, which is not ideal, or even possible for many cases.


Lest we forget dynamic linking, where the libraries are compiled prior to the application(s) using them, and we can't change the compiled libraries.


I think we're getting off-topic, or rather, out of scope. I'm not exactly sure :thinking:.

In most scenarios the compiler probably couldn’t be able to tell which would be better, most obviously when the caller isn’t being compiled at the same time as the callee. I’m merely pointing out that there are many scenarios where it could, and we should avoid a scenario where using either of these modifiers results in a worse outcome.

This is how @inlinable works: the compiler may or may not inline code with or without that attribute. In fact, CMO ignores it entirely! Code may be inlined in some places but not others, based on whether binary size or runtime performance is being emphasized, etc. There’s a lot of variables, and most of them are complete unknowns when the code is written.

That’s how Swift Package Manager works, which is where I expect the bulk of the usage is going to be.

It’s from the performance roadmap.

3 Likes

Yes, there are definitely such cases, common even. Better yet, in some cases, such as when the callee is non-public, the compiler can even ignore the marked convention entirely (though I'm sure figuring out the optimal convention is also a rather complex process).

OTOH, these keywords are essential for where the compiler most definitely can't do such things, by design and necessity. It's not just about optimization, it's also about the interfaces that the library can craft.

I think you’re assuming that what I’m describing is already in the pitch. I don’t think it is.

I suppose, but what you said essentially boils down to "the compiler can optimize the code while maintaining the original semantic." So, I guess, :woman_shrugging:. (Note though, that public ABI needs to follow the overridden convention, but that's probably not what you're thinking about anyway.)

What I’m thinking is, in order of priority:

  1. If the compiler can tell which convention produces less ARC traffic, it will use that. (Assuming it doesn’t impede binary size unnecessarily, etcetera, much like how it chooses to inline or specialize)
  • I don’t think the compiler does this right now, but it definitely could someday.
  1. If consuming or nonconsuming is specified, it will use that.

You should also note that it's a priority list where 1 > 2 > 3, took me a while to figure that out. But sure, let's also add

  1. Since the calling convention is ABI, if public or @usableByInline functions specify consuming or nonconsuming, it will use that.
1 Like

I’m counting that as not being able to tell. It obviously can’t inline functions in other binaries either.

It may be the cause of the overlong discussion, but I genuinely can't tell if you didn't think of it, or just assumed that everyone does.

The latter. I’m assuming a range of scenarios from “the compiler knows nothing” (library evolution) to “the compiler knows everything” (cross-module optimization).

1 Like

Brooding this over, I still don't see why we would need to include anything along this line. Compilers do that regularly; it's even known to change the function signature already. This will just add pedantry and get in the way of the discussion, especially since you normally don't observe the actual ref count (just whether it's zero).


But anyway, I'll stop.

1 Like

I’m not suggesting that you’ve gone ahead and invented the term out of whole cloth. However, we do need to evaluate the suitability of terminology in this time and context even if it has precedent in Objective-C.

As the discussion which has since unfolded reveals, Rust’s ownership mode has risen to occupy a prominent place in people’s minds. It would be appropriate to ensure that any terminology is conducive to learning (and mastery) by many audiences, not just those steeped in Objective-C, but also those who have no prior experience with ownership annotations and those who are experienced in Rust.

10 Likes

It's perhaps worth pointing out that the words "retained" and "unretained" (which @xwu mentioned before) have precedent from Unmanaged<T>.passRetained/.passUnretained.

In this case, of course, there is no risk of leaking/over-releasing, because you're just specifying a particular calling convention and the compiler will figure out how to balance it, but the words do have a similar conceptual meaning as they do in the Unmanaged APIs.

That said, I still prefer some variant of escaping/nonescaping. It looks less scary than "unretained":

extension MyInteger {
  init?(_ string: unretained String)   // Looks scary.
}

extension MyInteger {
  init?(_ string: nonescaping String)  // Less scary.
}
2 Likes

I’m not sure I agree with your scariness point, but I do think it’s important to make sure that this is never confused with unowned.

I’ve been staying out of this discussion, but I just want to chime in here to mention that I personally find the spellings of these two methods to be particularly unhelpful.

Every time I see them, I always wonder, “Wait, does passRetained mean I am passing a value which has already been retained, or one which I would like to be retained?”

And, “Does passUnretained mean I am passing a value which is currently not retained (and thus needs to be), or one which I would not like to retain (because it already has been)?”

Sometimes I look them up in the documentation and straighten it out in my mind for the moment, but invariably I forget again because the spellings emphatically do not convey to me the intended meanings. I find them both highly ambiguous.

Either one could be interpreted to mean either thing.

15 Likes

I absolutely have the same problem here too; no matter how many times I try, it ends up being no more illuminating and I treat it almost like a magical incantation that just has to be memorized.

I think the point is well taken though that the passRetained and passUnretained APIs deal with basically the same concept. Therefore, ideally, we'd discover a more intuitive name for these APIs as well!

(Honestly, of all the descriptions I've seen thus far, "passing the argument at +0" and "passing the argument at +1" makes the most intuitive sense to me...)

7 Likes