Переупорядочивание инструкций на Intel

Я пытаюсь понять переупорядочение инструкций на следующем простом примере:

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, а запись происходит не в основную память, а в кеш, они могут быть.

Как вы думаете, почему a и b будут храниться в объем памяти? В языке C++ такого требования нет, в крайнем случае компилятор может сгенерировать код, который сохранит эти два кода только в регистрах (например).

Ped7g 08.08.2018 13:46

@ Ped7g Да, я задал не тот вопрос ... Простите

St.Antario 08.08.2018 13:47

Нет проблем, поиск правильного вопроса - это часть процесса, и иногда этот процесс требует таких шагов, как этот. :) Но тема может быть довольно сложной, поэтому вам придется быть более точным, чтобы получить какой-то осмысленный ответ ..

Ped7g 08.08.2018 13:48

вам нужно предотвратить здесь переупорядочение компилятора. скажем, вставив a = 1; _ReadWriteBarrier(); b = 1; на x86 / x64, этого будет достаточно

RbMm 08.08.2018 13:51

Это на самом деле C или C++, или вы действительно спрашиваете о сборке, которая выполняет эти операции в указанном порядке? Потому что, очевидно, это UB в C, и для переупорядочения во время компиляции и подъема нагрузки применяется модель памяти C, а не модель памяти x86. IIRC, вы сделали это в предыдущем вопросе и зря потратили время на сортировку моделей памяти C и x86, поэтому пожалуйста, укажите в вопросе, который вы действительно имеете в виду, если asm выглядит так. Очевидно, что while(b == 0){} превратится в бесконечный цикл с любым нормальным компилятором C, если он вообще войдет в цикл.

Peter Cordes 08.08.2018 15:05

@PeterCordes Речь не шла о переупорядочении компилятора C и статического компилятора. Я имел в виду, что foo и bar выполняются разными потоками. Линия b может находиться в исключительном состоянии ядра, на котором выполняется поток, выполняющий foo, а линия a находится в общем состоянии на обоих ядрах. Таким образом, запись в a помещается в буфер хранения, а сообщение о недействительности чтения отправляется в ядро ​​2, которое может быть удалено позже, когда выполняется assert(a == 1).

St.Antario 08.08.2018 15:41

@ St.Antario: Тогда не помечайте его c и не публикуйте синтаксически корректный C без каких-либо объяснений того, что код нет на самом деле C. Или же исправьте ваш C, чтобы использовать хранилища релизов и получать нагрузки (которые отображаются непосредственно на инструкции x86), или как минимум volatile. Вы уже потратили время Вирсавии на написание ответа о UB. (И, кстати, простого удаления тега [c] едва ли достаточно. Все знают, как выглядит C, и что неатомарные переменные можно оптимизировать в не разделяемые регистры, так что это большое отвлечение.)

Peter Cordes 08.08.2018 15:44

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

St.Antario 08.08.2018 15:46

Если вы фактически сделали то, что вы имели в виду в x86 asm, просмотр b!=0 гарантирует, что вы также увидите a==1, потому что mov [b], 1 - это хранилище релизов (как и все хранилища x86, кроме NT), потому что x86 требует, чтобы хранилища фиксировались из буфера хранилища в L1d. в программном порядке. (Таким образом, изменение порядка нет разрешено. Только переупорядочение во время компиляции может нарушить этот пример на x86). Но если вы скомпилировали этот недопустимый C, это не так. Вы утверждаете, что фактически воспроизвели переупорядочение, о котором говорите? Если да, опубликуйте минимальный воспроизводимый пример.

Peter Cordes 08.08.2018 15:49

@PeterCordes Если вы действительно сделали то, что имели в виду, в x86 asm, увидев b! = 0, вы бы также увидели a == 1 Так как насчет случая, который я описал выше? Если a все еще находится в общем состоянии и стал недействительным после чтения?

St.Antario 08.08.2018 15:51

Только переупорядочение во время компиляции может сломать этот пример на x86. a не может по-прежнему находиться в общем состоянии после того, как b=1 виден; это будет означать, что поток, выполняющий foo, позволит переупорядочить свои хранилища.

Peter Cordes 08.08.2018 15:53

@PeterCordes Нет, я не утверждаю этого. Я пытался воспроизвести и не смог. Вот почему я спрашиваю.

St.Antario 08.08.2018 15:53

@PeterCordes a can't still be in shared state after b=1 is visible. Можете ли вы предоставить соответствующую цитату из документа Intel?

St.Antario 08.08.2018 15:58

@ Сент-Антарио: вы уже сделали: Записи в память не переупорядочиваются с другими записями.

Peter Cordes 08.08.2018 16:00
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
14
302
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Если один поток выполнял foo, а другой - bar, то поведение вашей программы будет неопределенный.

Вам не разрешено выполнять одновременное чтение и запись неатомарной переменной, такой как int.

Так что в этом случае допускается изменение инструкции.

@RbMm Если соответственно выровнять ... Но все равно UB.

St.Antario 08.08.2018 14:03

да, согласен с раскладом. но я не вижу никаких прагм в коде, которые в этом случае перезаписывают выравнивание по умолчанию. поэтому примите естественное выравнивание для int. и в этом случае не просматривать никаких уб. конечно, нужно заставить компилятор не переупорядочивать a=1,b=1

RbMm 08.08.2018 14:06
Ответ принят как подходящий

Ваше предположение неверно. Только переупорядочение во время компиляции может сломать этот пример на 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 08.08.2018 16:00

@ St.Antario: Я повторно добавил тег C, потому что это единственная причина, по которой я могу представить ваше утверждение, что "Известно, что в этом примере утверждение может не выполняться.". См. preshing.com/20120930/weak-vs-strong-memory-models и preshing.com/20120625/memory-ordering-at-compile-time.

Peter Cordes 08.08.2018 16:02

Другие вопросы по теме