Могут ли переупорядочиваться обращения к летучим?

Рассмотрим следующую последовательность записей в память volatile, которую я взял из статьи Дэвида Чисналла в InformIT, "Понимание C11 и С++ 11 Atomics":

volatile int a = 1;
volatile int b = 2;
             a = 3;

Мое понимание из С++ 98 состояло в том, что эти операции не могли быть переупорядочены, на С++ 98 1.9:

конформным реализации требуется, чтобы эмулировать (только) наблюдаемое поведение абстрактной машины как объяснено ниже... Наблюдаемое поведение абстрактной машины - это ее последовательность чтения и записи в изменчивые данные и вызовы функций библиотечного ввода/вывода

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

a = 1;
a = 3;
b = 2;

Или это:

b = 2;
a = 1;
a = 3;

С++ 11 повторяет формулировку С++ 98, в которой

конформным реализации требуется, чтобы эмулировать (только) наблюдаемое поведение абстрактной машины, как объяснено ниже.

но говорит об этом volatile (1.9/8):

Доступ к неустойчивым объектам оценивается строго в соответствии с правилами абстрактной машины.

1.9/12 говорит, что доступ к значению volatile glvalue (который включает в себя переменные a, b и c выше) является побочным эффектом, а 1.9/14 говорит, что побочные эффекты в одном полном выражение (например, оператор) должно предшествовать побочным эффектам более позднего полного выражения в том же потоке. Это приводит меня к выводу, что два переупорядочения Chisnall недействительны, поскольку они не соответствуют порядку, продиктованному абстрактной машиной.

Я что-то пропускаю или ошибаюсь в Chisnall?

(Обратите внимание, что это не вопрос о потоке. Вопрос заключается в том, разрешено ли компилятору переупорядочивать обращения к различным переменным volatile в одном потоке.)

Ответ 1

Интерпретация IMO Chisnalls (как представлено вами) явно ошибочна. Простейшим случаем является С++ 98. sequence of reads and writes to volatile data необходимо сохранить, и это относится к упорядоченной последовательности чтения и записи любых изменчивых данных, а не к одной переменной.

Это становится очевидным, если вы считаете оригинальную мотивацию для изменчивых: ввода-вывода с отображением памяти. В mmio у вас обычно есть несколько связанных регистров в разных ячейках памяти, и для протокола устройства ввода/вывода требуется определенная последовательность чтения и записи в свой набор регистров. Важен порядок между регистрами.

В формулировке С++ 11 избегает говорить об абсолютном sequence of reads and writes, потому что в многопоточных средах нет ни одной четко определенной последовательности таких событий по потокам, и это не проблема, если эти обращения идут к независимым ячейкам памяти. Но я считаю, что намерение заключается в том, что для любой последовательности волатильных доступов к данным с четко определенным порядком правила остаются такими же, как для С++ 98 - заказ должен быть сохранен, независимо от того, сколько разных мест доступно в этой последовательности.

Это совершенно отдельная проблема, которая влечет за собой реализацию. Как (и даже , если) наблюдается нестабильный доступ к данным из-за пределов программы и как порядок доступа программы к внешним наблюдаемым событиям не указан. Реализация должна, вероятно, дать вам разумную интерпретацию и разумные гарантии, но то, что разумно, зависит от контекста.

Стандарт С++ 11 оставляет место для расчётов данных между несинхронизированными волатильными обращениями, поэтому нет ничего, что окружало бы их полными заборами памяти или аналогичными конструкциями. Если есть части памяти, которые действительно используются в качестве внешнего интерфейса - для ввода/вывода с памятью или DMA - тогда может быть разумным, чтобы реализация дала вам гарантии того, как волатильный доступ к этим частям подвергается потребляющим устройствам.

Можно гарантировать, что одна гарантия может быть выведена из стандарта (см. [in.execution]): значения типа volatile std::sigatomic_t должны иметь значения, совместимые с порядком их записи, даже в обработчике сигналов - по крайней мере, многопоточная программа.

Ответ 2

Вы правы, он ошибается. Доступ к различным изменчивым переменным не может быть переупорядочен компилятором, если они встречаются в отдельных полных выражениях, то есть разделены тем, что С++ 98 называется точкой последовательности, или в терминах С++ 11 один доступ секвенирован перед другим.

Chisnall, похоже, пытается объяснить, почему volatile бесполезен для написания потокобезопасного кода, показывая простую реализацию мьютекса, основанную на volatile, которая будет нарушена переупорядочением компилятора. Он прав, что volatile бесполезен для безопасности потоков, но не по причинам, которые он дает. Это не потому, что компилятор может переупорядочить обращения к объектам volatile, а потому, что ЦП может их переупорядочить. Атомные операции и барьеры памяти препятствуют компилятору и процессору переупорядочивать вещи через барьер, если это необходимо для обеспечения безопасности потоков.

См. нижнюю правую ячейку таблицы 1 в информационном справочнике Sutter volatile vs volatile.

Ответ 4

Это зависит от вашего компилятора. Например, MSVС++ по сравнению с Visual Studio 2005 гарантирует, что * volatiles не будет переупорядочиваться (на самом деле то, что сделала Microsoft, и отказаться от программ, будет навсегда злоупотреблять volatile - MSVС++ теперь добавляет барьер памяти в отношении определенных способов использования volatile). Другие версии и другие компиляторы могут не иметь таких гарантий.

Короче говоря: не делай ставку на него. Создайте свой код правильно и не злоупотребляйте волатильностью. Вместо этого используйте барьеры памяти или полноразмерные мьютексы. С++ 11 atomic помогут.

Ответ 5

На данный момент я предполагаю, что ваш a=3 является просто ошибкой в ​​копировании и вставке, и вы действительно подразумевали, что они были c=3.

Реальный вопрос здесь - одна из отличий между оценкой и то, как все становится видимым для другого процессора. Стандарты описывают порядок оценки. С этой точки зрения вы полностью правы - заданные назначения a, b и c в этом порядке, назначения должны оцениваться в этом порядке.

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

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

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

Это не тот случай со всем оборудованием. Несмотря на то, что эта строгая модель упрощает работу, она также довольно дорога как с точки зрения дополнительного оборудования, так и для обеспечения согласованности и простой скорости, когда/если у вас много процессоров.

Изменить: если на мгновение игнорировать потоки, вопрос становится немного проще - но не очень. Согласно С++ 11, §1.9/12:

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

Таким образом, доступ к энергозависимым объектам должен быть инициирован по порядку, но не обязательно завершен в порядке. К сожалению, это часто завершение, которое внешне видимо. Таким образом, мы в значительной степени возвращаемся к обычному правилу as-if: компилятор может переупорядочить все так, как он хочет, поскольку он не производит никаких видимых внешних изменений.

Ответ 6

С++ 98 не говорит, что инструкции не могут быть переупорядочены.

Наблюдаемое поведение абстрактной машины - это ее последовательность чтения и записи в энергозависимые данные и вызовы функций библиотечного ввода/вывода

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

Проще говоря, это ничего не значит. Нет никакого "одного места" для наблюдения за порядками чтения и записи (Шина RAM? Шина процессора? Между кэшами L1 и L2? Из другого потока? Из другого ядра?), Поэтому это требование по сути бессмысленно.

Версии С++ перед любыми ссылками на потоки явно не указывают поведение изменчивых переменных, как видно из другого потока. И С++ 11 (мудро, IMO) не изменил этого, но вместо этого представил разумные атомарные операции с четко определенной семантикой межпоточных.

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

Обновление. Я вижу несколько downvotes, потому что людям не нравится эта истина. К сожалению, это правда.

Если стандарт С++ запрещает компилятору переупорядочивать обращения к отдельным летучим, то теория о том, что порядок таких доступов является частью наблюдаемого поведения программы, также требует от компилятора испускать код, который запрещает ЦП делать это, Стандарт не различает то, что делает компилятор, и то, что делает скомпилированный код компилятора, делает CPU.

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