What is ~Copyable for?

Maybe, but I think that's actually more of a core use case than the nice but maybe only nice-to-have semantic behaviors you can get with it. The ability to safely allocate and destroy memory without any reference counting, exclusivity checking, CoW overhead, is transformative for high-performance code.

While there are interesting semantic use cases, the core to my mind is the ability to allocate and destroy memory without any reference counting. The reality is that for some use cases, you cannot use ManagedBuffer because the costs it imposes are just too high.

Those performance needs are, as others have pointed out, fairly niche. The vast majority of programs (apps, servers, daemons) can eat the cost of modest reference counting overhead, extra heap allocations etc. But if your hard goal is to match the performance you could achieve with C, you cannot afford these costs. Cost here covers both compiled binary text size and runtime performance – in practice they often end up being the same thing.

Sometimes very marginal costs, of say 2-5% in runtime perf or text size, that would be noise under normal circumstances, are just too high a cost for these use cases, and these are costs that cannot be realistically eliminated by an optimizing compiler no matter how good it gets. For example, the "is the array unique" tax for copy-on-write types can add up. Yes it can be hoisted if the loop is statically visible to the compiler. But a) hoisting a check out of a loop doesn't mitigate the text size impact, and b) sometimes the loop is data-driven such as in an intepreter or parser, where static optimizations have no traction.

We have been applying Swift to more use cases like this, and ~Copyable types such as UniqueArray are invaluable in replacing C code with Swift, in both low-level performance-sensitive user space, and in "text + stack + heap has to be less than 200k" embedded use cases, without having to accept a regression.

This does lead to further questions, of course.

The primary one is "can those not interested ignore it". For the most part, the answer is yes. The Swift project remains committed to progressive disclosure and that's been at the heart of of the design of a lot of the ownership features. For example, the ability for Optional to hold noncopyable types was added without breaking source, and most Swift users remained completely oblivious of this feature. But for those interested in adopting noncopyable types, it unlocked key functionality.

Jordan is right that

but to my mind this is perfectly in keeping with progressive disclosure. Authors of generic libraries for others are expected in general to be intermediate users that probably do need to keep up with the more exotic parts of the language (they also need to understand things like specialized vs unspecialized generics, the source compatability consequences of library evolution etc). And they do still have the option of just not adopting new language features like supressed conformances, at the risk of annoying their users who do want to work with those features.

I also think, at the library level, supressing default copyability can be valuable in helping you think a little more clearly about your code. Why do I need to make a copy here, and what might the consequences of that be? Did I forget to annotate a function argument as consuming (we've found plenty of places in the std lib where we did). And – to kick another hornets nest – I do think we as a community need to confront the reality that copy-on-write is a footgun for so many users, because it leads to significant hard to detect performance problems from code where the copy was expected to be ephemeral, but wasn't (or wasn't enough).

It's also true that Swift remains relatively early in its ownership journey. The feature is usable today (and used heavily in some places) but there remain many many ergonomic wins we can make. Right now we're kind of persuing a "formula 1" model. High-end power users can make use of the feature to get C-like performance. Mixing in a little dusting of noncopyable types to speed up the core hot loop of your code sometimes works, but sometimes you hit the limits of the feature when doing this. Over time we need more support in both the language (if let needs to be able to borrow) and standard library (we need off-the-shelf things for e.g. the equivalent of Rust's RefCell without having to build it from a class+some boilerplate helpers every time, that will allow you to bridge from copyable to noncopyable idioms). Hopefully over time this means you will be able to drop in some noncopyable code to speed up you hot loop, while keeping the nice low-but-non-zero cost affordances that boost productivity everywhere else.

Finally, note that ~Copyable is just one part of ownership, alongside things like Span and borrowing vs consuming arguments, and other features.

32 Likes