Skip to content

Ensure atomicity in the Atomic module is respected by flambda#10035

Merged
xavierleroy merged 2 commits intoocaml:trunkfrom
gadmm:Atomic.flambda
Nov 24, 2020
Merged

Ensure atomicity in the Atomic module is respected by flambda#10035
xavierleroy merged 2 commits intoocaml:trunkfrom
gadmm:Atomic.flambda

Conversation

@gadmm
Copy link
Copy Markdown
Contributor

@gadmm gadmm commented Nov 20, 2020

This PR ensures that the Atomic module is really atomic when taking flambda into account. It might be considered as a bugfix for 4.12.

I explain in terms of ref but it goes the same for Atomic.t. The code below is a stack thread-safe for systhread:

    let r : unit list ref = ref []

    let rec push_this x =
      let old_val = r.contents in
      let new_val = x :: old_val in
      let success =
        if r.contents == old_val
        then (r.contents <- new_val; true)
        else false
      in
      if not success then push_this x

But flambda considers that it can move allocations around, and that it is equivalent to:

    let r : unit list ref = ref []

    let rec push_this x =
      let old_val = r.contents in
      let success =
        if r.contents == old_val
        then (let new_val = x :: old_val in r.contents <- new_val; true)
        else false
      in
      if not success then push_this x

This is no longer thread-safe because there can be a context-switch between the equality test and the assignation.

Code of this form is used in the standard library with the Atomic module. Atomic.compare_and_set is defined as follows:

    let compare_and_set r seen v =
      let cur = r.v in
      if cur == seen then
        (r.v <- v; true)
      else
        false

However, flambda considers that it can inline this function and subsequently move surrounding code between the test and the assignation.

There is no testcase in this PR, because I did not manage to trigger the issue in actual code. But it was confirmed to me that this is something flambda is allowed to do.

A short-term solution is to forbid inlining atomic functions from the Atomic module, which is the strategy taken in this PR. In the longer term it would be nice to have a way to force flambda to respect the semantics of polling locations inside specific pieces of code with an annotation (this is beyond the scope of this PR, there will be opportunities to discuss it elsewhere).

(Thank you to @lthls for his explanations, who I ping again in case he is interested in reviewing this PR.)

@gasche
Copy link
Copy Markdown
Member

gasche commented Nov 20, 2020

The key point in your example is the claim that Flambda may optimize

let new_val = x :: old_val in
if r.contents == old_val
then (r.contents <- new_val; true)
else false

into

if r.contents == old_val
then
  (let new_val = x :: old_val in
  r.contents <- new_val; true)
else false

This is counter-intuitive, as in my intuition they are not equivalent (the allocation may result in OCaml callbacks running that can change the result of the r.contents == old_val test). If I understand @gadmm correctly, the point is that Flambda assumes a more pessimistic model, where callbacks may fire not only at allocation points, but basically at any point, including right before and after computing the test of an if construct (so moving the allocation from before to after does not change the places that could callback).

Here again (as in #9998) the proposed solution is the use of [@inline never] to restrict the compiler reasoning. In this case, what we are working around is the fact that Flambda does does not model the fact that callbacks may only fire at certain synchronization point. I understand the need and it sounds desirable to have this PR as a protection, but it still a bit unfortunate. What would better alternatives be?

  • We could just have compare_and_set and fetch_and_add be compiler primitives that Flambda would treat atomically.
  • Flambda could use a less pessimistic model where only certain program constructs may run callbacks, not any program point. (But I suppose there are sensible reasons why the current behavior was chosen?)
  • There could be a way to localy "opt out" of the pessimistic be model to ask Flambda to be fine-grained only in some specific blocks. This would be ideal here, a more precise and meaningful annotation than [@inline never] (and wouldn't prevent inlining), and a more generic solution than adding more and more primitives as the need arises.

@gadmm
Copy link
Copy Markdown
Contributor Author

gadmm commented Nov 20, 2020

Yes, this is my understanding.

What would better alternatives be?

  • We could just have compare_and_set and fetch_and_add be compiler primitives that Flambda would treat atomically.

The problem is more general, see for instance https://github.com/search?p=1&q=org%3Ajanestreet+%28%2A+BEGIN+ATOMIC+%2A%29&type=Code or b84f7ee#diff-3281003824dbd4299b844117c245dab1aff4aa02452d0c534fd096d388105ad8R53-R66

  • Flambda could use a less pessimistic model where only certain program constructs may run callbacks, not any program point. (But I suppose there are sensible reasons why the current behavior was chosen?)

I do not think we need to ask as much, which sounds like would invalidate many transformations (most of the times code will be atomic only accidentally).

  • There could be a way to localy "opt out" of the pessimistic be model to ask Flambda to be fine-grained only in some specific blocks. This would be ideal here, a more precise and meaningful annotation than [@inline never] (and wouldn't prevent inlining), and a more generic solution than adding more and more primitives as the need arises.

As a heavy user of reasoning on polling locations (re. prototypes of safe resource management), this is something I am looking forward to. Discussing the details of such an annotation would heavily depend on details of the work on adding new polling locations for multicore, which is why I tried to make it out of scope of the current PR. But you are right that this PR is a very good example of why such an annotation is desired, for the reasons you give.

Copy link
Copy Markdown
Member

@gasche gasche left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems okay to me, even though I would prefer a more robust way to control program reasoning. Could you fill the Changes entry?

You placed this in the 4.12 section of the Changes. Merging this in the 4.12 release branch indeed sounds reasonable, as it fixes a potential issue (not observed yet) and is extremely safe. (It may hurt performance, but this is better than endangering correctness.)

@xavierleroy xavierleroy merged commit 2109946 into ocaml:trunk Nov 24, 2020
xavierleroy pushed a commit that referenced this pull request Nov 24, 2020
Prevent inlining of some of the functions, otherwise Flambda can move unrelated allocations inside the critical sections, making them non-atomic.

(cherry picked from commit 2109946)
@xavierleroy
Copy link
Copy Markdown
Contributor

Merged in trunk and in 4.12 (955a4b4).

@gadmm
Copy link
Copy Markdown
Contributor Author

gadmm commented Nov 24, 2020

Thanks!

@gadmm gadmm mentioned this pull request Nov 25, 2020
@gadmm gadmm deleted the Atomic.flambda branch November 27, 2020 14:15
@gadmm gadmm mentioned this pull request May 9, 2022
16 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants