В этом случае имеет значение неопределенный отбор?

Я увидел образец кода, демонстрирующий использование квалификатора volatile в ответе на вопрос функции летучих элементов С++, приведенные ниже:

volatile int x;
int DoSomething() {
 x = 1;
 DoSomeOtherStuff();
 return x+1; // Don't just return 2 because we stored a 1 in x. 
 // Check to get its current value
}

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

Я думаю, что это отличается от следующего случая:

volatile int x;
int DoSomething() {
 x = 1;
 while (x != 1)
 break;
}

В этом случае мы повторно читаем x сразу после записи в x, поэтому volatile необходимо, чтобы получить обновленное значение x, написанное другими потоками.

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

ИЗМЕНИТЬ (ответьте на комментарии): Извините, я не задал вопрос. Что касается первого фрагмента кода, я задаюсь вопросом, является ли код допустимым примером для возможного использования volatile (а не для гарантированного использования volatile). Я просто хочу знать, без volatile, гарантировало ли это, что любое возможное изменение x в DoSomeOtherStuff() может быть отражено в return x+1, если не считать многопотоковых или других нетривиальных вещей, таких как отображение IO памяти, Причина, гарантирующая, что она будет работать без volatile, тогда пример не подходит, не говоря уже о зависящем от платформы характере volatile, как указывалось в некоторых комментариях. Но если это не гарантировано, я боюсь, что некоторые из моих существующих кодов могут работать не так, как я ожидал. (Я, вероятно, не должен был помещать второй фрагмент кода вообще.)

5 ответов

Отборочный volatile никогда не имеет значения в значении самого кода. Если компилятор не может доказать, что DoSomeOtherStuff() не изменяет x, он должен перечитать x независимо, volatile или нет. Для volatile, чтобы быть релевантным, x должен был бы быть чем-то вроде IOP с отображением памяти, который может измениться вне программы. Если мы предположим, что это регистр, который увеличивается каждый микросекунда, например:

int
MeasureExecutionTime()
{
 x = 0;
 DoSomeOtherStuff();
 return x;
}

будет возвращать время, используемое в DoSomeOtherStuff; компилятор должен будет перезагрузить его, даже если он встроен DoSomeOtherStuff и увидел, что он никогда не изменял x.

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

EDIT:

Что касается второго фрагмента кода: как обычно, volatile не гарантирует, что вы получите обновленную копию x. Возможно, это не соответствует volatile, но это способ g++, Sun CC и, по крайней мере, некоторые версии VС++. Компиляторы выдадут нагрузку инструкции читать x каждый раз в цикле, но аппаратное обеспечение может найти значение уже в конвейере, а не распространять которые считывают запрос на шину памяти. Чтобы гарантировать новое чтение, компилятор должен будет вставить забор или инструкция membar.

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

Чтобы понять volatile, важно понять с которой он был введен. Когда он был введен, памяти и т.д. были неизвестны, а стандарт (C) игнорируются потоки (или несколько процессов с разделяемой памятью). Цель volatile состояла в том, чтобы разрешить поддержку сопоставленной памяти IO, где запись на адрес имела внешние последствия, и последовательные чтения не всегда будут читать одно и то же. Там было никогда не предполагалось, что другие переменные в коде будут синхронизированы. При общении между потоками обычно порядок чтения и записи ко всем общим переменным, которые это важно. Если я сделаю что-то вроде:

globalPointer = new Xxx;

и другие потоки могут получить доступ к globalPointer, это важно что все записи в конструкторе Xxx стали видимый перед изменением значения globalPointer имеет место. Для этого не только globalPointer volatile, но также и все члены Xxx, и любые переменные функции-члены Xxx могут использовать или любые данные доступный через указатель в Xxx. Это просто не разумно; вы очень скоро закончите все, что в программа volatile. И даже тогда это также потребует, чтобы компиляторы правильно реализуют volatile, выставляют забор или membar инструкции вокруг каждого доступа. (FWIW: забор или инструкция membar может умножать время, которое требуется для памяти доступ в 10 или более раз.)

Решение здесь не изменчиво, но для доступа к указатель (и только доступ к указателю) атомный, используя atomic_load и atomic_store, добавленные в С++ 11. Эти примитивы действительно вызывают необходимый забор или мембар инструкции для использования; они также сообщают компилятору не двигаться любой доступ к памяти через них. Чтобы использовать atomic_load to установить указатель выше, вызовет все предыдущие записи в памяти быть видимым для других потоков перед записью указателю становится видимым (при условии, что поток чтения использует atomic_read, конечно, atomic_write гарантирует, что все предыдущие записи доступны в "общей" памяти всех потоки и atomic_read гарантирует, что все последующие чтения пойдет в "общую" память, а не заберет некоторую ценность уже в стадии разработки).


Вот существующий вопрос SO о неустойчивости: Что такое ключевое слово volatile?

Только с первым фрагментом кода вы правы, нет очевидной причины, чтобы x объявлял volatile. Однако, если вы понимаете, какое ключевое слово предполагается использовать для вас, можно предположить, что x является изменчивым, потому что есть что-то вне этого кода, что может изменить его значение. Например, память может быть прикреплена к другому оборудованию или написана другой программой. Поэтому программист инструктирует компилятор, что он не может видеть все возможные способы изменения значения x. И поэтому компилятор может некорректно оптимизировать код.

Второй фрагмент кода сам по себе не требует ключевого слова volatile. Volatile используется в качестве подсказки компилятора, в котором говорится, что память может меняться силами за пределами текущей программы. Он не должен использоваться для связи потоков. Существуют новые типы С++, которые должны использоваться для этих ситуаций.

Я рекомендую читать эту статью Herb Sutter, а также этот разговор.


Компилятор может иметь некоторую информацию о функции DoSomeOtherStuff(). Например:

static int DoSomeOtherStuff()
{
 return 42;
}
volatile int x;
int DoSomething() {
 x = 1;
 DoSomeOtherStuff();
 return x+1; // Don't just return 2 because we stored a 1 in x.
 // Check to get its current value
}

GCC с опцией -O3 полностью удаляет вызов функции DoSomeOtherStuff() (и его тела), но все равно перезагружает x, чтобы вернуть x+1 в конце.


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

Ключевое слово volatile используется главным образом там, где мы непосредственно имеем дело с GPIO, прерыванием или регистром флага. Он также используется там, где глобальная переменная или буфер разделяются между потоками.

Правильное место для использования ключевого слова volatile

Переменная должна быть объявлена ​​изменчивой, когда ее значение может быть неожиданно изменено. На практике вы должны объявлять переменную как изменчивую, когда будете:

Accessing the memory-mapped peripherals register.
 Sharing the global variables or buffers between the multiple threads.
 Accessing the global variables in an interrupt routine or signal handler.

Эта ссылка также полезна: http://aticleworld.com/understanding-volatile-qualifier-in-c/


Единственный способ действительно быть уверенным здесь в том, чтобы изучить код сборки, сгенерированный для вашей целевой платформы. Если volatile выполняет то, что вы намереваетесь, любое чтение переменной x будет выполняться с помощью загрузки из памяти.

licensed under cc by-sa 3.0 with attribution.