Question

A question turned up when debugging some code at work for race conditions: here is a reduced example:

//! Schedules a callable to be executed asynchronously
template<class F> void schedule(F &&f);

int main(void)
{
  bool flag(false);

  // Ignore the fact this is thread unsafe :)
  schedule([&] { flag=true; });

  // Can the compiler assume under strict aliasing that this check
  // for flag being false can be eliminated?
  if(!flag)
  {
    // do something
  }
  return 0;
}

Obviously the code fragment is thread unsafe - that bool flag needs to be a std::atomic and then the seq_cst memory ordering would force the compiler to always check the value being tested by if. This question isn't about that - it's about whether initialising a capture-all reference lambda tells the compiler that flag may have been aliased, and therefore to not constexpr elide the check for flag's value later on under optimisation?

My own personal guess is that constructing a [&flag] {...} lambda would suggest potential aliasing of flag, while a [&] {...} clobbering all auto initialised variables with being potentially aliased sounds too extreme an anti-optimisation so I'm guessing no to that. However, I would not be surprised if reference capturing lambdas don't alias clobber anything at all.

Over to you C++ language experts! And my thanks in advance.

Edit: I knew that the lack of thread safety would be seen as an answer, however that is not what I am asking. Let me reduce my example still further:

int main(void)
{
  bool flag(false);

  // Note that this is not invoked, just constructed.
  auto foo=[&] { flag=true; };

  // Can the compiler assume under strict aliasing that this check
  // for flag being false can be eliminated?
  if(!flag)
  {
    // do something
  }
  return 0;
}

Now can that check for flag being false be elided?

Edit: For those of you coming here in the future, my best understanding of the answers below is "yes, the check can be elided" i.e. constructing a lambda which takes a local variable by reference is not considered by the compiler's optimiser as potentially modifying that variable, and therefore the compiler's optimiser could legally elide subsequent reloads of that variable's storage. Thanks to everyone for your answers.

Was it helpful?

Solution

You can't ignore the lack of thread safety. Data races yield undefined behaviour and this code has a data race, so the answer to "Can the compiler assume under strict aliasing that this check for flag being false can be eliminated?" is "The compiler can do whatever it wants."

If you fix that and make the code thread safe with a std::atomic<bool>, the question disappears: the compiler cannot discard the check because it has to conform to the memory model requirements of atomic variables.

If instead the schedule call didn't do anything related to multithreading, the compiler has to preserve the semantics of the abstract machine. The abstract machine does check the value of the flag, but a compiler might be able to perform static analysis that proves the flag will always have a certain value (either always false or always true) and skip the check. That's allowed under the as-if rule: it is not possible to write a C++ program that can reliably tell the difference between the two possibilities (optimise or not).

So, for the second example, the answer is "The compiler can do whatever it wants, as long as the observable behaviour is the same as if it performed the check."

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