A roadmap for improving Swift performance predictability: ARC improvements and ownership control

Maybe I shouldn't have used the term “semantic ARC,” because I'm not exactly sure what you're hearing when I say that. What I really mean is that there are still lots of places where extra traffic persists, @Karl​'s example being one. It seems to me that these extra retains and releases should never make it into unoptimized code in the first place, and the part of codegen that is responsible for them needs to be rethought. I've always assumed that was a part of the “semantic ARC” work, but if it isn't, IMO the problem needs to be attacked at a much more fundamental level than “how do we get rid of these extra things we inserted?”

and seeing the extent of breakage and instability that fell out from aggressively shortening lifetimes.

What I think I hear you saying is that there's a lot of code out there that's incorrect under the current rules but happened to work with the current implementation, and you want to change the rules to make that code correct. Essentially, Hyrum's law has played out?

If the problem is only source compatibility with that broken code, I'd say make the authors of unsafe code explicitly migrate and check their unsafe code. If the problem is binary compatibility with that code, you have my sympathy, and I have no way to judge the seriousness of the problem from Apple's perspective… but the vagueness and complexity of what's being proposed here still leaves me concerned for Swift's future. “A variable's lifetime ends immediately after its last use” is relatively easy to understand and reason about, but “releases are anchored to the end of the variable's scope, and that operations such as accessing a weak reference, using pointers, or calling into external functions, act as deinitialization barriers that limit the optimizer's ability to shorten variable lifetimes…[we] will go into more detail about what exactly anchoring means, and what constitutes a barrier…” is not.

To be clear, the goal is not to be exactly like C++, but to provide rules that establish an actual boundary to how much we can shorten lifetimes, while still providing some flexibility to optimize.

I'm on board with those goals!

I think there will always be value to having explicit annotations for people who want to establish performance guarantees for moves, without having to think like a compiler to get them.

I'm not arguing that no annotations are needed. I'm arguing that we only need one concept, call it whatever you like: “escaping,” “pass-by-move,” ”owned,” “consuming,” or “Fred;” there's just one thing here. We already have @escaping, and if you want to respell that I'm fine with it, but there's no need to add another one.

Those are good approaches.

consuming and nonconsuming become unavoidable concepts once we have move-only types, because they specify whether a move-only value even exists after getting used by a call.

Yes, but they are not unavoidably distinct from escaping/non-escaping. That's my point.

just because you may escape a value doesn't necessarily mean you want to consume your argument, since you may be copying it out of its existing location.

If you are going to copy it out of its existing location anyway, the optimal thing to do is to push that copy up to the caller and make it escaping/owned/consuming, in case they might be passing a value whose lifetime is going to end—in which case the copy can be avoided.

IIUC, the only case this doesn't cover is that you might want to pass a parameter that is only conditionally escaped: it may or may not be copied out of its existing location, based on some dynamic value. The horrible rvalue reference system of C++ (at least partly my fault!) essentially exists to support that scenario, and it's one of my great regrets, because at least IME we almost never take advantage of that capability in real code. I'd rather tell people that in those rare cases, they may pay for a copy at the call site that is later discarded, than add an ”unowned-but-escaping“ concept before it's been proven necessary. And this is especially true for Swift where a discarded copy is O(1). You can even justify this choice in the name of performance predictability.

And my apologies for leaving some things under-described; for some things, it's hard to find a balance between providing a synopsis of what it is that doesn't end up overwhelming the entire document.

My concern here is that if a full description would overwhelm the entire document, it's probably too much complexity for the user model. With no apologies to the Swift API Guidelines:

If you are having trouble describing your language's functionality in simple terms, you may have designed the wrong language.

3 Likes