4

According to C++ standards:

An evaluation A carries a dependency to an evaluation B if - the value of A is used as an operand of B, unless:

— B is an invocation of any specialization of std::kill_dependency (29.3), or

— A is the left operand of a built-in logical AND (&&, see 5.14) or logical OR (||, see 5.15) operator, or

— A is the left operand of a conditional (?:, see 5.16) operator, or

— A is the left operand of the built-in comma (,) operator (5.18); (...)

I can understand why the dependency ordered before relationship would stop upon kill_dependency call, but why operators such as logical AND, OR, comma, etc. would also break the dependency chain?

Does it mean the code below has undefined behavior?

//thread1
int y = 2
atomicVal.store(true);

//thread2 
auto x = atomicVal.load(std::memory_order_consume);
cout << x && y;
Lifu Huang
  • 11,930
  • 14
  • 55
  • 77

1 Answers1

5

memory_order_consume was an attempt to expose an asm-level CPU feature for use in C++. (It's temporarily deprecated until it can be reworked to something that compilers can implement in practice, and that doesn't require so much kill_dependency noise in the source). Understanding the CPU behaviour is key to understanding the design of the C++ stuff designed to expose it.

It's all about data dependencies, not control dependencies like conditional branches. C++11: the difference between memory_order_relaxed and memory_order_consume and [[carries_dependency]] what it means and how to implement have some more details.

e.g. an add x2, x2, x3 instruction can't execute until both its input registers are ready, and neither can ldr w1, [x2] perform the load until the address is ready, so if x2 came from another load, it's automatically ordered before this one. (Assuming CPU hardware is designed not to violate cause-and-effect, e.g. by doing value-prediction or whatever DEC Alpha did to violate causality in rare cases). But cbz w1, reg_was_zero can be predicted, so it's not sufficient to make reg_was_zero: ldr w3, [x4] wait for a load that produced w1. (This is AArch64 asm, BTW, a weakly-ordered ISA that guarantees dependency-ordering.)

Short-circuit evaluation of || or left && right is logically the same as an if(left) right, so branch prediction + speculative execution can be expected to run the right-hand side even if the left-hand side hasn't executed yet. There's no data dependency, only a control dependency.

And obviously comma left, right doesn't create any connection at all between the sides, it's basically a way to cram left; right; into a single expression.

Of course, if you use the same variable in both left and right sides, a data-dependency can exist that way, but it's not created by the operator.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • Thank you Peter, so did I understand correctly that the snippet I shared is indeed an undefined behavior in theory but safe in practice just because compilers implement consume as acquire? – Lifu Huang Apr 06 '21 at 06:26
  • 1
    @LifuHuang: Is `y` supposed to be a non-atomic variable? The example would make more sense if thread 1 did `y = 2;` before storing to atomicVal. Otherwise, if nobody's modifying `y`, it's always safe for any thread to read it! But yes, if thread 1 modified it then did a release-store to the atomic var, you need at least acquire to avoid the possibility of data-race UB. – Peter Cordes Apr 06 '21 at 06:33