Каким правилам должен следовать компилятор при работе с энергозависимыми ячейками памяти?
Я знаю, что при чтении из области памяти, в которую записаны несколько потоков или процессов, ключевое слово volatile должно использоваться для этого расположения, как в некоторых случаях ниже, но я хочу узнать больше о том, какие ограничения он на самом деле накладывает на компилятор и какие правила должен ли компилятор следовать при работе с таким случаем и есть ли исключительный случай, когда, несмотря на одновременный доступ к ячейке памяти, программист может игнорировать ключевое слово volatile.
volatile SomeType * ptr = someAddress;
void someFunc(volatile const SomeType & input){
//function body
}
5 ответов
Особая и очень распространенная оптимизация, которая исключается volatile
заключается в кэшировании значения из памяти в регистр и использовании регистра для повторного доступа (потому что это намного быстрее, чем каждый раз возвращаться в память).
Вместо этого компилятор должен извлекать значение из памяти каждый раз (принимая подсказку от Зака, я должен сказать, что "каждый раз" ограничен точками последовательности).
Также последовательность записей не может использовать регистр и записывать только окончательное значение позже: каждая запись должна быть выгружена в память.
Почему это полезно? В некоторых архитектурах определенные устройства ввода-вывода отображают свои входы или выходы в ячейку памяти (т. Е. Байт, записанный в эту ячейку, фактически выходит из последовательной линии). Если компилятор перенаправляет некоторые из этих записей в регистр, который очищается только изредка, тогда большая часть байтов не попадет в последовательную строку. Нехорошо. С помощью volatile
предотвращает эту ситуацию.
То, что вы знаете, является ложным. Volatile не используется для синхронизации доступа к памяти между потоками, применения каких-либо ограничений памяти или чего-либо подобного. Операции на volatile
память не является атомарной, и они не гарантированно находятся в каком-либо определенном порядке. volatile
является одним из самых неправильно понятых средств на всем языке. " Volatile почти бесполезно для многопоточного программирования ".
Какие volatile
используется для взаимодействия с отображенным в память оборудованием, обработчиками сигналов и setjmp
инструкция машинного кода.
Он также может быть использован аналогичным образом, что const
используется, и именно так Александреску использует это в этой статье. Но не заблуждайтесь. volatile
не делает ваш код волшебным потокобезопасным. Используемый таким особым образом, это просто инструмент, который может помочь компилятору сообщить вам, где вы могли ошибиться. Это все еще зависит от вас, чтобы исправить ваши ошибки, и volatile
не играет никакой роли в исправлении этих ошибок.
РЕДАКТИРОВАТЬ: Я постараюсь немного пояснить то, что я только что сказал.
Предположим, у вас есть класс с указателем на что-то, что не может измениться. Вы могли бы естественно сделать указатель const:
class MyGizmo
{
public:
const Foo* foo_;
};
Что значит const
действительно для тебя здесь? Это ничего не делает с памятью. Это не похоже на вкладку защиты от записи на старой дискете. Сама память по-прежнему доступна для записи. Вы просто не можете написать это через foo_
указатель. Так const
на самом деле это просто способ дать компилятору еще один способ сообщить вам, когда у вас могут возникнуть проблемы. Если вы должны были написать этот код:
gizmo.foo_->bar_ = 42;
... компилятор не допустит этого, потому что он помечен const
, Очевидно, что вы можете обойти это с помощью const_cast
выбросить const
Конечно, но если вам нужно убедиться, что это плохая идея, тогда вам не помогут.:)
Александреску использовать volatile
точно так же. Он ничего не делает для того, чтобы память каким-то образом была "поточно-ориентированной". Он дает компилятору еще один способ сообщить вам, когда вы, возможно, облажались. Вы помечаете вещи, которые вы сделали действительно "потокобезопасными" (благодаря использованию реальных объектов синхронизации, таких как мьютексы или семафоры), как volatile
, Тогда компилятор не позволит вам использовать их в volatile
контекст. Выдает ошибку компилятора, о которой вам нужно подумать и исправить. Вы могли бы снова обойти это, отбросив volatile
использование const_cast
, но это так же зло, как отбрасывание const
-ness.
Мой тебе совет полностью отказаться volatile
в качестве инструмента для написания многопоточных приложений (редактирование:), пока вы действительно не знаете, что делаете и почему. Это имеет некоторое преимущество, но не в том, как думает большинство людей, и если вы используете его неправильно, вы можете написать опасно небезопасные приложения.
Это не так хорошо определено, как вы, вероятно, хотите, чтобы это было. Большая часть соответствующих стандартов из C++98 находится в разделе 1.9 "Выполнение программы":
Наблюдаемое поведение абстрактной машины - это последовательность операций чтения и записи в
volatile
данные и вызовы функций ввода / вывода библиотеки.Доступ к объекту, обозначенному
volatile
Значение (3.10), изменение объекта, вызов библиотечной функции ввода-вывода или вызов функции, выполняющей любую из этих операций, являются побочными эффектами, которые являются изменениями в состоянии среды выполнения. Оценка выражения может привести к побочным эффектам. В определенных точках в последовательности выполнения, называемых точками последовательности, все побочные эффекты предыдущих оценок должны быть завершены, и никаких побочных эффектов последующих оценок не должно быть.Как только начинается выполнение функции, никакие выражения из вызывающей функции не вычисляются до тех пор, пока не будет завершено выполнение вызываемой функции.
Когда обработка абстрактной машины прерывается при получении сигнала, значения объектов с типом, отличным от
volatile sig_atomic_t
не определены, а стоимость любого объекта неvolatile sig_atomic_t
это изменяется обработчиком, становится неопределенным.Экземпляр каждого объекта с автоматической продолжительностью хранения (3.7.2) связан с каждой записью в его блоке. Такой объект существует и сохраняет свое последнее сохраненное значение во время выполнения блока и пока блок приостановлен (посредством вызова функции или получения сигнала).
Минимальные требования к соответствующей реализации:
В точках последовательности,
volatile
объекты стабильны в том смысле, что предыдущие оценки завершены, а последующие оценки еще не выполнены.При завершении программы все данные, записанные в файлы, должны быть идентичны одному из возможных результатов, которые могло бы дать выполнение программы в соответствии с абстрактной семантикой.
Динамика ввода и вывода интерактивных устройств должна происходить таким образом, чтобы подсказывающие сообщения фактически появлялись до того, как программа ожидает ввода. То, что составляет интерактивное устройство, определяется реализацией.
Так что это сводится к следующему:
Компилятор не может оптимизировать чтение или запись в
volatile
объекты. Для простых случаев, таких как упомянутый выше касабланка, это работает так, как вы думаете. Однако в таких случаях, какvolatile int a; int b; b = a = 42;
люди могут и действительно спорят о том, должен ли компилятор генерировать код, как если бы последняя строка прочитала
a = 42; b = a;
или, если это возможно, как обычно (в отсутствие
volatile
), генерироватьa = 42; b = 42;
(C++0x, возможно, обратился к этому вопросу, я не прочитал все это.)
Компилятор не может переупорядочивать операции на двух разных
volatile
объекты, которые встречаются в отдельных операторах (каждая точка с запятой - это точка последовательности), но полностью разрешено изменять доступ к энергонезависимым объектам относительно энергозависимых. Это одна из многих причин, почему вы не должны пытаться написать свои собственные спин-блокировки, и это основная причина, по которой Джон Диблинг предупреждает вас не обращатьсяvolatile
как панацея для многопоточного программирования.Говоря о потоках, вы заметите полное отсутствие какого-либо упоминания потоков в тексте стандартов. Это потому, что C++98 не имеет понятия о потоках. (C++0x делает, и вполне может указать их взаимодействие с
volatile
, но я бы не стал предполагать, что кто-то еще реализует эти правила, если бы я был вами.) Поэтому нет никакой гарантии, что доступ кvolatile
объекты из одного потока видны другому потоку. Это другая основная причинаvolatile
не особенно полезен для многопоточного программирования.Там нет никакой гарантии, что
volatile
доступ к объектам осуществляется одним куском или изменениямиvolatile
предметы не касаются других вещей прямо в памяти. Это не ясно в том, что я цитировал, но подразумевается материал оvolatile sig_atomic_t
-sig_atomic_t
часть была бы ненужной в противном случае. Это делаетvolatile
существенно менее полезен для доступа к устройствам ввода-вывода, чем предполагалось, и компиляторы, продаваемые для встроенного программирования, часто предлагают более строгие гарантии, но вы не можете рассчитывать на это.Многие люди пытаются сделать определенный доступ к объектам
volatile
семантика, например, делатьT x; *(volatile T *)&x = foo();
Это допустимо (потому что оно говорит "объект, обозначенный volatile lvalue", а не "объект с volatile типом"), но должно быть сделано с большой осторожностью, потому что помните, что я сказал о том, что компилятору полностью разрешено изменять порядок энергонезависимых доступы относительно изменчивых? Это происходит, даже если это один и тот же объект (насколько я знаю в любом случае).
Если вы беспокоитесь о переупорядочении доступа к более чем одному изменчивому значению, вам нужно понять правила точки последовательности, которые являются длинными и сложными, и я не собираюсь их здесь цитировать, потому что этот ответ уже слишком длинный, но вот хорошее объяснение, которое лишь немного упрощено. Если вам нужно беспокоиться о различиях в правилах точек последовательности между C и C++, вы уже где-то облажались (например, как правило, никогда не перегружайте
&&
).
Объявление переменной как volatile
означает, что компилятор не может делать какие-либо предположения о значении, которое он мог бы сделать иначе, и, следовательно, не позволяет компилятору применять различные оптимизации. По сути, это заставляет компилятор перечитывать значение из памяти при каждом доступе, даже если обычный поток кода не меняет значение. Например:
int *i = ...;
cout << *i; // line A
// ... (some code that doesn't use i)
cout << *i; // line B
В этом случае компилятор обычно предполагает, что, так как значение в i
не был изменен в промежутке, можно сохранить значение из строки A (скажем, в регистре) и вывести то же значение в B. Однако, если вы отметите i
как volatile
вы говорите компилятору, что какой-то внешний источник мог изменить значение в i
между строкой A и B, поэтому компилятор должен повторно извлечь текущее значение из памяти.
Компилятору не разрешается оптимизировать чтение циклических объектов в цикле, что в противном случае он обычно делал бы (например, strlen()).
Он обычно используется во встроенном программировании при чтении реестра оборудования по фиксированному адресу, и это значение может неожиданно измениться. (В отличие от "нормальной" памяти, она не меняется, если она не записана самой программой...)
Это его главная цель.
Его также можно использовать для того, чтобы один поток видел изменение значения, записанного другим, но это никоим образом не гарантирует атомарность при чтении / записи в указанный объект.