Я пытаюсь понять переупорядочение инструкций на следующем простом примере:
int a;
int b;
void foo(){
a = 1;
b = 1;
}
void bar(){
while(b == 0) continue;
assert(a == 1);
}
Известно, что в этом примере утверждение может завершиться ошибкой, если один поток выполняет foo
, а другой - bar
. Но я не понимаю почему. Я проконсультировался с Руководство Intel Vol. 3А, 8.2.2 и обнаружил следующее:
Writes to memory are not reordered with other writes, with the following exceptions:
— streaming stores (writes) executed with the non-temporal move instructions (MOVNTI, MOVNTQ, MOVNTDQ, MOVNTPS, and MOVNTPD); and
— string operations (see Section 8.2.4.1).
Здесь нет строковых операций, да и инструкций по перемещению NT
я не заметил. Итак ... Почему возможно изменение порядка записи?
Или память имеет значение в
Writes to memory are not reordered
? Итак, когда у нас есть кэшированные a
и b
, а запись происходит не в основную память, а в кеш, они могут быть.
@ Ped7g Да, я задал не тот вопрос ... Простите
Нет проблем, поиск правильного вопроса - это часть процесса, и иногда этот процесс требует таких шагов, как этот. :) Но тема может быть довольно сложной, поэтому вам придется быть более точным, чтобы получить какой-то осмысленный ответ ..
вам нужно предотвратить здесь переупорядочение компилятора. скажем, вставив a = 1; _ReadWriteBarrier(); b = 1;
на x86 / x64, этого будет достаточно
Это на самом деле C или C++, или вы действительно спрашиваете о сборке, которая выполняет эти операции в указанном порядке? Потому что, очевидно, это UB в C, и для переупорядочения во время компиляции и подъема нагрузки применяется модель памяти C, а не модель памяти x86. IIRC, вы сделали это в предыдущем вопросе и зря потратили время на сортировку моделей памяти C и x86, поэтому пожалуйста, укажите в вопросе, который вы действительно имеете в виду, если asm выглядит так. Очевидно, что while(b == 0){}
превратится в бесконечный цикл с любым нормальным компилятором C, если он вообще войдет в цикл.
@PeterCordes Речь не шла о переупорядочении компилятора C и статического компилятора. Я имел в виду, что foo
и bar
выполняются разными потоками. Линия b
может находиться в исключительном состоянии ядра, на котором выполняется поток, выполняющий foo
, а линия a
находится в общем состоянии на обоих ядрах. Таким образом, запись в a
помещается в буфер хранения, а сообщение о недействительности чтения отправляется в ядро 2, которое может быть удалено позже, когда выполняется assert(a == 1)
.
@ St.Antario: Тогда не помечайте его c и не публикуйте синтаксически корректный C без каких-либо объяснений того, что код нет на самом деле C. Или же исправьте ваш C, чтобы использовать хранилища релизов и получать нагрузки (которые отображаются непосредственно на инструкции x86), или как минимум volatile
. Вы уже потратили время Вирсавии на написание ответа о UB. (И, кстати, простого удаления тега [c]
едва ли достаточно. Все знают, как выглядит C, и что неатомарные переменные можно оптимизировать в не разделяемые регистры, так что это большое отвлечение.)
@PeterCordes Фактически, из-за вещей, связанных с буфером хранилища и кеш-памятью, другой поток наблюдал записи, сделанные foo
в другом порядке. Тем не менее, в процитированном мною руководстве ясно сказано, что запись в память не может быть переупорядочена другой записью.
Если вы фактически сделали то, что вы имели в виду в x86 asm, просмотр b!=0
гарантирует, что вы также увидите a==1
, потому что mov [b], 1
- это хранилище релизов (как и все хранилища x86, кроме NT), потому что x86 требует, чтобы хранилища фиксировались из буфера хранилища в L1d. в программном порядке. (Таким образом, изменение порядка нет разрешено. Только переупорядочение во время компиляции может нарушить этот пример на x86). Но если вы скомпилировали этот недопустимый C, это не так. Вы утверждаете, что фактически воспроизвели переупорядочение, о котором говорите? Если да, опубликуйте минимальный воспроизводимый пример.
@PeterCordes Если вы действительно сделали то, что имели в виду, в x86 asm, увидев b! = 0, вы бы также увидели a == 1 Так как насчет случая, который я описал выше? Если a
все еще находится в общем состоянии и стал недействительным после чтения?
Только переупорядочение во время компиляции может сломать этот пример на x86. a
не может по-прежнему находиться в общем состоянии после того, как b=1
виден; это будет означать, что поток, выполняющий foo
, позволит переупорядочить свои хранилища.
@PeterCordes Нет, я не утверждаю этого. Я пытался воспроизвести и не смог. Вот почему я спрашиваю.
@PeterCordes a can't still be in shared state after b=1 is visible
. Можете ли вы предоставить соответствующую цитату из документа Intel?
@ Сент-Антарио: вы уже сделали: Записи в память не переупорядочиваются с другими записями.
Если один поток выполнял foo
, а другой - bar
, то поведение вашей программы будет неопределенный.
Вам не разрешено выполнять одновременное чтение и запись неатомарной переменной, такой как int
.
Так что в этом случае допускается изменение инструкции.
@RbMm Если соответственно выровнять ... Но все равно UB.
да, согласен с раскладом. но я не вижу никаких прагм в коде, которые в этом случае перезаписывают выравнивание по умолчанию. поэтому примите естественное выравнивание для int. и в этом случае не просматривать никаких уб. конечно, нужно заставить компилятор не переупорядочивать a=1,b=1
Ваше предположение неверно. Только переупорядочение во время компиляции может сломать этот пример на x861.
Хранилища asm x86 - это хранилища релизов. Они могут выполнять фиксацию из буфера хранилища в кэш L1d только в программном порядке.
a
не может по-прежнему находиться в общем состоянии после того, как b=1
виден; это будет означать, что поток, выполняющий foo
, позволит своим хранилищам выполнить фиксацию не по порядку. Это то, что означает Записи в память не переупорядочиваются с другими записями для сохранения в кэшируемой памяти.
Если он находится в общем состоянии опять таки после того, как RFO был признан недействительным из потока, выполняющего foo
, тогда он будет иметь обновленное значение a
.
Сноска 1. Конечно, спин-цикл оптимизируется в if (b==0) infinite_loop
, потому что UB гонки данных позволяет компилятору поднять нагрузку. См. Программирование MCU - оптимизация C++ O2 прерывается, пока цикл.
Кажется, вы спрашиваете о правилах C, предполагая, что код будет наивно / напрямую переведен на x86 asm. Вы можете получить это с расслабленной атомикой, но не с volatile
, потому что доступы volatile
не могут быть переупорядочены (во время компиляции) с другими доступами volatile
.
Как я уже сказал, я ошибочно предоставил пример в C
, имея в виду сборку x86.
@ St.Antario: Я повторно добавил тег C, потому что это единственная причина, по которой я могу представить ваше утверждение, что "Известно, что в этом примере утверждение может не выполняться.". См. preshing.com/20120930/weak-vs-strong-memory-models и preshing.com/20120625/memory-ordering-at-compile-time.
Как вы думаете, почему
a
иb
будут храниться в объем памяти? В языке C++ такого требования нет, в крайнем случае компилятор может сгенерировать код, который сохранит эти два кода только в регистрах (например).