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

The question still stands.

A huge +1 to that. I completely support officially recognizing these points, which describe the understanding we had implicitly all along. Thank you!

For me, the issue is that the name looks like GitHub - apple/swift-se0270-range-set: Swift Evolution preview package for SE-0270., which implements APIs that will appear in the standard library. A user who does not carefully read the proposal can't tell the difference (I couldn't tell the difference even after reading the proposal). Furthermore, https://github.com/apple/swift-se-0282-experimental says "We expect the package will stabilize if/when SE-0282 is accepted." which suggests that it will contain a stable API.

I'm sorry for the confusion.

Thinking more about it, if we don't want "consume" in Swift, we should specify two things -- how Swift code sees "consume" operations executed by C code (suggestion: the same way as relaxed), and what Swift code should do if it wants to execute a consume operation (suggestion: use acquire).

There wasn’t any per se, but it more or less falls out of the first review thread: people agreed with the memory model, but didn’t agree about the api. This keeps the memory model and jettisons the api.

Right. The core team felt it was appropriate for this subset of the original proposal to go straight back to review without a need for a further pitch phase.

1 Like

I'm strongly opposed to ever adding "consume" ordering to a Swift memory memory model. I'm also strongly opposed to "upgrading" C's consume ordering to aquire". This would convert code that intentionally uses the most efficient ordering possible in that circumstance into code that uses the least efficient ordering.

Rather, we should ensure that LLVM continues to respect data dependencies for all atomic orderings.

The existence of a "consume" ordering is a tragic mistake because it introduces widespread undefined behavior in code that naturally relies on data dependencies, but yields no benefit. Swift should take the opportunity to define away that undefined behavior.

I admit, I don't understand why LLVM's langref currently recommends implementing memory_order_consume using an Acquire barrier. It should probably be Unordered, and data dependencies on Unordered access should be respected by LLVM optimization.

4 Likes

I'd guess that is the reason why LLVM does not implement "consume" in the way you suggested (even though it is a very reasonable suggestion!) This new constraint about preserving data dependencies would be a fundamental new invariant for LLVM transformations. Ensuring that we uphold it everywhere would require auditing and fixing a lot of code.

1 Like

I understand the decision, but for the purposes of transparency to people who are not on the core team and not employed by Apple, I'd prefer that the core team did not take such shortcuts in future.

3 Likes

What purpose do you think such a pitch would have served?

4 Likes

The same purpose as any other pitch -- discussing and improving the proposal before it is frozen for the review.

1 Like

I can't think of any LLVM optimization that would violate "consume" semantics. Maybe there is, I haven't thought very hard about it, but if that's true, then adding an "acquire" ordering won't save you from those optimizations!

If LLVM really is special-casing the "acquire" ordering in some optimization passes, then that should be a simple audit!

Let me add that it is an extremely common pattern to "publish" a pointer using "release" semantics and it's extremely important for performance to be able to read from such a pointer without "acquire" ordering. An "acquire" could cost hundreds of instructions. This relies on the data dependence rule, but there's no reasonable way for an optimizer to break it.

%x - %x ==> 0. For pointers though? IDK either.

It’s just polite.

2 Likes

I suggest that this process discussion be separated to a different thread. (It's a reasonable thing to raise here since it could affect the interpretation of the review discussion, but deciding what to do about it is separate from SE-0282 v2.)

4 Likes

Could it be related to WG21/P0371r1?

I think the idea of lifting relaxed ordering to the role originally intended for consume is really interesting, but I suspect it deserves its own discussion as a followup to this proposal.

IIUC, as things stand, Clang/LLVM works like (apparently) every other C compiler and treats memory_order_consume the same as acquire. Therefore, that is how such code gets imported to Swift, too. I don't think we need to codify this in the language specs; it sounds like an implementation detail to me.

I don't think it would be a good idea to draw attention to consume by singling it out in this (otherwise trivial) proposal; I worry this would do more harm than good. The best way I know to discourage the use of consume in Swift code is to simply not expose it in Swift atomics libraries.

consume has been an ill-defined part of the C++ (and C) standard for almost a decade now, and its problems have been relatively well-documented for quite a while. AFAIK the search for a solution is still on. (I'm really looking forward to curling up with that 45-page paper and others on the topic one lazy Sunday afternoon.)

1 Like

I really want to make sure Swift's memory model doesn't reintroduce "consume". I liked that your previous proposal for surfacing memory order in Swift dropped it.

Regarding the implementation, it seems I would need to take this up on clang-dev. There are multiple reasonable solutions

  • .consume is optimized as an acquire but emits a regular load instruction

  • .unordered respects data dependence (pointer comparisons aren't introduced from thin air)

For some reason, which totally escapes me, clang has instead chosen a ridiculous approach. Anyone diligent enough to write standards-compliant code is rewarded with egregiously bad codegen.

4 Likes

If the types defined in AtomicMemoryOrderings.swift were actually introduced, it would establish a strong example discouraging "consume". The rest of the API can still be deferred, but the memory orderings aren't otherwise going to change between now and the introduction of move-only types.

Introducing dormant APIs to the stdlib doesn't seem like a good idea to me. It would restrict the evolution of the stdlib without actually constraining any package-based atomics implementation.

Even assuming that we won't want to change the Atomic*Ordering structs between now and when we get to introducing a standard atomics API (which isn't a safe assumption at all), there is the matter of where to put them. I still maintain that these APIs do not belong in the default namespace of every Swift program; but introducing a new Atomics module just for the sake of a handful of constants seems premature to me.

Furthermore, this proposal doesn't have mandate to introduce new API in the first place -- as a reminder, the core team's decision was to narrow the proposal to the parts pertaining to C interoperability:

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 interoperability story around memory_order_consume doesn't look interesting to me -- it gets treated as an alias of acquire, because that's how it's implemented in Clang. This arguably renders it pointless in practice -- but this is merely just another (preexisting) C wrinkle in a long list of such wrinkles.

If for some reason a package author wants to introduce an ordering level called consume, then that's their prerogative. The Swift Evolution Process does not dictate what specific APIs packages are allowed to expose. But packages can only implement memory orderings that are actually supported by the compiler; the theoretical consume ordering as currently specified in the C standard is not one of these. (Note though that the last sentence in the quote above carries some weight. An atomics package published by the Swift project may have some influence in this area -- and it won't include a consume ordering.)

I am not a language lawyer, but my impression is that while the current C/C++ consume APIs are probably not long for this world, the underlying formal tools (such as the "dependency-ordered before" and "carries a dependency to" relations) are most likely here to stay. For example, AIUI, we would still need these to serve as the formal basis for @Andrew_Trick's suggestion of tightening the relaxed memory order.

5 Likes

Random suggestion, but the name of this proposal seems to be causing confusion for folks, even with new revised name. Would it make sense to change the name of this to something like "Clarify Swift memory consistency model" since that is what it is doing?

I'm thinking mostly about the future when people refer back to this, e.g. in the proposal list.

-Chris

7 Likes

I'm going to extend the review period to June 25 in order to give the community time to discuss and find agreement on how to handle consume. The Core Team agrees that this is an important point to specify. Even in C and C++, consume ordering semantics have proven difficult-to-impossible to implement in practice, and there are proposals to replace it with more explicit dependency-carrying APIs, so we agree it makes sense for Swift library designers to avoid introducing consume ordering API. It also wouldn't be appropriate to leave the interaction unspecified as an "implementation detail", because although Clang and LLVM may treat consume as equivalent to acquire, Swift also needs to interoperate with C and C++ code built with other compilers, such as GCC and MSVC. As you said, Dmitri, that means we need to specify how memory_order_consume operations look to Swift when they still happen in C or C++, and provide recommendations for people who want consume semantics in Swift. On the first point of consume operations from C, it sounds like there are three choices raised so far:

  • consider consume operations to be unordered accesses from Swift's point of view; effectively saying that dependency chains can't cross the language boundary (as proposed by @gribozavr)
  • consider them to be unordered accesses, but also limit Swift (or LLVM) from doing any pointer optimizations that might break dependency chains, so that all unordered pointer operations carry dependencies (as proposed by @Andrew_Trick)
  • consider consume operations to be acquire operations from Swift's point of view. I might be missing something, but this seems like that would be unsafe to assume for C implementations that did attempt stronger optimizations on consume operations than acquire.

The answer to the second point, "how do I expose consume-like semantics to Swift", seems at least partly dependent on our answer here: if we guarantee that Swift optimizations won't break dependency chains, then unordered access would be sufficient; otherwise, without further elaboration of Swift's memory model, we'd need to direct people to acquire like LLVM itself does.

4 Likes

Recommendation: Swift does not define 'consume' ordering.

C 'consume' operations are directly inherited from the C implementation. Presumably, clang will emit an acquire before returning to Swift code. Apparently other C compilers do the same, but there's no reason for Swift to define that behavior.

Swift (and C/C++) programmers who know with certainty that they need consume-like ordering (i.e. 'acquire' is too expensive) must use 'relaxed' ordering and should read about the rules of "semantic dependence":

More general guidance:

Programmers who aren't sure what they need should always use 'seq_cst', "sequential consistency".... it won't be significantly worse than what clang does for 'consume'!

Programmers who are sure they don't need "sequential consistency", but still need to order multiple loads/reads should use 'acquire'.

The only reason anyone would be interested in 'consume' semantics (which we should keep hidden as much as possible) is that 'acquire' does not meet their performance requirements. They need to be redirected to 'relaxed' with appropriate caveats.

My argument is based on these premises:

The introduction of C++ 'consume' ordering created a new category of undefined behavior in existing well-engineered code without accomplishing anything useful.

Regardless of the C++ standard, programmers who need "semantic dependence" need to ensure that "Subsequent execution must not lead to a situation where there is only one possible value for the variable that is intended to carry the dependency." There's no reasonable way for a compiler to automatically preserve data dependence under all possible circumstances.

In the compiler, there's no useful difference between C++ 'relaxed' vs. C++ 'consume'. If we actually wanted programmers who need the semantics of data dependence to use 'consume' ordering, then both 'relax' and 'consume' should be lowered to LLVM 'monotonic'.

Previously in this thread, I mentioned "tightening LLVM's treatment of unordered/monotonic". I don't think there's anything that needs to be fixed. We just need to agree that the compiler shouldn't actively break code that follows the rules of "semantic dependence" by, say, materializing pointer comparisons out of thin air.

Currently, clang degrades C++ 'consume' to 'acquire'. I can only guess this is meant to punish programmers who try to use 'consume'. So, if they really want "semantic dependence", and are willing to follow the rules, then they need to use 'relaxed' instead. The compiler has given them no other choice.

If making clang compliant to the letter of the standard really takes priority over usefulness, and we were not actively trying to deprecate 'consume', then a sensible middle-ground would be for the compiler to optimize 'consume' as if it had 'acquire' semantics, but emit a regular load instruction.

This is partially moot because C++ 'consume' and associated craziness will be deprecated in some future standard. I don't know what will happen in the C standard.

I think the overall situation we're in now is:

No one should ever use 'consume'.

Programmers who need 'consume'-like ordering should use 'relaxed' and know the rules for protecting themselves against the compiler.

Hopefully in the near future, dependence semantics will be formalized in C++ as a smart-pointer-like dependence-carrying type so the compiler can enforce those rules.

Swift should follow suit when it provides native atomics and define a dependence-carrying wrapper type.

All the existing code that was written using 'relaxed' ordering and follows "semantic dependence" rules should continue to be considered standards compliant, and programmers could continue to write code this way as long as they know the rules. The standard shouldn't retroactively consider it undefined behavior.

2 Likes