Live data from Hacker News

Swift 5 Exclusivity Enforcement

swift.org

21–30 of 63 posts

Re: Swift 5 Exclusivity Enforcement

#21
post #9

Earlier quoted context omitted.

It would be interesting to compare the cognitive load of the Swift way and the Rust way. Rust gives you better guarantees, but you must annotate your code.

AIUI, Rust can do it "the Swift way" (a misnomer, since it was first) for any given object simply by adding a RefCell or Mutex type constructor, as appropriate. These types are commonly used in combination with reference-counting smart pointers, Rc or Arc . (The smart pointers focus on making the "multiple ownership" aspect work, but this entails that these types have to return shared references, which are ordinarily…

The problem with RefCell/Mutex is granularity. If you wrap the entire object in one of them, the mutable borrow you need to write a field blocks anyone else from accessing the object at all, even to read unrelated fields. Note that Swift 5 exclusivity enforcement is imposing exactly this limitation on Swift "structs", but not its heap-allocated "classes".

In Rust, an alternative is to wrap individual fields in RefCell/Mutex, but that results in uglier syntax – you end up writing RefCell/Mutex and .borrow()/.borrow_mut() a lot of times – and adds overhead, especially in the Mutex case (since each Mutex has an associated heap allocation). There are alternatives, like Cell and Atomic*, that avoid the overhead, but have worse usability problems. I've long thought Rust has room for improvement here...

Re: Swift 5 Exclusivity Enforcement

#22
post #9

Earlier quoted context omitted.

It would be interesting to compare the cognitive load of the Swift way and the Rust way. Rust gives you better guarantees, but you must annotate your code.

Rust doesn't just give you better guarantees. It gives you no runtime checking, and potentially even better performance than naive C, because it can automatically tell LLVM when there is no memory aliasing going on.[0] Also, it gives you more confidence, because you can, for example, give out an array to someone and be sure that it won't be modified. And usually that kind of confidence is enough to write safe, race-f…

Swift is designed to support dynamically linked code. If Rust had the same design goal, it would need a lot more dynamic checks too.

Re: Swift 5 Exclusivity Enforcement

#23
post #20
post #8

Earlier quoted context omitted.

The examples are complex to follow because Swift model works just fine for all the normal cases. It only becomes problematic in some edge cases that regular programmer rarely meet. And with all the help that the compiler now provides (even before swift 5), it's becoming really really hard to shoot yourself in the foot.

I must be specially talented in this area, because I still run into memory issues fairly often. Just a couple days ago, I had a weak reference which was turning nil (Swift weak references are zeroing), and I couldn't figure out why. Both objects were still live. Property observers don't report the nil change, apparently. I couldn't figure out how to get the debugger or Instruments to provide any help, either. Eventua…

> I had a weak reference which was turning nil (Swift weak references are zeroing), and I couldn't figure out why

This is extremely unlikely. If zeroing weak references were broken a lot of macOS/iOS would be broken. You probably have a bug or the memory just hasn't been overwritten yet, but the retain count is actually zero, you overwrote the reference, or something similar.

Turn on Zombie Objects in Xcode then try it again. Objects are never deallocated but will turn into an instance of NSZombie when their refcount reaches zero. See if your supposedly "still alive" object is actually an NSZombie at that point.

> I wrap a lot of my GCD code in extra locks ... Without them, it occasionally crashes with strange memory errors that are impossible to figure out

You definitely have some race conditions or other concurrency bugs then. Common issues include being on a different queue than you expected (use dispatchPrecondition() to verify), being on a concurrent queue when you expected a serial queue, failure to use a barrier block on a concurrent queue (another good case where dispatchPrecondition() can help you), or accessing something both on and off the queue.

Re: Swift 5 Exclusivity Enforcement

#24

Earlier quoted context omitted.

Rust doesn't just give you better guarantees. It gives you no runtime checking, and potentially even better performance than naive C, because it can automatically tell LLVM when there is no memory aliasing going on.[0] Also, it gives you more confidence, because you can, for example, give out an array to someone and be sure that it won't be modified. And usually that kind of confidence is enough to write safe, race-f…

Swift is designed to support dynamically linked code. If Rust had the same design goal, it would need a lot more dynamic checks too.

Dynamic linking isn't a problem. Rust's checking is local, and doesn't require checking things in other functions, just their signatures. Swift is the same.

There are some cases were dynamic checks are required (e.g. with reference counting/shared ownership: class in Swift, Rc/Arc in Rust). Swift will automatically insert checks in these cases, whereas they have to be manually written into Rust code (via tools like RefCell). Swift and the APIs inherited from Obj-C use a lot more reference counting that Rust does by default, plus it's more difficult for the programmer to reason about the checks (e.g. to be able to write code that avoids them, to optimise) when they're implicit.

In summary, Rust and Swift have essentially the same rules, but Rust requires manual code to break the default rules, whereas the Swift compiler will implicitly insert those necessary checks (in some cases).

Re: Swift 5 Exclusivity Enforcement

#25
post #21

Earlier quoted context omitted.

AIUI, Rust can do it "the Swift way" (a misnomer, since it was first) for any given object simply by adding a RefCell or Mutex type constructor, as appropriate. These types are commonly used in combination with reference-counting smart pointers, Rc or Arc . (The smart pointers focus on making the "multiple ownership" aspect work, but this entails that these types have to return shared references, which are ordinarily…

The problem with RefCell/Mutex is granularity. If you wrap the entire object in one of them, the mutable borrow you need to write a field blocks anyone else from accessing the object at all, even to read unrelated fields. Note that Swift 5 exclusivity enforcement is imposing exactly this limitation on Swift "structs", but not its heap-allocated "classes". In Rust, an alternative is to wrap individual fields in RefCel…

You can get rid of the heap allocation for Mutex by using parking_lot, and there is work to merge this into standard library. The rest of what you wrote sounds right to me.

Re: Swift 5 Exclusivity Enforcement

#26

Earlier quoted context omitted.

Swift arrays have pass-by-value semantics (under the cover, they use copy-on-write to keep performance good) and are thus safe to pass to functions without concern that they'll be modified. (Unless it's passed as an `inout` parameter, which requires a prepended "&" at the call site.)

Rust can use its own bovine superpowers here https://doc.rust-lang.org/std/borrow/enum.Cow.html to enable developers to write code that's invariant over whether it's dealing with a fresh copy of something, or just a borrowed reference. So, it's not quite correct to say that Swift's use of copy-on-write gives it better performance. Rust can express the exact same pattern quite easily, and do it with far more generalit…

Are you sure that code expresses the same pattern? It looks like Rust's COW can only transition from Borrowed to Owned by making a copy. Swift's COW works by dynamically inspecting the reference count.

Re: Swift 5 Exclusivity Enforcement

#29
post #24

Earlier quoted context omitted.

Swift is designed to support dynamically linked code. If Rust had the same design goal, it would need a lot more dynamic checks too.

Dynamic linking isn't a problem. Rust's checking is local, and doesn't require checking things in other functions, just their signatures. Swift is the same. There are some cases were dynamic checks are required (e.g. with reference counting/shared ownership: class in Swift, Rc/Arc in Rust). Swift will automatically insert checks in these cases, whereas they have to be manually written into Rust code (via tools like R…

That's the point: with dynamic linking the static signature encodes too much.

In Rust sometimes you need to switch from FnOnce to Fn, or from Rc to Arc. These changes are binary incompatible. You have to recompile every client.

Swift can't tolerate that restriction. UIKit doesn't want to have to pick from among Fn/FnOnce/etc for every function parameter, and commit to it forever.

Swift types and functions need to evolve independently from their clients, so static checks are insufficient. That's why you see more dynamic checks. If Rust had the same dynamic linking aspirations it would insert more dynamic checks as well.

Re: Swift 5 Exclusivity Enforcement

#30
post #7

Earlier quoted context omitted.

Right. This is one of the things I don't like about automatic reference counting, that it's almost a complete solution, but cycles require more thought (but less cases) to deal with than manual memory management itself.

But how many times the problem truly happened? Is in my experience so rare that the experience is alike with a GC.

It depends on your domain. Any data structure that has links in both directions becomes a pain, and it's pretty common to have trees like that. It's usually solved by having parents own the children but not vice versa, but then you run into cases where you want the whole thing to live so long as someone holds an owning reference to a child.
Post reply on HN