Do C++ conditional statements carry a dependency from the condition expression to the statement?
Asked Answered
G

3

7

I'm asking specifically in the memory-model sense. http://en.cppreference.com/w/cpp/atomic/memory_order

I'm asking because I want to know if I can use a std::memory_order_consume in the below:

mLocalMemPtr1 and 2 and mAtomicMemPtr are pointers into a shared buffer.

In a producer thread I'm doing:

for (int x = 0; x < 10; ++x)
{
    ++mLocalMemPtr1
    *mLocalMemPtr1 = x;       // <========= A
    mAtomicMemPtr.store(mLocalMemPtr1, std::memory_order_release);
}

And in the consumer:

tempMemPtr = mAtomicMemPtr.load(std::memory_order_consume);
while (tempMemPtr != mLocalMemPtr2)
{
    ++mLocalMemPtr2;
    int test = *mLocalMemPtr2;   // <======== B
    doSomeLongRunningThing(test);
    tempMemPtr = mAtomicMemPtr.load(std::memory_order_consume);
}

So does the dependency chain go tempMemPtr -> mLocalMemPtr2 -> test -> doSomeLongRunningThing?

I'm specifically worried that B may be executed before A. I know I can use a std::memory_order_acquire, but I can use consume (which is more lightweight) if the conditional statement causes a memory order dependency.

Garbers answered 6/5, 2013 at 3:20 Comment(0)
D
3

CppReference:

Release-Consume ordering

If an atomic store in thread A is tagged std::memory_order_release and an atomic load in thread B from the same variable is tagged std::memory_order_consume, all memory writes (non-atomic and relaxed atomic) that are dependency-ordered-before the atomic store from the point of view of thread A, become visible side-effects in thread B, that is, once the atomic load is completed, thread B is guaranteed to see everything that thread A wrote to memory if it carries a data dependency into the atomic load.

1.10.10:

An evaluation A is dependency-ordered before an evaluation B if

— A performs a release operation on an atomic object M, and, in another thread, B performs a consume operation on M and reads a value written by any side effect in the release sequence headed by A (...)

1.10.9:

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); (...)

Basing on these facts I say that mLocalMemPtr2 should be synchronized. However there is still a question of the order of evaluation.

if (atomic.load(std::consume) < x)

Which one is evaluated first is unspecified. There is no guarantee (as I couldn't find it in the standard) that the compiler will first perform consume operation, refresh shared buffer and then load atomic and then x.

Having not found a proof that operands are evaluated in the "wished" way, I say that without explicit decomposition of the atomic load mLocalMemPtr2 it won't work and CPU might read stale value of memory pointed by mLocalMemPtr2. memory_order_acquire would not change much here, as mLocalMemPtr2 carries a data dependency.

Disconsolate answered 6/5, 2013 at 11:14 Comment(0)
I
0

I believe that with consume ordering, the compiler could actually make a copy of the entire mSharedBuffer in advance. You need acquire semantics to invalidate previously cached copies of variables other than mAtomicMemLocPtr .

Intramuscular answered 6/5, 2013 at 3:32 Comment(0)
S
0

If one were to take things literally as they are stated in carries dependency . Then I would say that

tempMemPtr = mAtomicMemPtr.load(std::memory_order_consume);

carries dependency into the condition (think of it as a boolean variable flag) of the loop. But that condition is not read as an operand in any of the loop body operations (nor does it write into an object which is read by another operation in the loop body). So one needs an acquire operation in order to make the operations that are sequenced before the release also happen before the operations after the acquire (that read that write release) without relying on the data dependency between the consume operation and the operations in the body of the loop

Maybe a solution that involves the release consume semantics is:

tempMemPtr = mAtomicMemPtr.load(std::memory_order_consume);
while (tempMemPtr != mLocalMemPtr2)
{
    mLocalMemPtr2 = tempMemPtr; // that line adds the dependency needed 
    ++mLocalMemPtr2;
    int test = *mLocalMemPtr2;
    doSomeLongRunningThing(test);
    tempMemPtr = mAtomicMemPtr.load(std::memory_order_consume);
}
Sussex answered 13/12, 2018 at 11:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.