Low-Level Atomic Operations

SGTM!

I mean, UnsafeMutablePointer<UnsafeMutablePointer<Foo>> is the current type of a pointer to a pointer. Someone coming from a C background might say that it is too long, but that's not what we are optimizing for.

The proposed types are memory unsafe, so they must have the word "unsafe" somewhere in them. They are also pointers, so I think using the word "Pointer" is appropriate. The word "Mutable" is less necessary (since there is no other option), but it is nice to be consistent with existing unsafe pointer naming, which uses it.

AtomicHandle for me invokes a sense of memory safety, which is not there. UnsafeAtomicHandle would be better, but it is somewhat obscuring the pointer nature -- "handle" is something quite abstract and often memory safe (think file handle, Windows kernel object handle etc.)

Both SGTM.

Could we use just one conformance somehow?

extension Optional: PrimitiveAtomic where Wrapped: PrimitiveAtomic {}

(and then, if necessary, use a trick similar to _customIndexOfEquatableElement in the standard library, for example, define _customLoadOptionalValue which would be implemented for UnsafePointer but not implemented for Int.)

1 Like

Hello Chris!

I think there is a strong and reasonable argument to be made that nothing about this API is really suitable for beginners. Optimizing for predictability seems like a higher priority than progressive disclosure of complexity.

With all due respect, I would argue that, equivalently, a language with a static type system is also not really suitable for beginners in programming. Does this mean beginners in programming should not start learning programming with Swift? As far as I have seen until now, Swift has been welcoming to beginners and when atomics are added to the standard library I'm fairly certain users with no prior experience in atomics will use them to implement counters, flags and other simple algorithms. Some of these people will get fascinated by them and will continue on their journey and learn more about the intricacies of memory orderings, cache line invalidation, etc. Should the rest have to figure out all these things before they ever implement a counter? [1]

My argument is that, as I understand it, sequentially consistent ordering offers the strongest guarantees, relieving the user from the burden of having to reason about a bunch of things that in my opinion sit one level lower than most people will care about. The end result is that the final implementation will be correct by default (assumming the algorithm itself is implemented correctly in the first place), and that at worst it will perform slightly worse than an implementation with manually specified orderings. In that regard, and I may be wrong, I consider memory orderings as a tool for optimising an algorithm.

That said, I'm also okay with not having a default if people end up agreeing against -- like it was mentioned upthread, this can simply be added in an extension as needed.

[1]: granted, you might argue that a relaxed memory ordering typically offers enough guarantees for a counter, but a user would still have to figure this out for themselves, if they cared enough, but a default seqcst ordering would still be correct.

AFAIU, any memory ordering can be replaced with seqcst and the algorithm will still be sound. Therefore, I do not consider worrying that a seqcst load or store happens to slip through during review -- this by itself is not an issue, the code will still behave correctly and it will at worst perform slightly worse.

What is an issue however, is people not having a complete understanding of memory visibility (and there is unfortunately no "I know just enough of this") or having a false security they know what they're doing and spraying acquire and release here and there -- this leads to code that crashes at runtime, or worse.

Edit: Ugh, re-reading my post I think I need to clarify that I don't mean to suggest you or your peers don't understand atomics or have a false sense of security :) All I'm saying is that I believe the bar before dabbling with orderings is quite high and can discourage most people from using atomics, or worse, use them incorrectly.

storeOnce doesn’t sound like a function that may have no effect. storeIfNil happens to be the name I picked for this functionality a couple years ago, so I agree with it.

2 Likes

Specifically because class initializers could not replace self -- otherwise ManagedBuffer.create would have been an initializer.

I'm not sure what you're getting at, but this doesn't seem very pertinent to the discussion on this thread. This is not a general discussion, this is a specific discussion about atomic APIs.

1 Like

Here is my thought process on this (having recently written a bunch of code doing fiddly things with atomics in the TFRT project):

People (should) only reach for atomics when they care about very low level performance. It is true that you can default to sequentially consistency semantics and get something that is correct, but if you're reaching for this for performance in the first place, it seems useful to make these places explicit. Particularly on non-X86 architectures, there can be huge performance differences between using the correct consistency model and relying on sequential consistency.

Atomics are not a beginner concept - there are many complex topics that cannot be swept under a rug. I personally don't think that defaulting this leads to a simpler or better API.

-Chris

6 Likes

Apologies, instead of “Does this mean beginners in programming should not start learning programming with Swift?” I should have asked “should Swift not make a reasonable effort to simplify concepts and syntax so that it’s approachable by beginners?”. That is in response to your argument that atomics are not really beginners’ material, which I agree, and that progressive disclosure is not important, which I personally disagree.

Thanks for sharing your reasoning in your other comment. I agree with everything you say but I still think there’s room to make atomics more generally approachable and defaulting to seqcst helps a bit to achieve that.

1 Like

It seems that the point of debate is whether or not memory ordering is an intrinsic concept with respect to atomics. Chris is arguing that it is, and that one should not use atomics if one is not prepared to understand that parameter and what it means. You are arguing that atomics are useful even if one defaults to a safe memory ordering, and thus one can make atomics available to less-knowledgeable programmers without detracting from both correctness and available power.

2 Likes

Yes, it is actually even more fundamental than atomics.

Typically, the language assumes a single-threaded execution model, so the compiler can reason about the state of memory and whether or not operations which change that state can be observed. That is what enables it to reorder or eliminate bits of code.

Atomics are intrinsically multi-threaded. We are telling the compiler that another thread will be executing, maybe reading or writing to memory in our address space, and the atomic is the channel over which they coordinate, because it has defined behaviour for concurrent reads/writes. It’s a hole in the typical, single-threaded execution model, so the memory order tells the compiler how it should do it’s job around that hole.

So, while it may not necessarily be intrinsic to an atomic operation itself, it really is intrinsic to using an atomic, because it’s about how everything else happens relative to them.

(So yes, I’m against having a default)

5 Likes

I do too think that there is room to make Atomic more approachable, but this is not the goal of a low-level atomic API IMHO.

It will then be pretty easy to build a simpler and more approchable API on top of it.

3 Likes

No offence taken :wink:

It seems though that (luckily!) we are converging on that part of the discussion that yes those are the low-level API after all and any kind of help for people writing those things is welcome. Thanks everyone, this will make reviews much simpler by making the ordering always spelled out explicitly.

Thanks for your replies gentlemen. Avi's summary above is extremely accurate (thank you!).

Yes, it does seem we're converging against having a default. While I'm still personally not convinced, I realise I argue for watering down a really difficult subject in order to be more widely consumed, which might ultimately be at the detriment of people that actually know what they're doing, and I respect that. I don't see any serious harm going forward without a default and we can revisit any time in the future if this becomes important enough and it would just be an additive change.

2 Likes

This can only be implemented when Optional<T> has a layout compatible with atomics, which may not be true even if T is itself atomicable.

UnsafeAtomicThingamabob<Int?> // Ought to be rejected

(There is a similar problem with RawRepresentable, since it doesn't require conformant types to have a layout that matches their RawValue. This is why custom atomic-representable types will use their RawValue as their storage representation.)

The problem is that we wouldn't be able to provide a default implementation that does anything other than trap.

The nullability constraint can be modeled in the type system through a refinement of the atomic protocol, and I think that's the right way to resolve this. (Why stop at adding just one protocol, when we can have two?)

This is now implemented:

Properly modeling the distinction between atomic values (AtomicProtocol) and their storage representation (AtomicStorage) is far more fiddly than it may seem; there is plenty to dislike about API usability there. But on the whole, I think this is the right approach.

I named the generic UnsafeAtomicPointer per Dmitri’s advice — although I still deeply dislike how similar it looks to UnsafeMutablePointer. The two API names blur together when I use this in actual code and it interferes with readability.

3 Likes

I successfully built myself a toolchain that implements this, but is there a way to convince Swift-on-mac to run this despite the impossible availability annotations? I would prefer not to have to build a linux toolchain!

edit: It turns out that similarly annotating my entire test suite was sufficient to convince it to run in Xcode. This did not work from the command line, however.

I ported one of my atomics-using projects to this experimental module. It was straightforward, and it reads pretty well. (It helps that it was already using a pointer-to-atomic-variable setup).

:+1:t4:

3 Likes

A new revision of the pitch document that describes these new protocols is now available at the same URL. (Beware, this is a really quick draft, fresh from my desk with no copy editing whatsoever.)

This version also removes default orderings, and it limits its intended audience to readers who are already familiar with the C++ memory model. (Don't worry -- the removed explanations will live on in the API documentation and/or a standalone document soon(ish).)

To keep everyone on their toes, in this incarnation I'm calling the two generic atomic structs UnsafeAtomic<Value> and UnsafeAtomicLazyReference<Instance>.

I'm open to concrete suggestions for better names for these types. Keep in mind that I expect init(at:)/create/destroy is going to be a shape adopted by a whole family of basic synchronization constructs, not just atomic values. It would be nice to have a lightweight naming scheme for this pattern. (Even if non-copiable types will eventually do away with the need for it.)

(My concern is that (arguably) overly descriptive prefixes like UnsafePointerToAtomic<Value>, UnsafePointerToAtomicLazyReference<Instance> or UnsafePointerToUnfairLock will lead to a sort of naming blindness, by diluting the most relevant information (atomic lazy reference and unfair lock) in a sea of joylessly ritualistic prefixes. Of course, the eventual safe, non-copiable variants won't be burdened any naming appendages, so I expect this would be a temporary problem.)

I'm also seeking feedback on the naming of the new protocols and their requirements.

protocol AtomicProtocol {...}

protocol AtomicInteger: AtomicProtocol, FixedWidthInteger {...}

protocol NullableAtomic: AtomicProtocol {...}

protocol AtomicRepresentable: AtomicProtocol, RawRepresentable 
where RawValue: AtomicProtocol, AtomicStorage == RawValue.AtomicStorage {}
3 Likes

Hello,

Since this pitch has started, I was surprised by this pattern. I find it actually concerning, and wouldn't like to see it escape in the wild.

Why don't we use classes that perform deallocation on deinitialization, and use the existing and established Foundation pattern init() / init(_:freeWhenDone:) (here, there, ...) in order to specify the ownership of the tracked pointer? Expected advantage: this would remove the need to balance each create with a destroy. This would make the API easier to use (even it it targets seasoned developers).

If the Foundation pattern is undesirable because it does not fit the Swift model very well (for some reason), then what would be needed to avoid this mandatory balance, so that we get a form of RAII?

Short version: While fine for data types like String/Data etc, using a class as wrapper for atomic primitives is not desirable: it would introduce potential ARC traffic and that's definitely not something you want around your hand optimized well balanced atomic operations.

6 Likes