Search code examples
c++multithreadingmemory-barriersconstantfolding

Constant folding/propagation optimization with memory barriers


I have been reading for a while in order to understand better whats going on when multithread programming with a modern (multicore) CPU. However, while I was reading this, I noticed the code below in the "Explicit Compiler Barriers" section, which does not use volatile for IsPublished global.

#define COMPILER_BARRIER() asm volatile("" ::: "memory")

int Value;
int IsPublished = 0;

void sendValue(int x)
{
    Value = x;
    COMPILER_BARRIER();          // prevent reordering of stores
    IsPublished = 1;
}

int tryRecvValue()
{
    if (IsPublished)
    {
        COMPILER_BARRIER();      // prevent reordering of loads
        return Value;
    }
    return -1;  // or some other value to mean not yet received
}

The question is, is it safe to omit volatile for IsPublished here? Many people mention that "volatile" keyword has nothing much to do with multithread programming and I agree with them. However, during the compiler optimizations "Constant Folding/Propagation" can be applied and as the wiki page shows it is possible to change if (IsPublished) into if (false) if compiler do not knows much about who can change the value of IsPublished. Do I miss or misunderstood something here?

Memory barriers can prevent compiler ordering and out-of-order execution for CPU, but as I said in the previos paragraph do I still need volatile in order to avoid "Constant Folding/Propagation" which is a dangereous optimization especially using globals as flags in a lock-free code?


Solution

  • If tryRecvValue() is called once, it is safe to omit volatile for IsPublished. The same is true in case, when between calls to tryRecvValue() there is a function call, for which compiler cannot prove, that it does not change false value of IsPublished.

    // Example 1(Safe)
    int v = tryRecvValue();
    if(v == -1) exit(1);
    
    // Example 2(Unsafe): tryRecvValue may be inlined and 'IsPublished' may be not re-read between iterations.
    int v;
    while(true)
    {
        v = tryRecvValue();
        if(v != -1) break;
    }
    
    // Example 3(Safe)
    int v;
    while(true)
    {
        v = tryRecvValue();
        if(v != -1) break;
        some_extern_call(); // Possibly can change 'IsPublished'
    }
    

    Constant propagation can be applied only when compiler can prove value of the variable. Because IsPublished is declared as non-constant, its value can be proven only if:

    1. Variable is assigned to the given value or read from variable is followed by the branch, executed only in case when variable has given value.
    2. Variable is read (again) in the same program's thread.

    3. Between 2 and 3 variable is not changed within given program's thread.

    Unless you call tryRecvValue() in some sort of .init function, compiler will never see IsPublished initialization in the same thread with its reading. So, proving false value of this variable according to its initialization is not possible.

    Proving false value of IsPublished according to false (empty) branch in tryRecvValue function is possible, see Example 2 in the code above.