Состояние гонки на x86

Кто-нибудь может объяснить это утверждение:

shared variables
x = 0, y = 0

Core 1       Core 2
x = 1;       y = 1;
r1 = y;      r2 = x;

Как возможно иметь r1 == 0 и r2 == 0 на процессорах x86?

Источник "Язык параллелизма", Бартош Милевски.


person Ation    schedule 08.07.2011    source источник
comment
preshing.com/20120515/memory-reordering-caught-in-the -act использует ассемблерную версию этого примера с/без mfence для демонстрации переупорядочения памяти с объяснением того, почему это разрешено. В C/C++ это просто UB, если они не atomic; если они есть, то переупорядочение может произойти только с memory_order_release или слабее.   -  person Peter Cordes    schedule 26.03.2020
comment
К сожалению(?) тег memory-order является синонимом memory-barriers, поэтому этот вопрос о упорядочении памяти можно пометить только тегом, который предотвратит изменение порядка. (По крайней мере, для достаточно сильного барьера памяти...)   -  person Peter Cordes    schedule 26.03.2020


Ответы (3)


Проблема может возникнуть из-за оптимизации, включающей изменение порядка инструкций. Другими словами, оба процессора могут назначать r1 и r2 до присвоения переменных x и y, если они считают, что это даст лучшую производительность. Эту проблему можно решить, добавив барьер памяти, который усилит ограничение порядка.

Процитируем слайд-шоу, которое вы упомянули в своем сообщении:

Современные многоядерные процессоры/языки нарушают последовательную согласованность.

Что касается архитектуры x86, лучший ресурс для чтения — это Intel® 64 и IA-32. Руководство разработчика программного обеспечения для архитектур (глава 8.2 Порядок использования памяти). Разделы 8.2.1 и 8.2.2 описывают упорядочение памяти, реализованное процессорами семейства Intel486, Pentium, Intel Core 2 Duo, Intel Atom, Intel Core Duo, Pentium 4, Intel Xeon и P6: модель памяти, называемая процессор порядка, в отличие от порядка программ (строгого порядка) более старой архитектуры Intel386 (где инструкции чтения и записи всегда выполнялись в том порядке, в котором они появлялись в поток команд).

В руководстве описываются многие гарантии упорядочения модели памяти упорядочения процессора (например, Загрузки не переупорядочиваются с другими загрузками, Хранилища не переупорядочиваются с другими хранилищами, Хранилища не переупорядочивается со старыми загрузками и т. д.), но также описывает разрешенное правило переупорядочивания, которое вызывает состояние гонки в посте OP:

8.2.3.4 Загрузки могут быть переупорядочены с более ранними магазинами в другие места

С другой стороны, если исходный порядок инструкций был изменен:

shared variables
x = 0, y = 0

Core 1       Core 2
r1 = y;      r2 = x;
x = 1;       y = 1;

В этом случае процессор гарантирует, что ситуации r1 = 1 и r2 = 1 недопустимы (из-за гарантии 8.2.3.3 Stores Not Reordered with Early Load), что означает, что эти инструкции никогда не будут переупорядочены в отдельных ядрах.

Чтобы сравнить это с различными архитектурами, ознакомьтесь со статьей: Упорядочивание памяти в современных микропроцессорах< /а>. Вы можете видеть, что Itanium (IA-64) переупорядочивает даже больше, чем архитектура IA-32:

Возможные изменения порядка процессоров для различных архитектур

person Groo    schedule 08.07.2011
comment
Это возможно. Итак, первая операция — заменить переменную константой. И второе — получить значение переменной и установить ее в r[x]. Поэтому, вероятно, он попытается получить значение переменной одновременно с выполнением первой операции. - person Ation; 08.07.2011
comment
Компилятор также может быть виновником, ср. «Как неправильно компилировать программы с помощью безопасных гонок данных» — usenix.org/events /hotpar11/tech/final_files/Boehm.pdf. - person Brian; 08.07.2011
comment
@Brian: Хотя интересно, многие примеры в этой статье являются ложными (недопустимые преобразования, которые может сделать компилятор) для C/POSIX из-за семантики обработки сигналов, особенно если пропущенный код содержит вызовы sigprocmask. - person R.. GitHub STOP HELPING ICE; 08.07.2011

На процессорах с более слабой моделью согласованности памяти (таких как SPARC, PowerPC, Itanium, ARM и т. д.) указанное выше условие может иметь место из-за отсутствия принудительной когерентности кэша при записи без явной инструкции барьера памяти. Таким образом, в основном Core1 видит запись на x до y, а Core2 видит запись на y до x. В этом случае не требуется полная инструкция ограждения ... в основном вам нужно будет только применить семантику записи или освобождения с этим сценарием, чтобы все записи были зафиксированы и видны всем процессорам до того, как произойдет чтение тех переменных, которые были написано на. Процессорные архитектуры со строгой моделью согласованности памяти, такие как x86, обычно делают это ненужным, но, как указывает Гру, сам компилятор может изменить порядок операций. Вы можете использовать ключевое слово volatile в C и C++, чтобы предотвратить переупорядочивание операций компилятором в данном потоке. Это не означает, что volatile создаст потокобезопасный код, который управляет видимостью операций чтения и записи между потоками... для этого потребуется барьер памяти. Таким образом, хотя использование volatile может по-прежнему создавать небезопасный многопоточный код, внутри данного потока будет обеспечиваться последовательная согласованность на уровне соблюдаемого машинного кода.

person Jason    schedule 08.07.2011
comment
ЦП также может переупорядочивать инструкции (сохраняя согласованность кеша с новым порядком). - person Alexandre C.; 08.07.2011
comment
Volatile не предотвратит переупорядочивание, за исключением старых компиляторов, которые использовали volatile в качестве ключевого слова для полного отключения оптимизатора. Вам нужна инструкция забора памяти. - person dascandy; 08.07.2011
comment
Если бы volatile не предотвратил переупорядочивание компилятора в заданном потоке, то это было бы абсолютно бессмысленно для использования в отображенном на память вводе-выводе... компилятор все равно переупорядочил бы операции чтения и записи, и вы бы получили всевозможные неопределенное поведение оборудования. Ключевое слово volatile не предотвращает переупорядочивание инструкций ЦП и не обеспечивает видимость операций чтения и записи между потоками... что требует барьера памяти... но не позволяет самому компилятору переупорядочивать операции в данном потоке как определено в исходном коде. - person Jason; 08.07.2011

Вот почему некоторые говорят: треды считаются вредными.

Проблема в том, что ни один из потоков не обеспечивает какого-либо порядка между своими двумя операторами, потому что они не зависят друг от друга.

  • Компилятор знает, что x и y не являются псевдонимами, поэтому не требуется упорядочивать операции.

  • ЦП знает, что x и y не являются псевдонимами, поэтому он может изменить их порядок для ускорения. Хорошим примером того, когда это происходит, является случай, когда ЦП обнаруживает возможность для объединения записей. Он может объединять одну запись с другой, если может сделать это, не нарушая своей модели согласованности.

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

person DigitalRoss    schedule 09.07.2011