Оптимизация постоянного сворачивания / распространения с барьерами памяти

Некоторое время я читал, чтобы лучше понять, что происходит при многопоточном программировании с современным (многоядерным) процессором. Однако, когда я читал это, я заметил код ниже в разделе "Явные барьеры компилятора", который не использует volatile для IsPublished Глобальный.

#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
}

Вопрос в том, безопасно ли исключать летучие для IsPublished Вот? Многие люди упоминают, что ключевое слово "volatile" не имеет ничего общего с многопоточным программированием, и я с ними согласен. Однако во время оптимизации компилятора может быть применено "постоянное сворачивание / распространение", и, как показывает вики-страница, можно изменить if (IsPublished) в if (false) если компилятор не знает много о том, кто может изменить значение IsPublished, Я что-то здесь упускаю или неправильно понял?

Барьеры памяти могут помешать упорядочению компилятора и неупорядоченному выполнению для CPU, но, как я сказал в предыдущем параграфе, мне все еще нужно volatile чтобы избежать "константного свертывания / распространения", которое является опасной оптимизацией, особенно с использованием глобалов в качестве флагов в коде без блокировки?

1 ответ

Решение

Если tryRecvValue() вызывается один раз, можно безопасно исключить volatile для IsPublished, То же самое верно в случае, когда между вызовами tryRecvValue() есть вызов функции, для которого компилятор не может доказать, что он не изменяет ложное значение 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'
}

Постоянное распространение может применяться только тогда, когда компилятор может доказать значение переменной. Так как IsPublished объявлен как непостоянный, его значение может быть доказано, только если:

  1. Переменной присваивается заданное значение или после чтения из переменной следует ветвь, выполняемая только в том случае, если переменная дала значение.
  2. Переменная читается (снова) в потоке той же программы.

  3. Между 2 и 3 переменная не изменяется в потоке данной программы.

Если вы не позвоните tryRecvValue() в какой-то функции.init компилятор никогда не увидит инициализацию IsPublished в том же потоке с его чтением. Таким образом, доказать ложное значение этой переменной в соответствии с ее инициализацией невозможно.

Доказательство ложного значения IsPublished в соответствии с ложной (пустой) веткой в tryRecvValue функция возможна, см. Example 2 в коде выше.

Другие вопросы по тегам