SE-0282 (Review #2): Interoperability with the C Atomic Operations Library

Hello Swift community,

The second review of SE-0282, "Interoperability with the C Atomic Operations Library" begins now and runs through June 16, 2020. The previous review discussion thread is here:

This proposal includes a proof-of-concept package demonstrating the use of atomics from C to provide a low-level atomic API to Swift:

https://github.com/apple/swift-se-0282-experimental

Reviews are an important part of the Swift evolution process. All reviews should be made in this thread on the Swift forums or, if you would like to keep your feedback private, directly in email to me as the review manager.

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to the Swift Package Manager?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages, libraries, or package managers with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available on the Swift Evolution website.

As always, thank you for participating in Swift Evolution.

Joe Groff
Review Manager

22 Likes

This proposal looks really great to me, I'm happy to see Swift nailing down this part of its specification and contract with low level software.

-Chris

13 Likes

This is as close to a no-brainer as anything I've seen proposed for Swift. Great job, Karoy!

9 Likes

Thank you! This looks great to me. Swift definitely needs these capabilities so let's start with officially documenting the guarantees we kind of already had.

Thanks also for taking the time to write up the great examples of illegal uses (especially around & in Swift), they were previously illegal but I'm not sure how widely that was understood.

3 Likes

+1. Small and clear proposal which formalises the status quo. It doesn’t introduce any new technical debt as a result of not being able to quite get the API we would like, so that’s great.

One thing that might be worth clarifying is the amendment to the law of exclusivity. I think it would be more accurate to write it like this:

Two accesses to the same variable aren't allowed to overlap unless both accesses are non-atomic reads or both accesses are atomic .

Since, as it says later in the proposal:

Like C, we leave mixed atomic/non-atomic access to the same memory location as undefined behavior, even if these mixed accesses are guaranteed to never overlap.

So overlapping reads are not alllowed in general - only if both of them are atomic/not atomic.

Does “aren’t allowed” mean that the compiler will detect and not allow them by giving an error?

In general I was somewhat confused in which cases the law of exclusivity will crash a program on runtime. Where can I read more about this?

SE-0176 is a good starting point.

1 Like

"Aren't allowed" here means that breaking the rules leads to undefined behavior. Unfortunately, it isn't practical to guarantee a clean runtime trap (much less a compile-time error) for all cases of invalid concurrent access. (The Thread Sanitizer is hugely helpful in detecting such problems, but leaving it enabled in production wouldn't be a viable choice for many Swift applications.)

Like Cory says, SE-0176 introduced the Law of Exclusivity and implemented guaranteed diagnosis of violations in some (but not all) single-threaded cases. (Which moved these cases from the realm of undefined behavior to compiler errors and/or runtime traps.) This proposal changed the language such that some (ostensibly) working code ceased to compile/run in the new Swift release that included its implementation.

As it happens, SE-0176 technically also disallowed atomic concurrent access to the same variable -- not because of any actual desire to outlaw these, but because the concept of an atomic access does not exist in the language. This leaves it unclear how/if atomic operations imported from C would successfully interoperate with Swift constructs. This more focused second version of the proposal concentrates on clarifying this point.

Of course, atomic operations and memory orderings have always worked in Swift -- dispatch queues, locks and similar synchronization constructs deeply rely on them, and such constructs are used in essentially all non-trivial Swift code. But I think it's worth spending a proposal on putting them on a little more stable foundation. (Note that SE-0282 falls far short of specifying a concurrency memory model for Swift -- but by simply declaring that Swift adopts a C++-style model that interoperates with C's atomic library, and highlighting some unique aspects of this interaction, I think we can already go a long way towards enabling people to start using low-level atomic primitives directly from Swift code.)

6 Likes

Indeed. My comment was that the proposed amendment to the law:

Two accesses to the same variable aren't allowed to overlap unless both accesses are reads or both accesses are atomic .

Might be read as saying that overlapping atomic/non-atomic reads would be allowed.

Of course, while we’re importing C’s operations, we can’t add definition to things they leave undefined - but out of interest, is there some practical reason they couldn’t be allowed when using Swift-native atomic operations? Something to do with CPU caching perhaps?

1 Like

Karoy, thank you very much for continuing to work on atomics!

Sorry, but I can't find the community discussion of the second revision -- could someone point me at the thread?

I'm afraid I can't provide a review of this proposal because I have a hard time understanding what exactly is being proposed, what changes are made to the language, what changes are made to the library, and what is just sample code that demonstrates usage of newly added capabilities.

I don't understand the role of the https://github.com/apple/swift-se-0282-experimental package. Is it intended to be a just some experimental code, or is it intended to become a part of the standard library? I get mixed signals:

  • On one hand, the proposal says "Effect on ABI Stability: None." Therefore, no code will be added to the standard library.

  • On the other hand, the location and name of this package makes it look like a Standard Library Preview package, as described by Swift.org - Standard Library Preview Package. That page says

    The preview package provides access to functionality that has been accepted into the Swift standard library through the Swift Evolution process, but has not yet shipped as part of an official Swift release.

    I interpret that to mean that the preview package will be added to the standard library after the review concludes.

The proposal also says "While this proposal enables the use of C's atomics operations in Swift code", but I don't understand through which mechanism. In the preview package (which might not even become a part of the standard library), the entry points for wrappers of C's stdatomic.h APIs are a part of the _AtomicShims module. The underscore in the name implies that _AtomicShims is an implementation detail, not a public API. If so, I don't understand how the proposal enables the use of C's atomics from Swift.

I might be completely misunderstanding the direction and the intent, but it seems to me that the proposal that enables the use of C's atomics in Swift would be changing the Clang importer to enable importing _Atomic qualified types as some corresponding Swift overlay types, with corresponding wrappers from _AtomicShims available from the libc overlay.


If the proposal only clarifies the interaction between Swift's and C's memory model, does not change the standard library API, does not change Clang importer behavior, does not add any new modules to the standard library, does not change the libc overlay etc., then I support the proposal. However, the https://github.com/apple/swift-se-0282-experimental package must be renamed so that it does not look like a Standard Library Preview package that contains stable APIs that were approved by the evolution process. It would be also great if the proposal text was also adjusted to clearly say that the referenced repository is just sample code and not a part of the proposal itself.

One thing that the proposal (and the provided preview package) does not address is the consume memory ordering. Could we say that we upgrade it to acquire from the point of view of Swift?

Hi @gribozavr, my understanding is that this is a clarification and endorsement of a memory model in Swift, without adding any language or library features. The linked package is just an "FYI".

3 Likes

Ah, interesting. For the purposes of exclusivity, atomic accesses are distinct from either read or write accesses.

I believe the practical problem is with compiler optimizations (and instruction reordering). I found Hans Boehm's classic 2011 article a good read on this.

To be clear, this proposal does not expose any Swift-native atomic operations, so such things aren't really on the table yet. Later on, as we hopefully start formalizing Swift's concept of memory, I do think there will be plenty of opportunity to define things that C/C++ leaves undefined. But the need for continued interoperability between these languages means that we ought to be careful not to let them diverge too much. C++ is not standing still -- extensions such as tearing atomics are being discussed, and the memory model is being refined with every new version of the standard. I think it would be best if we kept things in sync as much as possible.

Which is not to say Swift cannot do things its own way or go a little bit ahead of where C++ is. The APIs provided in the initial version of this proposal (now in the supplementary package) already made some (inconsequential) departures from C++17: atomic operations are expected to have lock-free implementations, and CAS already supports all combinations of success/failure orderings. (Although internally it does currently "round them up" to the nearest C-supported combos, for obvious reasons.)

I expect we would eventually provide limited support for mixed atomic/nonatomic access, probably through something like explicit nonatomicLoad operations. (I don't know if this would happen before or after C++ takes a similar step.) Allowing/encouraging regular everyday reads of "atomic" memory locations feels like it would be a mistake, though -- which is one reason why I am so hesitant about directly exposing pointer-based atomics.

Why exactly do you think mixed atomic-nonatomic access is worth bringing up at this point?

Wouldn't it be better to disallow memory_order_consume until it's fixed?

I'm sorry to hear that. To clarify, the section titled "Proposed Solution" contains the proposed changes. In a nutshell, it boils down to

  1. Stating that Swift's memory model is compatible with that of C,
  2. Amending the Law of Exclusivity as introduced in SE-0176 to make sure we don't actually outlaw concurrent atomic access.

The name merely indicates that the package is associated with this proposal. ¯\_(ツ)_/¯

The preview package provides access to new functionality that has been accepted into the standard library through the Swift Evolution process. This proposal doesn't add any new stdlib API, so it is unrelated to the preview package.

It would probably be a good idea to rename the package in case the proposal is accepted, so that people will have an easier time using it. The README explicitly warns potential users that this may happen.

It does this by resolving an uncomfortable ambiguity introduced by a strict reading of SE-0176.

It has always been possible to call C's atomic operations in Swift code, by wrapping them in importable C constructs of varying complexity. We don't need to do anything particular to enable this further.

The problem as I see it here is that I don't see what Swift construct we would map imported _Atomic types. Move-only types with directly addressable components could probably help with that, too.

Upgrading to acquire seems to be a pragmatic choice. (Although -- can't llvm track data dependencies through language boundaries? :thinking:)

3 Likes

How/why would we disallow that? The Swift runtime is itself using consume ordering, and AIUI it does work in Swift the same way it does in C (as implemented by Clang) -- which is to say it behaves like acquire ordering.

memory_order_consume tried to solve a real problem; unfortunately as I understand it failed at that, but artificially forbidding it would just add interoperability problems without resolving any actual issue.

Given consume's problems, the initial version of this proposal (and now the example package) do not expose a consuming ordering. If someone wants to create an atomics package that does include consume, then so be it -- it will be largely pointless, but I don't see how we could possibly disallow that.

It can't be formally disallowed, but we can explicitly recommend against it until there is a meaningful implementation in clang/llvm.

As I see it, the existence of the "upgrade" is a problem, in that someone can write an algorithm that they think uses consume ordering, and it cannot be tested because the compiler silently upgrades it to "acquire". More than likely, when actual support arrives, the implementation will stop working. The upgrade is a nice solution from the point of view of the compiler, but it's a bad solution from the point of view of users.

As I understand it, the consume ordering will never get implemented in clang (or any other C/C++ compiler). The problem is that consume as currently defined is not implementable. The fix will most likely involve replacing consume with new APIs rather than merely tweaking its semantics.

It is always safe to make an operation “more atomic”, or to give it stricter ordering guarantees than it had. Acquire has all of the guarantees of consume, and more.

Basically, there is no observable side-effect.

Isn't that a good justification for explicitly discouraging its use? If it indeed never gets implemented, at best it creates duplicate code paths in libraries. If it does get implemented, a user code might start behaving differently. There's no upside.

That's the view from the compiler implementor. The view from an algorithm tester is you can't test what's silently upgraded. You can write an algorithm that requires acquire ordering and annotate it (wrongly) with consume ordering. Nothing will fail, because the compiler lies.