Question

I'm looking for the second time at Herb's great "atomic weapons" talk and I'm trying to wrap my mind around the concepts that undergo the whole memory model / sequential consistency story. There's one thing that now bothers me at the conceptual level. On of the takeaway from the talk is that by using atomics we can "hint" the compiler about interactions between threads that the compiler wouldn't be able to detect otherwise.

So I started worrying about the following scenario:

int local_copy_of_shared_var = shared_var;
if (local_copy_of_shared_var > some_threshold)
{
   DoSomething();
}
... Do some work

if (local_copy_of_shared_var > some_threshold)
{
   DoSomethingElse();
}

In this case, as also pointed out by Hans Bohem in 'How to miscompile programs with “benign” data races' (with variable names adjusted accordingly for the snippet above):

If the compiler decides that it needs to spill the register containing local_copy_of_shared_var between the two tests, it may well decide to avoid storing the value (it’s just a copy of shared_var, after all), and to instead simply re-read the value of shared_var for the second comparison involving local_copy_of_shared_var.

[...] the core problem arises from the compiler taking advantage of the assumption that variable values cannot asynchronously change without an explicit assignment. Such an assumption is entirely legitimate if data races are disallowed by the language specification as in our setting. No such asynchronous changes are possible in the absence of a data race

Now, since atomics (with the default seq_cst memory ordering) should guarantee that there are no data races and since they are the "hint" to the compiler that there's interaction on such variables between different threads, could one argue that using atomics in the preceeding snippet would prevent the compiler to insert such "re-read" from shared_var and instead consider local_copy_of_shared_var as a "one-shot" snapshot avoiding the inconsitency between the two tests?

I think I have something wrong in my reasoning, because driven by common sense I wouldn't think that by just using atomics here I'd be guaranteed that the compiler will take measures so that local_copy_of_shared_var doesn't get updated between the two tests. On the other hand, as Herb says in his talk, the memory model now guarantees that no spurious memory operations should be added by compilers when using atomics, which (looking at this case as a spurious read) would suggest again that this example is now "safe". I'm very confused and would like to hear from the community and possibly get corrected if there's some bug in my reasoning.

Was it helpful?

Solution

Compilers don't get to just make code transformations willy-nilly, they have to follow the as-if rule, which basically states that the generated program must behave as if it executes the code as written in the input program. What makes the optimization you refer to admissible - even in old school C++03 - is that the compiler must be able to prove that the value of shared_var does not change between the two references to local_copy_of_shared_var. Typically that means that all the intervening code is visible to the compiler and that it contains no assignments to shared_var.

This optimization is still legal in C++11 if shared_var is a non-atomic type, since any concurrent modification of shared_var in another thread would be a data race, and hence undefined behavior. Making shared_var a C++11 atomic is a notice to the compiler that it can't prove that shared_var doesn't change between the two references, since it might be changed by another thread, and that this particular optimization would not comply with the as-if rule.

TLDR: Compilers in general are forbidden to introduce spurious reads to atomics as they would introduce data races.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top