Предотвращение появления значений Out of Thin Air с барьером памяти в C ++

Давайте рассмотрим следующую двухпотоковую параллельную программу на C ++:

x,y - глобальные, r1,r2 - локальные для потока, store и от load до int - атомарные. Модель памяти = C ++ 11

int x = 0, int y = 0
r1 = x   | r2 = y 
y = r1   | x = r2

Компилятору разрешено компилировать его как:

int x = 0, int y = 0
r1 = x   | r2 = 42 
y = r1   | x = r2
         | if(y != 42) 
         |    x = r2 = y    

И хотя это согласовано внутри потока, это может привести к неожиданным результатам, потому что возможно, что выполнение этой программы приведет к (x, y) = (42, 42)

Это называется проблемой нехватки воздуха. И он существует, и мы должны с этим жить.

Мой вопрос: препятствует ли барьер памяти компилятору выполнять дикие оптимизации, которые приводят к небывалым значениям?

Например:

[fence] = atomic_thread_fence(memory_order_seq_cst);

int x = 0, int y = 0
r1 = x   | r2 = y 
[fence]  | [fence]
y = r1   | x = r2

person Gilgamesz    schedule 08.07.2018    source источник


Ответы (2)


По теме: мой ответ на Что формально гарантирует, что неатомарные переменные не могут видеть неожиданные значения и создавать гонку данных, как теоретически может атомарное расслабление? более подробно объясняет, что формальные правила C ++ ослабили атомную память модель не исключает из воздуха значений. Но они действительно исключают их в примечании. Это проблема только для формальной проверки программ, использующих mo_relaxed, а не для реальных реализаций. Даже неатомарные переменные защищены от этого, если вы избегаете неопределенного поведения (которое вы не было в коде в этом вопросе).


У вас есть гонка данных Undefined Behavior на x и y, потому что они не atomic переменные, поэтому в стандарте C ++ 11 абсолютно ничего не говорится о том, что может происходить.

Было бы уместно взглянуть на это для более старых языковых стандартов без формальной модели памяти, где люди все равно выполняли потоки, используя volatile или простой int и барьеры compiler + asm, где поведение могло зависеть от компиляторов, работающих так, как вы ожидаете в подобном случае. Но, к счастью, старые плохие времена работы над текущими реализациями потоковой передачи остались позади.


Барьеры здесь бесполезны, ведь нечем создать синхронизацию; как объясняет @davmac, ничто не требует выстраивания барьеров в глобальном порядке операций. Подумайте о барьере как об операции, которая заставляет текущий поток ждать, пока некоторые или все его предыдущие операции станут глобально видимыми; барьеры не взаимодействуют напрямую с другими цепочками.


Неопределенные значения - это то, что может произойти в результате такого неопределенного поведения; компилятору разрешено делать программное прогнозирование значений для неатомарных переменных и изобретать записи в объекты, которые определенно будут записаны в любом случае. Если бы было хранилище релизов или хранилище с ослабленным режимом + барьер, компилятору могло бы быть запрещено изобретать записи перед ним, потому что это могло создавать

В общем, с точки зрения юриста по языку C ++ 11, вы ничего не можете сделать, чтобы сделать вашу программу безопасной (кроме мьютекса или ручной блокировки с помощью атомики, чтобы предотвратить чтение одним потоком x, в то время как другой его пишет).


Расслабленной атомики достаточно, чтобы компилятор не изобретал записи без каких-либо других затрат.

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

atomic_int x = 0, y = 0
r1 = x.load(mo_relaxed)    | r2 = y.load(mo_relaxed)
 y.store(r1, mo_relaxed)   | x.store(r2, mo_relaxed)

Прогнозирование значения может спекулятивно получить будущее значение для r2 в конвейер до того, как поток 2 увидит это значение из y, но на самом деле оно не может стать видимым для других потоков, пока программное обеспечение или оборудование не узнают наверняка, что прогноз был правильным. (Это было бы изобретением записи).

например поток 2 может компилироваться как

r2 = y.load(mo_relaxed);
if (r2 == 42) {                   // control dependency, not a data dependency
    x.store(42, mo_relaxed);
} else {
    x.store(r2, mo_relaxed);
}

Но, как я уже сказал, x = 42; не может стать видимым для других потоков, пока он не является неспекулятивным (аппаратное или программное обеспечение), поэтому прогнозирование значений не может изобретать значения, которые могут видеть другие потоки. Стандарт C ++ 11 гарантирует, что атомики

Я не знаю / не могу придумать какой-либо механизм, с помощью которого хранилище 42 могло бы быть действительно видимым для других потоков до того, как y.load увидит фактическое 42. (т.е. переупорядочение LoadStore загрузки с более поздним зависимым хранилищем). Однако я не думаю, что стандарт C ++ формально гарантирует это. Может быть, действительно агрессивная межпоточная оптимизация, если компилятор сможет доказать, что r2 всегда будет 42 в некоторых случаях, и удалит даже зависимость управления?

Чтобы заблокировать нарушения причинно-следственной связи, определенно будет достаточно накопления загрузки или выпуска. Это не совсем mo_consume, потому что r2 используется как значение, а не указатель.

person Peter Cordes    schedule 08.07.2018
comment
Можно предположить, что то, что показывает OP, является псевдокодом, а чтение и запись в x и y - это расслабленные атомарные чтения (они отмечают, что загрузка и сохранение в int являются атомарными, но да, это не совсем понятно). Обычно, когда эти вопросы обсуждаются, они включают одновременный доступ к расслабленным атомным переменным. Такое поведение разреженного воздуха в настоящее время разрешено стандартом C ++ 11 и является примером 2 здесь. Может, они исправили это в более поздних версиях, я не уверен. - person BeeOnRope; 09.07.2018
comment
@BeeOnRope: Спасибо за ссылку. Но предложенная оптимизация в вопросе, утверждающая, что компилятор может ввести безусловный x = 42 (а затем повторно прочитать y? Выглядит запутанным ...), возможна только с переменными, отличными от atomic. Компилятор может разрешить переупорядочивание и разрыв зависимостей, но он не может изобретать записи вне условных ветвей. Таким образом, часть вопроса имеет смысл только в том случае, если он говорит о реальном int, а также о том, что проблема разреженного воздуха реальна и мы должны с ней мириться. Это не реально (в данном случае) ни для одной реальной реализации, особенно. не [x86]. - person Peter Cordes; 09.07.2018
comment
@BeeOnRope: Хм, применима только формулировка в ISO C ++ 29.4, подраздел 3 на seq_cst, требуя, чтобы атомарная загрузка видела какое-то значение, которое действительно было в объекте в какой-то момент. Но в подразделе 9 только говорит, что реализациям следует избегать значения из воздуха, не делая это жестким требованием. Это, конечно, не позволяет вам непредвзято увидеть 42, если объект никогда не содержал 42 в течение жизни программы. - person Peter Cordes; 09.07.2018
comment
См. Мою ссылку - пример, который использует OP, идентичен примеру 2, который является классической проблемой из воздуха. Рассмотрим последующий код OP с условным примером того, как это может произойти, например, в результате спекулятивного аппаратного механизма. Часть should в подразделе 9 на самом деле является всего лишь отговоркой - как показывает остальная часть ссылки, на самом деле трудно даже формализовать, как избежать этой ситуации, столь же абсурдной, как могут показаться сокращенные примеры. - person BeeOnRope; 09.07.2018
comment
@BeeOnRope: В этой статье, похоже, упускается тот факт, что это может произойти только в результате неверных предположений, которые необходимо откатить. Если вы сделали эти спекулятивные хранилища видимыми для других потоков, тогда придется откатить и другие потоки. Они говорят: Мы не ожидаем, что будущее оборудование будет использовать прогнозирование значения нагрузки, которое потребовалось бы, чтобы сделать его наблюдаемым. но такое прогнозирование значения должно быть совершенным, прежде чем вы сможете позволить ему быть ненадежным. - предположительно, и тогда он не может предсказать значение буквально из воздуха. (Или, что более вероятно, проверял перед выходом на пенсию, как сказано в моем ответе.) - person Peter Cordes; 09.07.2018
comment
Рассмотрим один механизм, с помощью которого это могло произойти: спекуляция значений в сочетании с процессорами, которые видят друг друга спекулятивным состоянием буфера хранилища (например, братья и сестры SMT в проекте с такой функцией). Каждый ЦП мог предположить чтение 42 (или любое значение), а затем выполнить запись, и затем каждый мог подтвердить свои предположения относительно буфера хранения другого ЦП, и чтение могло появиться из воздуха. Однако такая архитектура с отслеживанием спекулятивного состояния между логическими ядрами кажется маловероятной! - person BeeOnRope; 09.07.2018
comment
но такое предсказание ценности должно быть совершенным, прежде чем вы позволите ему быть неспекулятивным - под идеальным, я полагаю, вы имеете в виду, что оно должно быть правильным, верно? Да, определенно, но, как указано выше, прогноз может быть подтвержден как верный в будущем на основе действия другого потока. Когда они говорят, [мы] не ожидаем, что будущее оборудование примет прогноз значения нагрузки, который потребуется, чтобы сделать его наблюдаемым, - я считаю, что они не ожидают, что часть спекулятивного состояния чтения станет реальностью, поскольку полностью отказаться от прогнозирования стоимости кажется маловероятным. - person BeeOnRope; 09.07.2018
comment
@BeeOnRope: ну да, потоки, видящие спекулятивные хранилища из других потоков, позволят каждому подтвердить свои предположения, если переупорядочение также разрешено. Ага, это идея, которую я упустил. - person Peter Cordes; 09.07.2018
comment
... поскольку прогнозирование стоимости - актуальная вещь в исследованиях uarch, и в течение некоторого времени (возможно) находилась за горизонтом для реального оборудования. - person BeeOnRope; 09.07.2018
comment
Очевидно, я имел в виду std :: atomic. Я должен упомянуть об этом. Мне просто было интересно, мешает ли барьер компилятору выполнять дикие оптимизации (такие, которые приводят к значениям OOAT), и я до сих пор не знаю (я знаю, что этот барьер не волшебная инструкция, которая волшебным образом синхронизирует потоки. Я просто предполагаю, что если компилятор видит барьер, он воздерживается перед выполнением прогнозов - вот почему я поставил барьер, казалось бы, глупым). - person Gilgamesz; 09.07.2018
comment
Очевидно, большое спасибо вам (@BeeOnRope и @PeterCordes) за такую ​​информативную дискуссию между великими людьми. Я горжусь тем, что поднял его;)! - person Gilgamesz; 09.07.2018
comment
@Gil - FWIW Я думаю, но не уверен, что ответ на ваш исходный вопрос (при условии расслабленных атомных операций) заключается в том, что да, забор seq_cst предотвращает возможность чтения из воздуха , соглашаясь с последней частью ответа Питера. - person BeeOnRope; 09.07.2018
comment
Впрочем, мне бы тоже хотелось это понять ;-). Насколько я понимаю, стандарт C ++ позволяет наблюдателям видеть предположения о загрузке, но для этого требуется возможность отката ошибочно заданного значения для всех потоков (так что если поток 1 наблюдал загрузку ошибочно опубликованного значения потоком 2, то должна быть существует механизм, который облегчает откат обоих потоков, но, поскольку @BeeOnRope указывает, что этот механизм может дать сбой, да? - person Gilgamesz; 09.07.2018
comment
Что ж, стандарт C ++ не говорит о предположениях о нагрузке или о чем-то в этом роде. У него более абстрактная формальная модель, и именно эта модель допускает различные несущественные проблемы, подобные вашему примеру, просто потому, что язык их не исключает. То есть вы не найдете формулировок, в которых говорится, что спекулятивные нагрузки разрешены, вы просто найдете (более или менее) список ограничений, которым должно следовать выполнение кандидата, и все, что делает это, разрешено. Ограничения достаточно жесткие, поэтому выполнение разреженного воздуха разрешено, потому что оно не запрещено. - person BeeOnRope; 09.07.2018
comment
Таким образом, все, что приводит к разрешенному выполнению, является честной игрой, но тогда возникает вопрос, есть ли разумное оборудование или компилятор, которые могли бы вызвать такой результат, а также не усложняет ли этот результат программу. Если ответ на оба вопроса одновременно положительный на оба вопроса относительно одного поведения: вам придется пойти на сложный компромисс. Однако в наиболее известных примерах ответы обычно понимаются как «нет» и «да» соответственно, что указывает на то, что в идеале такое поведение запрещено, но стандарт позволяет их, потому что это сложно указать. . @gil - person BeeOnRope; 09.07.2018

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

  (Thread #1)       |   (Thread #2)
r1 = x              |
[fence]             |
y = junk temporary  |
                    | r2 = y    // junk!
                    | [fence]
                    | x = r2
y = r1              |

Самый простой способ избежать случайных результатов - использовать атомарные целые числа: если x и y атомарны, то они не могут иметь значений «из воздуха»:

std::atomic_int x = 0, y = 0;
int r1 = x;    |    int r2 = y;
y = r1;        |    x = r2;
person davmac    schedule 08.07.2018
comment
Вам не нужно использовать seq_cst атомарную загрузку / хранение, не так ли? Это намного дороже, чем вам нужно, если вы просто хотите предотвратить ненужные значения. Я думаю, что для предотвращения переупорядочения LoadStore будет достаточно освободить хранилища или получить загрузку. (На самом деле вам нужно только победить прогнозирование значений, поэтому любая нормальная реализация, вероятно, безопасна с расслаблением и, безусловно, с mo_consume, но текущие реализации усиливают его до mo_acquire.) - person Peter Cordes; 09.07.2018
comment
Err на самом деле relaxed достаточно, чтобы предотвратить случайные значения, потому что компилятор не может изобретать записи в атомар. Я думал о том, чтобы сохранить правильное значение y, прежде чем увидеть его, нарушая причинно-следственную связь. - person Peter Cordes; 09.07.2018
comment
@PeterCordes нет, вам не нужна seq_cst атомарная загрузка / хранение, и да, я думаю, relaxed будет достаточно. Но для простоты примера ... - person davmac; 09.07.2018
comment
seq_cst дает вам столько (и стоит так дорого), что почти делает ответ неинтересным. Конечно, это безопасно. Я добавил ответ с помощью mo_relaxed. - person Peter Cordes; 09.07.2018
comment
В памяти C ++ 11 вы можете теоретически получать значения из воздуха, как описывает OP, даже с атомарными значениями (расслабленное чтение), это известный недостаток модели. На практике компиляторы вряд ли сгенерируют код, который использует это, и оборудование обычно не вызывает этого. Тем не менее, это недостаток, который они активно пытались исправить после C ++ 11. Хороший справочник. - person BeeOnRope; 09.07.2018