[Pitch] `@noImplicitCopy` attribute for local variables and function parameters

Hi everyone! Another feature we're working on to provide tools for predictable performance is a way to fully suppress implicit copying on specific variables, and let you manually copy them where necessary. We hope this will provide a wider-scoped, but still localized, tool for controlling the compiler's copy behavior than things like the [take operator]. It's also a good building block on the way to move-only types that we think will still be valuable even when we have those, since we expect that a lot of code will still be generally fine with implicitly-copyable types and only want to take explicit control in sensitive parts of the codebase. I've put up a draft proposal for you all to start reading here:

A few things in particular I'm looking for feedback on:

  • The name @noImplicitCopy says what it does, I guess, but is undeniably clunky.
  • Once we take away the ability to implicitly copy or retain a value, we need to account for what operations on the value are borrowing and consuming on the value. I started a list for both in the proposal, but I undoubtedly forgot some cases.

Thank you for your feedback!

11 Likes

Suggestion: @copying(explicit). And maybe there could be a @copying(forbidden) if it makes sense to forbidden copy at all, even using the copy operator.

7 Likes

Our thinking for this attribute is that it should remain non-transitive, meaning that you can pass your copy-restricted bindings to other functions that may still copy them, since to do otherwise would require an expensive audit of existing code to annotate what functions internally do not copy their arguments, and that at that level of effort, adopting move-only types might be a better investment. Do you think @copying(forbidden) would still be useful as a local constraint?

1 Like

I see the ergonomic reason for this, but it's somewhat unfortunate that @noImplicitCopy really only means "no implicit copy visible directly within this scope"—it seems like the attribute... overpromises what it actually protects from. It would still be local-only, for instance, to require potentially copying uses of a @noImplicitCopy binding to be annotated with copy, but perhaps this would require copy in too many places in practice...

Somewhat relatedly, would it potentially be desirable for an explicit borrow parameter to imply @noImplicitCopy? It seems like you'd do this when you specifically want to avoid copying, so it might be appropriate to ensure you don't actually end up copying the value internally. But this would introduce a difference between implicitly borrow parameters and explicitly borrow parameters which might be undesirable.

1 Like

Before having read the document, my naive impression regarding a @noImplicitCopy binding was simply that one would have to either explicitly copy or explicitly move take.

Having read it now, I see that what's perhaps even more salient to the feature than "no implicit copy" is the "eager move" ("eager take"?) aspect. Without attempting to nail down a perfect non-clunky name, I'd suggest that this should be foregrounded in how the feature is described and taught.

2 Likes

I'm not sure if I understand correctly, but I have a small concern on this feature.

@noImplicitCopy let x = 42
func foo(_ value: take Int) {}
func bar(_ value: Int) {}

// not Error
foo(copy x)
bar(x)

However, after the author of foo changes the function into borrow, it still compiles without error.

// modified function
func foo(_ value: borrow Int) {}

// still not Error
foo(copy x)
bar(x)

Here, no implicit copy happens by default, but the copy operator remains even though it's no longer necessary and actually wasteful. I believe this type of change happens to some extent. In order to avoid this kind of mistake, should there be some warning for non-sense copy usages?

IIUC, copy doesn’t guarantee that there’s some sort of memory-level copy that actually occurs—the optimizer is still free to eliminate unnecessary copies. So at least in trivial cases it seems like this shouldn’t be wasteful.

1 Like

You're right, but I think warning here would still be helpful.

2 Likes

@Michael_Gottesman has gotten his prototype implementation to support @noImplicitCopy on mutable variables as well, so I've updated the proposal to reflect this.

1 Like

Would the name @consumable make any sense? There isn't an implicit copy, but there does appear to be an implicit consume in the "eager-move" semantics.

Some background for bike-shedding... @eagerMove would be a better name, but we also need to be able to talk about "eager-move" types. Our CoW types, Array, Set, and Dictionary, actually have eager-move semantics. In the future, there will be some way for programmers to annotate their own copy-on-write types, or value-like types in general, and those will be eager-moved. In the meantime, it can be forced with an @_eagerMove attribute.

Of course, these types are still implicitly copied. So we need to be careful not to confuse the type and variable attributes. Are we ok saying that the same attribute does more when applied to values (adds in a restriction on copying)?

We get into trouble with names like "move-only" and "consumable" because this attribute is also useful for borrowed values which cannot in fact be moved or consumed.

1 Like

The borrow/inout declarations that are being discussed also create bindings that are non-implicitly-copyable.

Following that pattern, we could add an entirely new binding keyword:

// `x` is a NON-implicitly copyable mutable value
// it is initialized from `c`
// `c` is out of scope after this
consuming x = c

// Compare: After SE-0366,
// `y` is an implicitly copyable mutable value
// It is initialized from `d`
// `d` is out of scope after this
var y = consume d

Andrew Trick summarized this idea in the borrow/inout bindings thread:

1 Like

I should have said that the no-implicit-copy bindings could be spelled: borrowing/consuming/mutating.

This isn't a grammatical concern. Rather, we need to distinguish the binding keywords:

func f1(x: borrowing X) // keyword position is debatable
func f2(x: consuming X)
borrowing x =
consuming x =

From the operational keywords:

f1(borrow x)
f2(consume x)
f2(copy x)

The bottom line is that a growing subset of programmers want to migrate toward ownership control. Asking them to clutter their code with awkward attributes is not the way to encourage that. Binding keywords are the best way to control ownership and mesh well with the need to specify parameter ownership conventions.

I do appreciate this work, and I would add that there are two problems with ARC as it stands- it's over decorating of our code with ARC calls and copies, and the cost of ARC. (Even if we did the work below, its still silly/costly to incessantly retain/release when objects are static and immortal)

Related to the cost of ARC, there's a better paradigm where we don't use atomics when the compiler knows the allocations are local:

One could imagine if the function call to allocate the HeapObject under the swift object was aware of the locality of reference of the ptr being asked for then the ARC call could in most cases not be atomic, and not use a global malloc call. References could then be promoted to global space only when they enter it.

And when they are there, use the old ARC code.

This kind of move would directly address the other half of the problem- which is that swift currently doesn't do a good job of letting we developers communicate our intention of local work, local allocation to the operating system in a way that lets us take advantage of the performance inherent in modern hardware. (We can communicate it (via structs, or local refs, but because ARC is implemented as a global "WE NEED ATOMICS ALL THE TIME" , "WE NEED GLOBAL MALLOC" and because the implementations of Arrays etc use this stuff internally, our performance is mangled regardless.

If you are doing local alloc, and local compute, you don't need atomics, or a global allocator.

By putting it under the hood this way, developers would just benefit and not be exposed to the change directly. And it could be rolled out with @newarc pragma thing on specific instances for testing, and one of those "I want swift6 in my codebase" feature flags.

Also, I do think it's worth considering annotating whole class hierarchies as never copy. Oh, and in our work, we have found we are going to need a less reference counting happy Array. Its pretty hard to dodge Arrays all the time in performance needing code, and you don't want all of us dreaming up performant substitutes.

2 Likes