Предположим, у меня есть такая структура:
volatile struct { int foo; int bar; } data;
data.foo = 1;
data.bar = 2;
data.foo = 3;
data.bar = 4;
Гарантируется ли, что все задания не будут переупорядочены?
Например, без volatile компилятору явно будет разрешено оптимизировать его как две инструкции в другом порядке, например:
data.bar = 4;
data.foo = 3;
Но с volatile требуется ли, чтобы компилятор не делал что-то подобное?
data.foo = 1;
data.foo = 3;
data.bar = 2;
data.bar = 4;
(Обработка членов как отдельных несвязанных изменчивых объектов - и выполнение переупорядочения, которое, как я могу себе представить, может попытаться улучшить локальность ссылки, например, в случае, если foo и bar находятся на границе страницы.)
Кроме того, соответствует ли ответ текущим версиям стандартов C и C++?
Полная цитата здесь не переупорядочена для C++ (C может быть другим) — en.cppreference.com/w/cpp/language/cv «объект, тип которого является изменчивым, или подобъект изменчивого объекта».. . _ "Каждый доступ (операция чтения или записи, вызов функции-члена и т. д.), выполненный через выражение glvalue типа volatile-qualified, рассматривается как видимый побочный эффект в целях оптимизации"
Если речь идет о C++ и «параллелизме» как таковом (как говорится в теге), проверьте std::atomic. Он имеет аналогичные гарантии отсутствия повторного заказа.
@bloody: К сожалению, типы volatile std::atomic ведут себя нелогично, по крайней мере, в текущих компиляторах. Например здесь загрузка из volatile std::atomic<int> оптимизирована, потому что ее значение не используется, хотя это не было бы для обычного volatile int.
@NateEldredge Я никогда не думал о том, чтобы присоединиться к std::atomic с volatile. Если op предоставляет эту структуру для взаимодействия с вводом-выводом, то использование volatile несомненно. Однако тег op предполагает, что речь идет о параллелизме (многопоточная программа), и в этом случае std::atomic является правильным инструментом для использования, а не volatile. Возможно, это просто свободный стиль именования тегов.
@bloody в первую очередь я смотрю на C, но, поскольку между языками часто есть тонкие различия (C++, похоже, давно отошел от цели стать надмножеством), мне любопытно, в частности, volatile, поскольку это применимо к переносимости C код на С++. Да, C++ действительно имеет гораздо лучшие библиотеки для решения подобных задач.
@NateEldredge Это обязательное поведение, оно связано с выражениями с отброшенными значениями и тем, что представляет собой чтение. С другой стороны, вы все равно не должны volatile std::atomic в первую очередь.
Компилятор не обязан что-либо делать, то, что представляет собой изменчивый доступ, определяется реализацией, стандарт просто определяет определенное отношение упорядочения доступа с точки зрения наблюдаемого поведения и абстрактной машины для ссылки на документацию по реализации. Генерация кода не рассматривается стандартом.





Они не будут переупорядочены.
C17 6.5.2.3 (3) говорит:
Постфиксное выражение, за которым следует расширение . оператор и идентификатор обозначают член структуры или объект объединения. Значением является значение именованного члена, 97) и lvalue, если первое выражение lvalue. Если первое выражение имеет уточненный тип, результат имеет уточненную версию типа. назначенного члена.
Поскольку data имеет volatile-квалифицированный тип, то же самое имеют data.bar и data.foo. Таким образом, вы выполняете два назначения объектам volatile int. И согласно 6.7.3 сноске 136,
Действия над объектами, объявленными таким образом [как
volatile], не должны быть «оптимизированы» реализации или переупорядочены, за исключением случаев, разрешенных правилами вычисления выражений.
Более тонкий вопрос заключается в том, может ли компилятор назначить их обоих с помощью одной инструкции, например, если они являются непрерывными 32-битными значениями, может ли он использовать 64-битное хранилище для установки обоих? Я бы так не думал, и, по крайней мере, GCC и Clang не пытаются.
Спасибо за цитирование стандарта (поскольку у меня нет копии). Кажется, это отвечает на вопрос, но ваш текст «вы назначаете два объекта volatile int» вводит в заблуждение, поскольку, если бы они не считались одним и тем же объектом, ответ был бы быть другим, или для компилятора потребовалось бы дополнительное ограничение, чтобы сохранить порядок доступов, которые являются изменчивыми, даже если они находятся в несвязанных объектах. Может быть, лучше сохранить цитату и уточнить текст ответа...
Я думаю, что изменение операций на одновременные (использование одной инструкции для двух назначений) должно считаться изменением порядка. Если не в соответствии со строгой интерпретацией стандарта, то, безусловно, в соответствии с духом стандарта причина такого ограничения (которое влечет за собой снижение производительности) применяется независимо от того, обманываете ли вы формулировку.
@TedShaneyfelt: перефразировано на «два задания volatile int объектам».
Вы имеете в виду два присваивания одному и тому же изменчивому объекту int? Это было бы удовлетворительно.
Обратите внимание, что они являются разными частями одного и того же объекта, а не отдельными изменчивыми объектами, а одним и тем же объектом, как указано в первой цитате спецификации, которую вы дали...
@Ben должен быть прав насчет одновременного изменения порядка. Изменение операций на одновременные повлияет на аппаратное обеспечение, например, установка битов данных, а затем переключение бита строба на отображенном в память вводе-выводе, очевидно, было бы оптимизировано, если бы это было разрешено.
То, что представляет собой доступ к volatile-объекту, определяется реализацией. Если реализация C нацелена на оборудование, на котором эффекты 64-битной записи могут быть такими же, как две 32-битные записи (например, две 32-битные записи могут быть видны другим компонентам, использующим общую память, по отдельности, но они могут рассматриваться как неразличимы, поэтому 64-битная запись, которая обязательно выполняется одновременно, неотличима от двух 32-битных операций записи, которые фактически выполняются одновременно), то для реализации может быть разумно определить «доступ», чтобы 64-битная запись могла быть использовал.
@Eric Postpischil, в этом случае они не будут написаны одновременно, даже если они оптимизированы для одной инструкции. Тогда вроде нормально. Но если бы они были различимы, как это было бы, если бы строб стал активным во время записи данных, а не позже, то это было бы неправильно переупорядочено, чтобы быть одновременным. Компилятору необходимо будет принять во внимание, является ли выравнивание таким, что он может обойтись без одной инструкции записи, разделенной на два доступа для записи данных.
@TedShaneyfelt: члены типа структуры сами по себе являются объектами. 6.2.5 (20): «Тип структуры описывает последовательно размещаемый непустой набор объектов-членов». Таким образом, мы действительно выполняем два доступа к volatile-объектам, и они оказываются разными объектами, хотя оба они также являются частью объекта data. Я изменил формулировку, чтобы было понятно, что переупорядочивание все равно будет запрещено даже для двух обращений к одному и тому же объекту (что в данном случае не так).
1. Да, конечно, отдельные члены объекта сами являются объектами. 2. Да, стандарт в сноске 136 явно запрещает оптимизацию доступа, например: data.bar=4; data.foo=3; подойдет. 3. Сноску можно интерпретировать как «Действия над [любым из] объектов, объявленных таким образом [как изменчивые], не должны быть «оптимизированы» реализацией или переупорядочены, за исключением случаев, разрешенных правилами вычисления выражений. [но его отношение к другие подобные объекты здесь не учитываются], поэтому тот факт, что они являются частью одного и того же объекта, представляется существенным.
По поводу сохранения порядка операций... open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html
Если вы хотите использовать это в нескольких потоках, есть одна существенная ошибка.
Хотя компилятор не будет переупорядочивать записи в переменные volatile (как описано в ответе Нейта Элдреджа), есть еще одна точка, где может произойти переупорядочение записи, и это сам ЦП. Это зависит от архитектуры ЦП, и ниже приведены несколько примеров:
См. Технический документ по заказу памяти для архитектуры Intel® 64.
Пока сами инструкции магазина не переупорядочиваются (2.2):
- Магазины не переупорядочиваются с другими магазинами.
Они могут быть видны разным процессорам в разном порядке (2.4):
Упорядочение памяти Intel 64 позволяет видеть записи двух процессоров в разном порядке по эти два процессора
AMD 64 (обычный x64) имеет аналогичное поведение в спецификации:
Как правило, запись не по порядку не допускается. Инструкции записи, выполняемые не по порядку, не могут зафиксировать (записать) свой результат в память, пока все предыдущие инструкции не будут завершены в порядке программы. Однако процессор может удерживать результат неправильной инструкции записи в частном буфере (невидимом для программного обеспечения) до тех пор, пока этот результат не будет зафиксирован в памяти.
Я помню, что мне приходилось быть осторожным с этим на Xbox 360, в котором использовался процессор PowerPC:
Хотя ЦП Xbox 360 не меняет порядок инструкций, он меняет порядок операций записи, которые завершаются после самих инструкций. Эта перестановка операций записи специально разрешена моделью памяти PowerPC.
Чтобы избежать переупорядочения ЦП переносимым способом, вам нужно использовать границы памяти , такие как C++11 std::atomic_thread_fence или C11 atomic_thread_fence. Без них порядок записи, видимый из другого потока, может быть другим.
Это также отмечено в статье Википедии Барьер памяти:
Кроме того, не гарантируется, что операции чтения и записи volatile будут отображаться в том же порядке другими процессорами или ядрами из-за кэширования, протокола когерентности кеша и упрощенного порядка памяти, что означает, что сами по себе volatile-переменные могут даже не работать как межпоточные флаги или мьютексы. .
«Это поднимает вопрос о том, следует ли придавать volatile реальное значение, которое обеспечивает как атомарность, так и видимость между потоками, примерно по аналогии с volatile в Java. Хотя мы считаем, что абстрактно это обеспечивает существенное улучшение, придавая семантику тому, что в настоящее время имеет почти нет переносимой семантики, похоже, существует ряд практических препятствий, вызванных проблемами обратной совместимости, которые заставляют нас, по крайней мере, колебаться». - Ханс Бём и Ник Макларен open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html ...
Обеспокоенность Boehm & Maclaren, возможно, могла бы решить комитет по стандартам, добавив синтаксическую конструкцию, в рамках которой летучие объекты будут вынуждены вести себя в большей степени в соответствии с духом летучести, чего они не решаются требовать по причинам обратной совместимости. например Новый синтаксис: volatile { block } будет достаточным дополнением к языку, чтобы обеспечить обратную совместимость, а также обеспечить более интуитивно понятное и полезное поведение volatile объектов в этом блоке. Как и в случае с пространством имен, лучше всего позволить ему охватывать несколько определений функций. Как то это тупо.
Если вы используете это из нескольких потоков, у вас есть гонка данных, и все ставки отключены. В отличие от типов atomic, объекты volatile не являются потокобезопасными и не избегают гонок данных. Единственным жизнеспособным использованием volatile в наши дни является доступ к аппаратным устройствам с отображением памяти, и в этом случае у вас обычно будет память, помеченная как «некэшируемая» каким-то специфичным для машины способом, который должен препятствовать переупорядочиванию ЦП и гарантировать, что устройство видит загрузки и сохраняет в порядке программы (на уровне сборки).
Я не знаю, но я очень на это надеюсь, иначе структуры очереди, которые я использую для прерывания связи, могут быть в беде :)