Why do left operands of logical AND/OR not carry dependency to the parent evaluation?

161 Views Asked by At

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;
1

There are 1 best solutions below

2
On BEST ANSWER

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.