Применяется ли изменение порядка операторов к условным / контрольным операторам? - PullRequest
1 голос
/ 26 февраля 2020

Как описано в других публикациях, без какой-либо квалификации volatile или std::atomic, компилятор и / или процессор могут переупорядочивать последовательность операторов (например, присваивания):

// this code
int a = 2;
int b = 3;
int c = a;
b = c;

// could be re-ordered/re-written as the following by the compiler/processor
int c = 2;
int a = c;
int b = a;

Тем не менее, условные и управляющие операторы (например, if, while, for, switch, goto) также могут быть переупорядочены, или они по сути считаются «забором памяти» ?

int* a = &previously_defined_int;
int b = *a;

if (a == some_predefined_ptr)
{
   a = some_other_predefined_ptr; // is this guaranteed to occur after "int b = *a"?
}

Если приведенные выше операторы можно переупорядочить (например, сохранить a во временном регистре, обновить a, а затем заполнить b путем отмены ссылки на "старый" a во временном регистре), который, я полагаю, мог бы быть при том же поведении «абстрактной машины» в однопоточном окружении, тогда почему нет проблем при использовании блокировок / мьютексов?

bool volatile locked = false; // assume on given implementation, "locked" reads/writes occur in 1 CPU instruction
                              // volatile so that compiler doesn't optimize out

void thread1(void)
{
    while (locked) {}
    locked = true;
    // do thread1 things // couldn't these be re-ordered in front of "locked = true"?
    locked = false;
}

void thread2(void)
{
    while (locked) {}
    locked = true;
    // do thread2 things // couldn't these be re-ordered in front of "locked = true"?
    locked = false;
}

Даже если использовался std::atomic, операторы non-atomi c можно по-прежнему переупорядочивать вокруг операторов atomi c, так что это не помогло бы гарантировать, что операторы "критической секции" (т.е. "do threadX вещи") ) содержались в их предполагаемом критическом разделе (то есть между блокировкой / разблокировкой).


Редактировать: На самом деле, я понимаю, что пример блокировки на самом деле не имеет ничего общего с вопросом оператора условия / управления Я попросил. Тем не менее, было бы неплохо получить разъяснения по обоим задаваемым вопросам:

  • переупорядочение внутри и вокруг условных / контрольных операторов
  • - это блокировки / мьютексы приведенной выше формы надежный?
    • Пока что ответ из комментариев: «Нет, потому что между проверкой while () и получением блокировки есть условие гонки», но кроме этого, мне также интересно узнать о расположении потока код функции вне «критической секции»

Ответы [ 5 ]

3 голосов
/ 26 февраля 2020

Правило «как будто» ([intro.abstract]) важно отметить здесь:

Описания semanti c в этом документе определяют параметризованный недетерминированность c абстрактная машина. Этот документ не предъявляет никаких требований к структуре соответствующих реализаций. В частности, им не нужно копировать или эмулировать структуру абстрактной машины. Скорее, соответствующие реализации требуются для эмуляции (только) наблюдаемого поведения абстрактной машины, как объяснено ниже

Все что угодно * может быть переупорядочено при условии, что реализация может гарантировать, что Наблюдаемое поведение результирующей программы остается неизменным.

Конструкции синхронизации потоков в общем случае не могут быть правильно реализованы без ограждений и предотвращения переупорядочения. Например, стандарт гарантирует, что операции блокировки / разблокировки на мьютексах будут вести себя как операции atomi c. Атомика также явно вводит ограждения, особенно в отношении указанных memory_order. Это означает, что операторы, зависящие от (неосуществленной) операции atomi c, не могут быть переупорядочены, в противном случае наблюдаемое поведение программы может измениться.

[intro.races] говорит о многом о данных рас и порядках.

2 голосов
/ 26 февраля 2020

Условия также могут быть переупорядочены при условии, что переупорядочение не повлияет на поведение программы, соответствующей правилам. Нет проблем с блокировками / мьютексами, потому что оптимизации являются законными, только если они не нарушают программы, которые следуют правилам. Программы, которые должным образом используют блокировки и мьютексы, следуют правилам, поэтому реализация должна быть осторожна, чтобы не нарушать их.

В качестве примера кода вы используете while (locked) {}, где locked равно volatile, либо следует правилам платформы, либо это не так. Есть некоторые платформы, где volatile имеет гарантированную семантику, которая заставляет этот код работать, и этот код будет безопасным на этих платформах. Но на платформах, где volatile не имеет определенной многопоточной семантики, все ставки отключены. Оптимизациям разрешено нарушать код, поскольку код зависит от поведения, которое не гарантирует платформа.

Мне также интересно узнать о размещении кода функции потока вне «критической секции»

Проверьте документацию вашей платформы. Это либо гарантирует, что операции не будут переупорядочены при доступе к volatile объектам, либо нет. Если этого не произойдет, то вы можете переупорядочить операции, и вы будете безрассудно полагаться на то, что это не так.

Будьте осторожны. Было время, когда у тебя часто не было иного выбора, кроме как делать такие вещи. Но это было долго раз go, и современные платформы предоставляют разумные атомы c операции, операции с четко определенной видимостью памяти и так далее. Появляется история новых оптимизаций, которые значительно повышают производительность реализуемого c кода, но нарушают код, основанный на предполагаемой, но не гарантированной семантике. Не добавляйте к проблеме без причины.

2 голосов
/ 26 февраля 2020

Ветви очень сильно отличаются от забора памяти в сборке. Спекулятивное выполнение + exe-of-order exe c означает, что управляющие зависимости не являются зависимостями данных, поэтому, например, if(x) tmp = y; может загружать y, не ожидая пропадания кэша в x. Посмотрите модель памяти, как на самом деле работает load load semanti c?

Конечно, в C ++ это просто означает, что нет, if() не помогает. Определения (по существу устаревшие) memory_order_consume могут даже указывать, что если это не зависимость от данных. Реальные компиляторы продвигают его до acquire, потому что его слишком сложно реализовать, как было указано изначально.

Так что TL: DR: вам все еще нужны атомики с mo_acquire и mo_release (или сильнее), если вы хотите установить sh случай между двумя потоками. Использование релаксированной переменной в if() совсем не помогает, и фактически облегчает реорганизацию на реальных процессорах .

И, конечно, неатомовые c переменные не безопасны без синхронизации. Тем не менее, if(data_ready.load(acquire)) достаточно для защиты доступа к переменной не-Atomi c. Так же как и мьютекс; Счетчик блокировок / разблокировок взаимной блокировки как операции получения и освобождения объекта взаимной блокировки, согласно определениям C ++. (Многие практические реализации включают в себя полные барьеры, но формально C ++ гарантирует только acq и rel для мьютексов)

0 голосов
/ 27 февраля 2020

В 41:27 из разговора Херба Саттера он заявляет, что «непрозрачные вызовы функций» (я бы предположил, что функции, которые компиляция может видеть только объявление, но не определение), требуют полного барьера памяти , Поэтому, хотя условные / управляющие операторы полностью прозрачны и могут быть переупорядочены компилятором / процессором, обходным путем может быть использование непрозрачных функций (например, #include библиотека с функциями, имеющими реализации "NOP" в исходном файле).

0 голосов
/ 26 февраля 2020

Компилятор может свободно 'Оптимизировать' исходный код, не влияя на результат программы.

Оптимизация может заключаться в переупорядочении или удалении ненужных операторов, если на результат программы net это не влияет.

Например, следующие назначения и 'if условие может быть оптимизировано в один оператор:

До оптимизации:

int a = 0;
int b = 20;
// ...
if (a == 0) {
    b = 10;
}

Оптимизированный код

int b = 10;
...