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

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

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

person Pooria    schedule 09.11.2010    source источник
comment
Обратите внимание, что в переносимом C ++ volatile не может использоваться в качестве синхронизации потоков для бедняков (хотя компиляторы могут таким образом расширить его значение). Запись в объект volatile в одном потоке не обязательно означает, что другой поток увидит обновленное значение. (Он мог быть записан только в кэш одного ЦП, но не через кеш в ту память, которую разделяют ЦП.) Для этого вам нужны барьеры памяти.   -  person sbi    schedule 09.11.2010
comment
@sbi: Хотя ваш комментарий выделен жирным шрифтом, я не думаю, что у соответствующего компилятора есть способ оставить значение в кеше ЦП, а не сбрасывать его в память. В конце концов, это фактическое значение volatile: записи должны быть в основной памяти. Причина, по которой его нельзя использовать для синхронизации, заключается в том, что гарантии не гарантируют атомарность или переупорядочение с использованием энергонезависимых переменных.   -  person David Rodríguez - dribeas    schedule 09.11.2010
comment
@dribeas: абстрактная машина C ++ 98 не имеет понятия кеш-памяти ЦП (или регистров, если на то пошло), поэтому нет, нет необходимости сбрасывать volatile записи в основную память.   -  person zwol    schedule 09.11.2010
comment
@David: пишет, что нужно сделать это в основной памяти. Я здесь не в себе, но, насколько я знаю, volatile часто используется для адресов, которые даже не соответствуют памяти, поэтому я думаю, что это должно быть неправильно. Но да, я забыл о проблемах атомарности и порядка записи.   -  person sbi    schedule 09.11.2010
comment
@sbi, @Zack: верно, я набрал быстрее, чем мог подумать: абстрактная машина не имеет концепции кеширования процессора. Я смешал две концепции, модель памяти C ++ определяет, что она должна быть записана в память. Аппаратная архитектура - это то, что обеспечивает единообразие представления памяти для разных процессоров, даже если это накладывает определенное бремя на производительность. Язык определяет, что он записан в память. Аппаратная архитектура гарантирует, что память, которую видят разные процессоры, согласована.   -  person David Rodríguez - dribeas    schedule 10.11.2010
comment
@dribeas: на самом деле, более чем несколько архитектур практически не дают гарантии согласованности памяти между процессорами. См. kernel.org/doc/Documentation/memory-barriers.txt. для исчерпывающего обзора того, с чем, возможно, придется столкнуться системным программистам.   -  person zwol    schedule 10.11.2010


Ответы (5)


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

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

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

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

person dmckee --- ex-moderator kitten    schedule 09.11.2010

То, что вы знаете, ложно. 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.

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

person John Dibling    schedule 09.11.2010
comment
@John Dibling_Как я сказал, что volatile используется для синхронизации доступа к памяти между потоками? - person Pooria; 09.11.2010
comment
@Pooria: В самом первом предложении. - person sbi; 09.11.2010
comment
@Pooria: Здесь: при чтении из области памяти, которая записывается несколькими потоками или процессами, для этого места следует использовать ключевое слово volatile. Это утверждение абсолютно неверно. volatile здесь ничего не делает для вас, кроме как дает вам ложное чувство безопасности. - person John Dibling; 09.11.2010
comment
Я бы добавил, что некоторые компиляторы действительно реализуют семантику получения и выпуска для volatile, но она нестандартна и зависит от реализации. - person Aaron Klotz; 09.11.2010
comment
@John Dibling_: Я не понимаю, что я использую, и я думаю, что все остальные будут использовать для синхронизации обращений к памяти потоками, это своего рода объект синхронизации, возможно, у вас неправильное представление о моих утверждениях. - person Pooria; 09.11.2010
comment
@Pooria: Тогда почему, как ты думаешь, тебе нужно использовать здесь volatile? - person John Dibling; 09.11.2010
comment
@John Dibling_: Я считаю, что ключевое слово volatile имеет собственное применение, помимо объектов синхронизации, как если бы вы взглянули на некоторые вещи, которые, по словам других людей, вы поймете, например, исключение кеширования значения из памяти в регистр и использование регистра для повторного доступа, как упомянул dmckee. - person Pooria; 09.11.2010
comment
@Pooria: Это так, и я перечислил его использование. Но ни одно из этих применений не применимо к контексту, который вы нам дали. Ваше первое предложение дает нам наш контекст, и его можно перефразировать так: При многопоточном программировании переменные должны быть помечены volatile. Я спрашиваю вас, почему вы думаете, что вам нужно volatile в вашем конкретном случае. - person John Dibling; 09.11.2010
comment
@John Dibling_Просто прочтите, что говорят другие, и вы узнаете. - person Pooria; 09.11.2010
comment
@Pooria: Я могу только надеяться, что ваша защитная позиция и отказ отвечать на мои вопросы являются признаком того, что вы обижены, потому что узнали, что были неправы. Как бы то ни было, я всегда читаю все, что все говорят. Вот как я учусь. - person John Dibling; 09.11.2010
comment
@John Dibling_: Меня совершенно сбивает с толку, почему вы настаиваете на таком заявлении о полном выбросе ключевого слова volatile в корзину в многопоточном программировании, что противоречит тому, что говорили некоторые другие, а также посмотрите некоторые комментарии на этой странице, на которую вы поместили ссылку (< программное обеспечение href = "http://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/" rel = "nofollow noreferrer">. intel.com/en-us/blogs/2007/11/30/), что некоторые вещи в этой статье не соответствуют действительности. - person Pooria; 10.11.2010
comment
@John Dibling_check это (stackoverflow.com/questions/72552/). - person Pooria; 10.11.2010
comment
@Pooria: В вашем связанном вопросе принятый ответ касается конкретно оборудования с отображением памяти. Если вы читали мой пост, вы заметили, что а) я конкретно говорю, что volatile предназначен, и б) даже это не имеет ничего общего с многопоточным программированием. Это связано с доступом к оборудованию. - person John Dibling; 12.11.2010

Это не так хорошо определено, как вы, вероятно, хотели бы. Большинство соответствующих стандартов C ++ 98 находится в разделе 1.9, «Выполнение программы»:

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

Доступ к объекту, обозначенному volatile lvalue (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();
    

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

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

person zwol    schedule 09.11.2010
comment
Обратите внимание, что некоторые известные эксперты (в частности, Херб Саттер) упоминают, что умный компилятор может даже рассматривать переменную volatile как энергонезависимую, если он может продемонстрировать, что она не может быть прочитана извне: подумайте о переменной, объявленной volatile, но не привязанной к конкретному адресу, для которого в другой код не передаются указатели / ссылки - пример: автоматическая летучая переменная, соответствующий компилятор может рассматривать ее как энергонезависимую. - person David Rodríguez - dribeas; 09.11.2010
comment
Я вижу логику, но, если вы спросите меня, это слишком сильно опирается на правило «как если бы». - person zwol; 09.11.2010
comment
@ DavidRodríguez-dribeas: Интересная идея, хотя я могу представить ее актуальной только в том случае, если цикл считывает или записывает изменчивую переменную несколько раз; если такие операции чтения или записи должны выполняться последовательно, никакой код после цикла не может иметь наблюдаемых побочных эффектов, пока процессор не выполнит соответствующее количество операций чтения или записи. Этого можно было бы добиться лучше, если бы в стандарте был указан макрос __SIDE_EFFECT(), который заставлял бы компилятор делать все необходимое для обеспечения последовательного выполнения кода, окружающего его. - person supercat; 18.06.2013

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

person casablanca    schedule 09.11.2010

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

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

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

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

person Macke    schedule 09.11.2010