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

yield is actually already implemented in the language for use with _read and _modify.

There is a fundamental difference between return and yield: return produces a value, while yield lends a value. This means that, unlike code after a return statement, code after a yield statement will be run (unless an error is thrown). And, if generators are implemented in Swift, yield could be called multiple times.

While we could reuse the return keyword, I think yield is a different-enough concept to warrant a new keyword, especially since new keywords in Swift don’t break code the way that new keywords in other languages do.

3 Likes

I don’t see any reason to implement generators as a first-class kind in Swift. They’re a pale imitation of constructs that already exist.

Assuming those aren’t implemented: would you ever use a return in a read or modify block? Or could you just pass a binding and be done with it?

struct Foo {
    private var _x: [Int]
    
    var x: [Int] {
        read { _x }
        modify { &_x }
    }
}

Note that this would already work for get and set blocks, and I expect most programmers to assume it would there as well.

Given the lack of local state, I feel yield is an inappropriate concept to introduce for the purpose.

2 Likes

How would you express a more complex example, such as the implementation of Array’s subscript, without yield?

    _modify {
      _makeMutableAndUnique() // makes the array native, too
      _checkSubscript_mutating(index)
      let address = _buffer.mutableFirstElementAddress + index
      yield &address.pointee
      _endMutation();
    }

(I don’t have particularly strong feelings about having an implicit yield for single-expression accessors — I can see good arguments both ways.)

4 Likes

That’s a much better example, thank you. You’re clearly right about needing a keyword there.

Two questions, though:

  1. When is _endMutation(); called?
  2. Why is there a semicolon?
1 Like

_endMutation() is called after the value is mutated.

...
anArray[0] = 0
// _endMutation is implicitly called here
...

I don’t know — I can’t see any reason why it would be necessary. My guess is that a programmer got confused with another language and put it in without thinking.

1 Like

…When the lifetime of the output ends?

The mutation obviously isn’t finished until that is called, so I can only assume it’s that.

I don’t really see the relation to yield in other languages, where control resumes when the function is called again.

@dabrahams before we spill too much ink here, why don't we wait for @Andrew_Trick to post the document that he promised which describes in more detail what we mean by lexical lifetimes.

6 Likes

It might make more sense if you think about it like a function that accepts a closure:

extension Array {
    mutating func withElement(_ index: Int, _ body: (inout Element) throws -> Void) rethrows {
        _makeMutableAndUnique() // makes the array native, too
        _checkSubscript_mutating(index)
        let address = _buffer.mutableFirstElementAddress + index
        try body(&address.pointee)
        _endMutation();
    }
}

anArray.withElement(0) { $0 = 0 }

I do agree that it’s a bit different from yield in other programming languages, but it is similar in that it brings the surrounding code to a halt that can be resumed later.

3 Likes

I was thinking it seemed like an inverted async-await. Perhaps a different name could be more intuitive?

Would it be crazy to make it actually work like that? Result builders get away with similar compile-time replacement.

Without commenting on the important questions:
How about pin? It's a full word, and it still fits into the three-letter scheme ;-)

1 Like

It would be great to drop support COW types with ARC and replace them with structs with deinit, copy and move constructors. And add SharedPointer, MutableSharedPointer(with sync), UniquePointer for concurrency.

I don’t think move replaces COW. Sharing data is still important.

2 Likes

I appreciate what this proposal is doing, but I worry that complexity will inevitably creep into code that doesn't need it.

My understanding is that under this proposal, most code shouldn't need to change at all – only areas that are highly sensitive to performance should need to worry about the new semantics and features. But I don't know how users should know where to draw the line. It seems likely that organizations will adopt move() to assign instance variables from init() arguments as a standard practice because it can avoid a performance pitfall, and it's hard to make a compelling argument against doing it all the time.

I like how in Objective-C you can disable ARC on a per-file basis. This gives programmers the control they need for critical bits of code while being sufficiently onerous that no one would do it unless necessary. I don't think it's possible or desirable to take a similar approach with Swift, but I hope that we land on something that would be less tempting to use when it's not truly needed.

7 Likes

Why in the case of @noImplicitCopy you use a @ but not in the case of consuming/nonconsuming? What's the logic behind that choice?

1 Like

I would like to also highlight the inconsistency in capitalization and wording. Why is it @Sendable, but @noImplicitCopy, and @nonescaping instead of @nonEscaping, @noEscape or @NonEscapable?

(I understand this is more of a bike-shedding issue, and perfectly happy if the discussion of these inconsistencies is moved to a separate thread. Just wasn't sure if these were addressed at any point).

3 Likes

@noImplicitCopy is the latest in a long line of function attributes that will probably get replaced by a more comprehensive system someday. Compare with @inlinable, @Sendable, or @discardableResult.

consuming and nonconsuming are parameter keywords. Compare with inout.

@Sendable is a bizarre choice in my eyes, brought about because it is named after the equivalent protocol (which functions can’t conform to).

1 Like

I emphatically disagree. Swift code should be safe, idiomatic and easily understood, and always performant. To the greatest extent possible, there should never be any perception of a choice between those three objectives.

If you want to avoid making a mistake, @noImplicitCopy sounds more than capable of ensuring minimal runtime overhead (assuming no one does something ridiculous like defensive copying while using it).

The more guarantees the compiler can be given about your code, the more aggressively it can optimize. Performance-tuning means identifying and eliminating unnecessary assumptions, which is only made harder by lower-level code.

I don’t necessarily see it playing out like this.

I remember some pretty wild and dire speculation about how @dynamicMemberLookup was going to wreck Swift’s whole aesthetic and turn it into JS/Python/Ruby…yet since adoption, the general impact of that feature in practice has been close to zero.

Established language culture subsumes new language features 99% of the time.

I would be very interested to see the results of a toolchain built with an early version of this applied to existing code.

6 Likes

@dynamicMemberLookup ultimately went quite well, in my estimation. The danger isn’t having language features, it’s having language features that are easily misunderstood or misused. If there is a perception (even an inaccurate one!) that Swift code is less performant if it doesn’t use the features in this roadmap, those features will be misused.

1 Like

Yeah, there was that one time when the community (including myself) worried too much about the impact of a new language feature. That doesn't mean we should never worry about anything ever again.

The features are very different from @dynamicMemerLookup. That feature is about API design, and there turned out not to be that many APIs which could benefit from it. These features are about performance, and impact basically every project.

Also - who doesn't care about performance? That's time you could be using to perform useful work, and energy that will drain the user's battery. I'll note that we're not even talking about getting better performance here; only predictable performance. Who wants unpredictable performance?

The thing I worry about is that people will see these annotations all over the place in highly tuned packages like swift-algorithms, -collections, NIO, etc, and think that this is what the "pros" do; that it's some secret trick for high performance and they should throw it around everywhere, too.

But it all depends on how we expose things. If we can clearly communicate that, for instance, move only exists to assert that a variable is dead at a certain point, or that a nonconsuming argument is a sort of hint that the value probably won't extend its lifetime beyond the function call, I think we can significantly limit potential misuse and (most importantly) make it so that code which uses these features is still comprehensible to newcomers.

5 Likes