Каким правилам должен следовать компилятор при работе с энергозависимыми ячейками памяти?

Я знаю, что при чтении из области памяти, в которую записаны несколько потоков или процессов, ключевое слово 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()).

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

Это его главная цель.

Его также можно использовать для того, чтобы один поток видел изменение значения, записанного другим, но это никоим образом не гарантирует атомарность при чтении / записи в указанный объект.

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