Thank you for the proposal! I have a couple of comments:
The proposed abstractions seem to allow atomic operations on non-atomic objects. Usually, the atomics abstraction combines the storage and the operations in one type, and ensures that users can't perform non-atomic accesses to the storage. The proposed abstractions don't guarantee that. I understand why this is designed like that -- some use cases would want to control precisely where their atomics are allocated and laid out in memory. However, I would want us, at this point, to avoid adopting a model where mixing atomic and non-atomic accesses to storage is allowed. Atomics are difficult as they are, and other languages don't have experience mixing atomic and non-atomic accesses to the same object. C++ only added std::atomic_ref
just now in C++ 2020.
There are certainly valid use cases for mixing atomic and non-atomic accesses, however, I think we should gain more experience before adding such capability to the standard library.
How to fix: just specify that if a storage location is accessed with an atomic operations, all accesses except initialization must be atomic. We can relax it later.
Names. I wanted to echo the comments above that UnsafeAtomicInt
is not an int itself, but it is a pointer. Consider UnsafeMutablePointerToAtomicInt
(similarly for other types), or, if we adopt a "universal" atomic pointer type, UnsafeMutablePointerToAtomic<Pointee>
.
UnsafeAtomicInt.create(initialValue:)
should probably be an initializer on the type, and include the word "allocate" in the argument label, or it should be called "allocate" like the corresponding unsafe pointer method.
UnsafeAtomicInt.destroy()
should probably reuse the "deallocate" terminology established by existing unsafe pointer types.
UnsafeAtomicLazyReference.initialize(to:)
is a "maybe-initialize" operation, unlike other existing operations in the standard library called "initialize". I think this difference in semantics should be reflected in the name, like initializeIfNil(to:)
. Or should it even use the word "initialize"? The memory is already initialized to nil
, this operation changes nil
to non-nil
-- we call that operation "store" or "set". So maybe setIfNil(to:)
? Or even ifNilSet(to:)
, or ifNilStore(_:)
?
Explore the feasibility of a "semi-universal" pointer-to-atomic type. I would very much like us to explore a design where the user must know about as few types as possible, and where atomics compose with existing standard library types, something along the lines of SwiftNIO, or the design that Jordan posted in this thread.
I completely agree with the concerns expressed in the "A Truly Universal Generic Atomic Type" section, and I agree that this type must provide lock-free guarantees.
I think there are two possible directions to explore:
-
Only allow composing a limited set of standard library types with this semi-universal pointer-to-atomic.
-
Allow a user-defined type to declare that it is "bitwise equatable", that is, that its Equatable
conformance can be implemented by comparing the underlying bits. This way, a struct Point { var x, y: Int8 }
could compose with such a semi-universal pointer-to-atomic type.
Nullability and mutability of UnsafeAtomicMutablePointer<Pointee>
. I would prefer to see a more detailed argument why the pointee should be nullable. Similarly regarding mutability. If we can't make a convincing argument, then I think we should provide more variants of these types. It is important to note that adopting a generic type UnsafeMutablePointerToAtomic<Pointee>
would allow us to avoid the question altogether and allow each user to specify whether they want a nullable/non-nullable pointer, mutable/non-mutable pointer.
Only providing wrapping semantics of arithmetic. I understand that there is a desire to provide direct access to the underlying CPU instructions that don't perform overflow checks. However, I think we should discuss providing both wrapping and trapping variants of atomic arithmetic, with trapping semantics being default.
No convenience operations? If there is a desire for the proposed abstractions to only reflect the underlying machine (from the list of goals "Every atomic operation must compile down to the corresponding CPU instruction (when available), with minimal overhead. (Ideally even if the code is compiled without optimizations.)"), then we should think about the place where the convenience operations will go (for example, trapping variants of arithmetic or some kind of closure-based variant of compare-exchange). If the proposed types are not the right place for convenience operations (because they may have more overhead and don't correspond to a machine instruction), maybe we should name these types something like "machine atomics", freeing up more ergonomic names for more user-friendly abstractions.
Explain in more detail what implementations are acceptable. You specified that a implementation must be lock-free. What about wait freedom? Regardless of what the answer is, the proposal should be explicit about the intent: whether a compare-exchange loop is an acceptable implementation is important to users.
Explore which of the proposed operations are implementable on supported architectures. It would be great if the proposal confirmed if the proposed operations are implementable, and if not, listed the operations that will be missing on certain architectures.
Exposing atomics as a separate module. I would like the proposal to discuss the pros and cons of including atomics into the standard library module vs. exposing them as a separate module. Some thoughts from me: adding atomics to the standard library means that they would be imported into every Swift program, most of which are not going to use atomics. On the other hand, putting atomics into a separate module means that the standard library implementation won't be able to use them. It is almost as if we want a "submodule" that is imported on demand.