0

I was trying to write some code that allow me to observe reordering of memory operations.

In the fallowing example I expected that on some executions of set_values() order of assigning values could change. Especialy notification = 1 may occur before the rest of operations, but in dosn't happend even after thousens of iterations. I've compiled code with -O3 optimization. Here is youtube material that i'm refering to : https://youtu.be/qlkMbxUbKfw?t=200

int a{0};
int b{0};
int c{0};
int notification{0};

void set_values()
{
    a = 1;
    b = 2;
    c = 3;
    notification = 1;
}

void calculate()
{
   while(notification != 1);
   a += b + c;
}

void reset()
{
    a = 0;
    b = 0;
    c = 0;
    notification = 0;
}

int main()
{
    a=6; //just to allow first iteration

    for(int i = 0 ; a == 6 ; i++)
    {
        reset();
        std::thread t1(calculate);
        std::thread t2(set_values);

        t1.join();
        t2.join();
        std::cout << "Iteration: " << i << ", " "a = " << a << std::endl;
    }
    return 0;
}

Now the program is stuck in infinited loop. I expect that in some iterations order of instructions in set_values() function can change (due to optimalization on cash memory). For example notification = 1 will be executed before c = 3 what will trigger execution of calculate() function and gives a==3 what satisfies the condition of terminating the loop and prove reordering

Or maybe someone can provide other trivial example of code that help observe reordering of memory operations?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
  • what is your expected output? how do you know notification=1 is not happening? – prhmma Nov 05 '19 at 21:43
  • `t1` will loop at its first line until `notification == 1`, which only happens at the end of `t2`. Then they join, reset and start over. So there is no way that the execution could happen in any other sequence. – wally Nov 05 '19 at 21:54
  • 1
    Theres no synchronization so the assignments are UB. And any kind of reordering has to have no visible side effects so you can't observe it. At least not without external tools. – François Andrieux Nov 05 '19 at 22:32
  • If there reordering have no visible side effects why things like std::atomic_thread_fence exist? ActuallyI looking for some simple example of program where i should use memory bariers, and becous i don't use them some bad things occur. – Przemek No Nov 05 '19 at 22:52
  • @PrzemekNo `atomic_thread_fence` exists in the context of proper inter thread signaling, for which normal variables by definition can't be used, as **data races are UB** – curiousguy Nov 09 '19 at 02:58
  • @FrançoisAndrieux "_At least not without external tools_" Tools like `ptrace`, or the local equivalent. – curiousguy Nov 09 '19 at 02:59

2 Answers2

0

The compiler can indeed reorder your assignments in the function set_values. However, it is not required to do so. In this case it has no reason to reorder anything, since you are assigning constants to all four variables.

Now the program is stuck in infinited loop.

This is probably because while(notification != 1); will be optimized to an infinite loop.

With a bit of work, we can find a way to make the compiler reorder the assignment notify = 1 before the other statements, see https://godbolt.org/z/GY-pAw. Notice that the program reads x from the standard input, this is done to force the compiler to read from a memory location.

I've also made the variable notification volatile, so that while(notification != 1); doesn't get optimised away.

You can try this example on your machine, I've been able to consistently fail the assertion using g++9.2 and -O3 running on an Intel Sandy Bridge cpu.

Be aware that the cpu itself can reorder instructions if they are independent of each other, see https://en.wikipedia.org/wiki/Out-of-order_execution. This is, however, a bit tricky to test and reproduce consistently.

Zagrosss
  • 11
  • 2
  • "_can reorder instructions if they are independent of each other_" and if their execution is either certain (barring CPU exceptions?) or don't produce effect visible outside the execution unit – curiousguy Nov 09 '19 at 03:03
0

Your compiler optimizes in unexpected ways but is allowed to do so because you are violating a fundamental rule of the C++ memory model.

You cannot access a memory location from multiple threads if at least one of them is a writer.

To synchronize, either use a std:mutex or use std:atomic<int> instead of int for your variables

Tootsie
  • 655
  • 3
  • 11
  • "_access a memory location_" ...location of a normal variable, not a synchronization tool (mutex, atomic, semaphore...) – curiousguy Nov 09 '19 at 03:01