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

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

volatile SomeType * ptr = someAddress;
void someFunc(volatile const SomeType & input){
 //function body
}

Ответ 1

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

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

Также не может последовательность записей использовать регистр и только записывать окончательное значение позже: каждая запись должна быть вытолкнута в память.

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

Ответ 2

То, что вы знаете, неверно. Volatile не используется для синхронизации доступа к памяти между потоками, применения каких-либо заграждений памяти или чего-либо подобного. Операции с памятью volatile не являются атомарными, и они не гарантируются в каком-либо конкретном порядке. volatile является одним из самых непонятных объектов на всем языке. " Летучие почти бесполезны для многопоточного программирования."

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

Его также можно использовать аналогично тому, как используется const, и именно так Alexandrescu использует его в в этой статье. Но не ошибитесь. volatile не делает ваш код волшебным потоком безопасным. Используемый таким образом, это просто инструмент, который может помочь компилятору рассказать вам, где вы, возможно, перепутались. Вам все равно исправить свои ошибки, а volatile не играет никакой роли в устранении этих ошибок.

EDIT: Я попытаюсь немного рассказать о том, что я только что сказал.

Предположим, что у вас есть класс, который имеет указатель на то, что не может измениться. Вы, естественно, можете сделать указатель const:

class MyGizmo
{ 
public:
  const Foo* foo_;
};

Что действительно делает const для вас здесь? Он ничего не делает для памяти. Это не похоже на вкладку защиты от записи на старом дискете. Сама память все еще доступна для записи. Вы просто не можете писать в нее с помощью указателя foo_. Таким образом, const - это просто способ дать компилятору еще один способ сообщить вам, когда вы можете запутаться. Если вы должны были написать этот код:

gizmo.foo_->bar_ = 42;

... компилятор не допустит этого, потому что он помечен const. Очевидно, вы можете обойти это, используя const_cast, чтобы отбросить const -ness, но если вам нужно убедиться, что это плохая идея, тогда вам не поможет.:)

Использование Alexandrescu volatile точно такое же. Он ничего не делает, чтобы сделать память каким-то образом "потокобезопасной". Что он делает, так это дает компилятору другой способ сообщить вам, когда вы, возможно, прищурились. Вы отмечаете то, что вы сделали действительно "потокобезопасным" (с использованием реальных объектов синхронизации, таких как Мьютекс или Семафоры) как volatile. Тогда компилятор не позволит вам использовать их в контексте volatile. Он выдает ошибку компилятора, которую вам нужно подумать и исправить. Вы можете снова обойти его, отбросив volatile -ness с помощью const_cast, но это так же, как зло, как отбрасывание const -ness.

Мой совет - полностью отказаться от volatile как инструмент для написания многопоточных приложений (edit:), пока вы действительно не знаете, что вы делаете и почему. Он имеет некоторую выгоду, но не так, как думают большинство людей, и если вы используете его неправильно, вы можете написать опасно опасные приложения.

Ответ 3

Это не так точно определено, как вы, вероятно, хотите, чтобы это было. Большинство соответствующих стандартов из С++ 98 приведены в разделе 1.9 "Исполнение программы":

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

Доступ к объекту, обозначенному значением volatile lvalue (3.10), модификацией объекта, вызовом функции ввода-вывода библиотеки или вызовом функции, которая выполняет любую из этих операций, являются всеми побочными эффектами, которые являются изменениями состояния среды исполнения. Оценка выражения может привести к побочным эффектам. В определенных определенных точках последовательности выполнения, называемых точками последовательности, все побочные эффекты предыдущих оценок должны быть полными, и никаких побочных эффектов последующих оценок не должно быть.

Как только выполнение функции начинается, никакие выражения от вызывающей функции не вычисляются до тех пор, пока выполнение вызываемой функции не завершится.

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

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

Наименьшие требования к соответствующей реализации:

  • В точках последовательности объекты volatile стабильны в том смысле, что предыдущие оценки завершены, а последующие оценки еще не произошли.

  • При завершении программы все данные, записанные в файлы, должны быть идентичны одному из возможных результатов выполнения программы в соответствии с абстрактной семантикой.

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

Так что это сводится к:

  • Компилятор не может оптимизировать чтение или запись в объекты volatile. Для простых случаев, таких как упомянутый один casablanca, это работает так, как вы думаете. Однако в таких случаях, как

    volatile int a;
    int b;
    b = a = 42;
    

    люди могут и спорить о том, должен ли компилятор генерировать код, как если бы последняя строка прочитала

    a = 42; b = a;
    

    или если он может, как обычно, (в отсутствие volatile), сгенерирует

    a = 42; b = 42;
    

    (С++ 0x, возможно, обратился к этому вопросу, я не прочитал все это.)

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

  • Говоря о потоках, вы заметите полное отсутствие любого упоминания потоков в тексте стандартов. Это связано с тем, что С++ 98 не имеет понятия потоков. (С++ 0x делает, и вполне может указать их взаимодействие с volatile, но я бы не предполагал, что кто-либо выполнит эти правила, если бы я был вами.) Поэтому нет гарантии, что доступ к объектам volatile из один поток виден в другом потоке. Это еще одна важная причина volatile не особенно полезна для многопоточного программирования.

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

  • Многие люди пытаются сделать определенные обращения к объектам семантикой volatile, например. делать

    T x;
    *(volatile T *)&x = foo();
    

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

  • Если вы беспокоитесь о переупорядочении доступа к более чем одному изменчивому значению, вам нужно понять правила последовательности, которые являются длинными и сложными, и я не буду приводить их здесь, потому что этот ответ уже слишком долго, но здесь хорошее объяснение, которое немного упрощено. Если вам нужно беспокоиться о различиях в правилах последовательности точек между C и С++, вы уже что-то прикрутили (например, как правило, никогда не перегружайте &&).

Ответ 4

Объявление переменной как 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, поэтому компилятор должен повторно извлечь текущее значение из память.

Ответ 5

Компилятору не разрешается оптимизировать прочтение изменчивого объекта в цикле, который в противном случае он обычно выполнял бы (т.е. strlen()).

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

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

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