SE-0282: Low-Level Atomic Operations

Here is the hero shot of pointer-based atomics:

import Atomics
import Dispatch

let counter = UnsafeMutablePointer<Int.AtomicStorage>.allocate(capacity: 1)
counter.initialize(to: Int.atomicStorage(for: 0))

DispatchQueue.concurrentPerform(iterations: 10) { _ in
  for _ in 0 ..< 1_000_000 {
    Int.atomicWrappingIncrement(at: counter, ordering: .relaxed)
  }
}
print(Int.atomicLoad(at: counter, ordering: .relaxed))
Int.disposeAtomicStorage(counter.pointee)
counter.deinitialize(count: 1)
counter.deallocate()

This is objectively terrible. I'm really not looking forward to trying to rationalize this mess. If we must do this, please let's at least do it through UnsafeAtomic.

Now of course, integers don't need a custom dispose implementation, they have trivial storage, and on current platforms they can work with AtomicStorage = Self. Do we only care about integers?

For the low-level API I think the answer is yes, we can probably get away with only integers. For the low-level API we use integers as a substitute for "opaque bits stored in memory".

However, inviting users to perform atomic operations on non-integer values (even trivial, for example, pointers) is muddying our memory model with regards to aliasing -- however the issue seems solvable to me, we already have operations that allow rebinding the memory.

1 Like

Hi Karoy,

I'm not interested in toy problems. Atomics are an advanced feature that only make sense to use if you're managing locality and other things at the same time. The only way to express this sort of thing is with unsafe pointers (or clang-imported types) in Swift today anyway.

My most recent intersection with significant atomics use was with the TFRT project which is not yet open source (though it is "coming soon" I'm told). The only details are a high level talk about it here.

That project is written in C++, but would have been much nicer if written in Swift and would use pointer based atomics like crazy. It includes things like:

  1. A futures implementation with user-customizable memory management that would have to be backed by unsafe pointer. Atomics are used for the reference count and other things. See the "Key Abstraction: AsyncValue" slide.

  2. Several helpers that depend on isolated atomic counters, e.g. for closure-based fork-join parallelism helpers.

  3. An "executor" for executing dataflow graphs that use counters per node, which use an "array of atomics" like pattern.

None of these use-cases would benefit from the higher level types or wrappers around them, because there is enough inherent complexity in the concurrency problems they were addressing.

I'll remind you that concurrent mutations in Swift are inherently unsafe. A future move-only Atomic type will not make concurrent mutations safe in Swift. UnsafePointer is the tip of the iceberg in terms of the issues involved.

-Chris

In implementing the Michael-Scott queue algorithm (a toy example in more than one way), the queue object's declaration I arrived at looks like this (excuse my atomics layer):

final public class LockFreeQueue<T>
{
  let storage = UnsafeMutablePointer<AtomicTaggedMutableRawPointer>.allocate(capacity: 4)
  private var head: UnsafeMutablePointer<AtomicTaggedMutableRawPointer> { return storage+0 }
  private var tail: UnsafeMutablePointer<AtomicTaggedMutableRawPointer> { return storage+1 }
  private var poolhead: UnsafeMutablePointer<AtomicTaggedMutableRawPointer> { return storage+2 }
  private var pooltail: UnsafeMutablePointer<AtomicTaggedMutableRawPointer> { return storage+3 }

  public init()
  {
    let node = Node<T>.dummy
    let tmrp = TaggedMutableRawPointer(node.storage, tag: 1)
    CAtomicsInitialize(head, tmrp)
    CAtomicsInitialize(tail, tmrp)
    CAtomicsInitialize(poolhead, tmrp)
    CAtomicsInitialize(pooltail, tmrp)
  }
  // actual algorithm skipped for brevity
}

The node is a wrapper around an UnsafeMutableRawPointer, with accessors returning the fields, an atomic tagged pointer and an atomic pointer; a single allocation for 2 more atomic values.

It might qualify as overcomplicated, though I don't see a simpler way at the moment.

This makes me start to think that a selected "good enough"/"for now" API should live in a non-stdlib package. Given the number of asterisks we're putting next to all of the proposed variations, "a separate import so that people don't find it on accident" still feels too close-at-hand.

Sure, I know a capital-P package won't be able to access the compiler intrinsics; but nobody's died from the C bridging status quo. What they have been suffering from is from lack of input from domain experts w.r.t. Swift's memory model (+ encoding that expertise into the language or static analysis, like the advent of @_nonEphemeral), address pinning for members, etc. What I would see as a win from this effort is interested parties being able to have a definitive place to even look.

As much as I want better atomics, and how I like the proposal as pitched, I don't think it's a failure to say that there isn't currently an answer for which the stdlib is comfortable making a compatibility guarantee about.

2 Likes

What is your evaluation of the proposal?

-1

I was neutral on this proposal, because I'm more likely to use higher-level APIs. But the negative feedback from library authors is worrying.

I'd like to see a revised proposal, which moves most of the APIs into a swift-atomics package. This would be a similar concept to the swift-numerics package β€” an umbrella module, and separate modules for ManagedAtomic, UnsafeAtomic, etc.

Only the APIs which wrap Builtin atomics (for integers only) would be added to the standard library (in the Swift module):

  • the static methods of AtomicIntegerConformances.swift.gyb
  • the structures and top-level function of AtomicMemoryOrderings.swift

All of the protocols (including _PrimitiveAtomicInteger) would be added to the package.

Is the problem being addressed significant enough to warrant a change to Swift?

Yes, but the solutions could be explored in a package.

Does this proposal fit well with the feel and direction of Swift?

No, adding a new module to the standard library doesn't seem necessary.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I studied the proposal and implementation.

This is certainly on the roadmap. (Note: doing it as a new load ordering would allow easy back deployment but it may not be the right approach.)

AIUI, adding tearable atomics right now could potentially diverge the Swift memory model from that of C++, which doesn't seem like a good idea yet (as of this proposal, we don't provide any formal semantics beyond "see the C++ standard, and beware of the Law of Exclusivity"). The implementation will need to come with tooling/sanitizer etc. support for such operations.

1 Like

The review for SE-0282 Atomics ran from April 14 to April 24, 2020. The core team has decided to return this proposal for revision. During the review, support was nearly unanimous for the memory model the proposal establishes, bringing Swift in line with the model standardized by C. The core team concurs with the review discussion on this subject, and would like to see a revised proposal that focuses on specifying the memory model. Guaranteeing a C-compatible memory model allows developers that currently wrap atomic primitives written in C and import them into Swift to rely on this continuing to work. This would also provide stable ground for building atomics packages outside of the standard library for experimentation and use by early adopters. The Swift project itself plans to develop one of these packages.

The most intense review discussion on SE-0282 centered on the best paradigm in which to expose these atomic operations in Swift, both now and in the future. The core team would like to see the community develop more implementation experience before committing to a standard library API for exposing atomics. There is general agreement that a move-only Atomic type should be the ultimate goal, similar to what has successfully been implemented in Rust; however, the best we can hope to implement in Swift as it exists today without fundamental overhead is something that performs atomic operations on unmanaged memory through pointers. The proposal manifests this as an UnsafeAtomic<T> type, which wraps a pointer, along with AtomicProtocol and AtomicInteger protocols with implementation-defined requirements to which eligible types conform. The review discussion raised concerns about this approach:

  • The naming of UnsafeAtomic does not make the pointer-like nature of the type apparent.
  • The UnsafeAtomic type is proposed to have create and destroy methods, which combine dynamic allocation of storage and initialization for an atomic value. The convenience of these APIs suggests they may be the primary intended API for working with the type, further adding to confusion about the nature of the type, and adding indirection that is likely to be an unacceptable overhead for the sorts of performance-bound applications that require atomics.
  • Since the requirements of the AtomicProtocol are hidden, it isn't clear what guarantees it provides for generic code, or how generic code should work with types constrained by the protocol. Without further implementation experience it also isn't clear that the implementation-detail protocol is adequate for future expansion to double-wide types or other extensions in the future.

Other design paradigms that were explored in the review and pitch thread include:

  • Exposing the atomic operations as free functions, static methods, or instance methods taking values of the existing Unsafe*Pointer types. This makes the pointer-based nature of the operations clear, but also invites mixing and matching atomic and nonatomic operations on the same pointer.
  • Creating a safe class AtomicReference<T> that manages the lifetime of the atomic storage. This would present a mostly safe interface to atomics, since classes allow shared references to a common resource and provide the necessary lifetime management facilities via deinit to manage atomic storage. However, classes would have the problem of added indirection, along with potential added overhead and interference from ARC reference counting operations, making this an inefficient approach.

All of these approaches have serious shortcomings, and they're likely to be quickly superseded by move-only atomics in the future. How far that future is from the present is an open question, and having some sort of low-level API for atomic primitives could still have a small niche for advanced users even with move-only atomics as the primary interface. The core team will keep an eye on the package ecosystem to see what's needed and what works well in practice in this space.

Thanks to everyone who participated in the review!

23 Likes

I would like to discuss this point further (I had a draft post for this review but forgot to finish it :sweat_smile: it’s been a hectic couple of weeks, ok?).

Will there be another discussion thread soon, or is this a longer delay until more data is gathered/language features are implemented?

It will probably be a little while. Your best bet is likely to make a small sample implementation of anything you would like to be considered.

1 Like