Предотвращает ли атомарность в С++ 11 повторное чтение компилятором общих переменных?

Я второй раз смотрю на великий доклад Херба об атомном оружии и пытаюсь осмыслить концепции, которые проходят через всю историю модели памяти / последовательной согласованности. Есть одна вещь, которая беспокоит меня сейчас на концептуальном уровне. Один из выводов из разговора заключается в том, что, используя атомарные методы, мы можем намекнуть компилятору о взаимодействиях между потоками, которые иначе компилятор не смог бы обнаружить.

Поэтому я начал беспокоиться о следующем сценарии:

int local_copy_of_shared_var = shared_var;
if (local_copy_of_shared_var > some_threshold)
{
   DoSomething();
}
... Do some work

if (local_copy_of_shared_var > some_threshold)
{
   DoSomethingElse();
}

В этом случае, как также указал Ханс Бохем в «Как неправильно компилировать программы с «доброкачественными» гонками данных» (с именами переменных, соответствующим образом скорректированными для приведенного выше фрагмента):

Если компилятор решит, что между двумя тестами ему нужно сбросить регистр, содержащий local_copy_of_shared_var, он вполне может решить не сохранять значение (в конце концов, это всего лишь копия shared_var) и вместо этого просто перечитать значение shared_var. для второго сравнения с использованием local_copy_of_shared_var.

[...] основная проблема возникает из-за того, что компилятор использует предположение, что значения переменных не могут изменяться асинхронно без явного присваивания. Такое предположение вполне правомерно, если гонки данных запрещены спецификацией языка, как в нашем случае. Такие асинхронные изменения невозможны при отсутствии гонки данных.

Теперь, поскольку atomics (с порядком памяти по умолчанию seq_cst) должна гарантировать отсутствие гонок данных и поскольку они являются подсказкой компилятору о взаимодействии таких переменных между различными потоками, можно ли утверждать, что использование atomics в предыдущем фрагменте запретить компилятору вставлять такое повторное чтение из shared_var и вместо этого рассматривать local_copy_of_shared_var как одноразовый снимок, избегая несоответствия между двумя тестами?

Я думаю, что у меня есть что-то неправильное в моих рассуждениях, потому что, руководствуясь здравым смыслом, я бы не подумал, что, просто используя здесь атомарность, я могу гарантировать, что компилятор примет меры, чтобы local_copy_of_shared_var не получать обновления между двумя тестами. С другой стороны, как говорит Херб в своем выступлении, модель памяти теперь гарантирует, что компиляторы не будут добавлять ложные операции с памятью при использовании атомарных операций, что (рассматривая этот случай как ложное чтение) снова предполагает, что этот пример теперь сейф. Я очень запутался и хотел бы услышать мнение сообщества и, возможно, получить исправление, если в моих рассуждениях есть какая-то ошибка.


person abigagli    schedule 18.06.2013    source источник


Ответы (1)


Компиляторы не могут просто выполнять преобразования кода волей-неволей, они должны следовать как если бы правило, которое в основном гласит, что сгенерированная программа должна вести себя как если бы она выполняла код, написанный во входной программе. Что делает оптимизацию, на которую вы ссылаетесь, допустимой - даже в старой школе С++ 03 - так это то, что компилятор должен быть в состоянии доказать, что значение shared_var не меняется между двумя ссылками на local_copy_of_shared_var. Обычно это означает, что весь промежуточный код виден компилятору и что он не содержит присваиваний shared_var.

Эта оптимизация по-прежнему допустима в C++11, если shared_var не является атомарным типом, поскольку любая параллельная модификация shared_var в другом потоке будет гонкой данных и, следовательно, поведением undefined. Преобразование shared_var в C++11 атомарным является уведомлением компилятора о том, что он не может доказать, что shared_var не изменяется между двумя ссылками, поскольку оно может быть изменено другим потоком, и что это конкретная оптимизация не будет соответствовать правилу как если бы.

TLDR: компиляторам в целом запрещено вводить ложные операции чтения в атомарные операции, так как это приведет к гонкам данных.

person Casey    schedule 18.06.2013
comment
Во всех реализациях C++03 volatile должен предотвращать повторное чтение. Обратите внимание, что в первоначальном случае использования volatile — чтение/запись отображаемых в память регистров — повторное чтение значения может иметь гораздо худшие последствия, чем для общих переменных: чтение может изменить состояние оборудования, так что не только считанное значение будет фиктивным. , предположения программы об управляемом оборудовании также могут быть фиктивными. Согласно стандарту, доступ к изменчивой переменной является наблюдаемым поведением, и поэтому оптимизация не может ни оптимизировать его, ни добавлять дополнительные доступы. - person celtschk; 19.06.2013
comment
@celtschk Совершенно верно, когда я писал это, я действительно думал о влиянии изменения порядка доступа к энергонезависимым объектам по отношению к энергозависимым. Некоторые реализации не будут переупорядочивать энергонезависимые с помощью volatile, некоторые будут — MSVC, в частности, обеспечивает семантику получения-освобождения для volatile. Но да, это правда, что компиляторы также не могут вводить ложные чтения volatile. - person Casey; 19.06.2013
comment
Я полностью удалил ссылку на volatile, чтобы не путать читателей с тем, какие именно гарантии обеспечивает volatile: понимание атомарности достаточно проблематично, не добавляя в смесь volatile. - person Casey; 19.06.2013
comment
Я думаю, что компилятор, соответствующий С++ 03, также может выполнить оптимизацию, даже если некоторый промежуточный код не виден, если shared_var имеет автоматическую продолжительность хранения, а адрес &shared_var никогда не используется и никакая ссылка никогда не привязана к shared_var. - person aschepler; 19.06.2013
comment
Возвращаясь к своему собственному вопросу спустя долгое время, я с подозрением отношусь к строке TLDR в принятом ответе: TLDR: компиляторам в целом запрещено вводить ложные чтения в атомарные, поскольку они могут привести к гонкам данных. Разве это не правда? что атомарность никогда не способствует гонкам данных (даже при использовании ослабленного порядка памяти, см. 1.10.5 в стандарте)?? - person abigagli; 03.06.2015
comment
Может быть, вы имели в виду, что компиляторам в целом запрещено вводить ложные чтения в атомарные, поскольку они больше не могут предполагать, что не будут наблюдать какое-то асинхронное изменение, вызванное другим потоком, который одновременно записывает такие атомарные? - person abigagli; 03.06.2015
comment
@abigagli Ты меняешь местами причину и следствие. Стандарт требует, чтобы атомарность никогда не способствовала гонкам данных. Одним из следствий этого требования является то, что компиляторы не могут вводить ложные чтения, потому что это может привести к гонке данных. - person Casey; 04.06.2015
comment
@Casey, спасибо, что ответили спустя столько времени, но, извините, я все еще не могу прийти в себя. Если то, что вы говорите, верно, то по тому же признаку программа, в которой поток A не ложно читает, а поток B не ложно записывает (т.е. и чтение, и запись действительно присутствуют в исходном коде) в общий атом, будет содержать данные гонка? IOW: atomics только заставляет компилятор не вводить ложные доступы, но сами по себе не гарантируют DRF, если я явно ввожу конфликт данных (т.е. одновременный доступ хотя бы с одной записью)? Я теперь еще больше запутался... - person abigagli; 04.06.2015
comment
@Casey Гонка данных по определению 1) не включает примитив синхронизации 2) может существовать в абстрактном выполнении (след абстрактных обращений к памяти) программы C/C++ (не в сгенерированном ассемблерном коде). Нет такой вещи, как введение гонки данных во время компиляции. Ассемблерный код может (на практике всегда так) иметь гонки при доступе к ячейкам памяти. Гонки данных делают поведение C/C++ неопределенным (UB). На уровне процессора нет такой вещи, как UB. Недопустимая компиляция никогда не приведет к гонке данных. - person curiousguy; 10.05.2019
comment
@abigagli Atomics по своей сути не создает гонки данных, у которых нет определенного поведения. В противном случае они были бы совершенно бесполезны! Конечно, любое полезное использование атомарного объекта должно вводить состояние гонки, точно так же, как мьютексы или любой блокирующий примитив. - person curiousguy; 10.05.2019
comment
@aschepler компилятор, соответствующий C++03, также может (...) согласно этой гипотезе любой компилятор версии C++ может - person curiousguy; 10.05.2019
comment
@curiousguy Наверное, я написал это, потому что это был комментарий в конце первого абзаца? - person aschepler; 10.05.2019
comment
@aschepler Я вижу. Я также указывал, что такое преобразование кода допустимо даже для примитивов синхронизации (блокировка, разблокировка, использование атомарных элементов). - person curiousguy; 10.05.2019