Вопрос

Что делает volatile ключевое слово do?В C ++ какую проблему это решает?

В моем случае я никогда сознательно в этом не нуждался.

Это было полезно?

Решение

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

Раньше я работал с двухпортовой оперативной памятью в многопроцессорной системе на языке straight C.Мы использовали аппаратно управляемое 16-битное значение в качестве семафора, чтобы знать, когда другой парень закончил.По сути, мы сделали это:

void waitForSemaphore()
{
   volatile uint16_t* semPtr = WELL_KNOWN_SEM_ADDR;/*well known address to my semaphore*/
   while ((*semPtr) != IS_OK_FOR_ME_TO_PROCEED);
}

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

Другие советы

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

Некоторые процессоры имеют регистры с плавающей запятой, точность которых превышает 64 бита (например.32-разрядный x86 без SSE, см. Комментарий Питера).Таким образом, если вы выполните несколько операций с числами двойной точности, вы фактически получите ответ с более высокой точностью, чем если бы вам пришлось усекать каждый промежуточный результат до 64 бит.

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

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

Из "Изменчивость как обещание" статья Дэна Сакса:

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

Вот ссылки на три его статьи, касающиеся volatile ключевое слово:

Вы ДОЛЖНЫ использовать volatile при реализации структур данных без блокировки.В противном случае компилятор волен оптимизировать доступ к переменной, что изменит семантику.

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

Например, вот как InterlockedIncrement объявляется в Win32 API:

LONG __cdecl InterlockedIncrement(
  __inout  LONG volatile *Addend
);

Большое приложение, над которым я работал в начале 1990-х, содержало обработку исключений на основе C с использованием setjmp и longjmp.Ключевое слово volatile было необходимо для переменных, значения которых необходимо было сохранить в блоке кода, который служил предложением "catch", чтобы эти переменные не были сохранены в регистрах и не были уничтожены longjmp.

В стандарте C одно из мест для использования volatile находится с обработчиком сигнала.Фактически, в стандартном C все, что вы можете безопасно сделать в обработчике сигналов, - это изменить volatile sig_atomic_t переменная или быстрый выход.Действительно, AFAIK, это единственное место в стандарте C, где использование volatile требуется, чтобы избежать неопределенного поведения.

ISO/IEC 9899: 2011 §7.14.1.1 signal функция

¶5 Если сигнал возникает не в результате вызова abort или raise функция, поведение не определено, если обработчик сигнала ссылается на любой объект со статическим или потоковым значением продолжительность хранения, которая не является атомарным объектом без блокировки, за исключением присвоения значения объекту, объявленному как volatile sig_atomic_t, или обработчик сигнала вызывает любую функцию в стандартной библиотеке, отличную от abort функция, обеспечивающая _Exit функция, обеспечивающая quick_exit функция, или signal функция с первым аргументом, равным номеру сигнала, соответствующему сигналу, который вызвал вызов обработчика.Кроме того, если такой призыв к signal функция возвращает значение SIG_ERR, значение errno является неопределенным.252)

252) Если какой-либо сигнал генерируется асинхронным обработчиком сигналов, поведение не определено.

Это означает, что в стандартном C вы можете написать:

static volatile sig_atomic_t sig_num = 0;

static void sig_handler(int signum)
{
    signal(signum, sig_handler);
    sig_num = signum;
}

и больше ничего особенного.

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

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

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

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

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

template <typename T> 
class Foo {
  std::enable_if_t<sizeof(T)==4, void> f(T& t) 
  { std::cout << 1 << t; }
  void f(T volatile& t) 
  { std::cout << 2 << const_cast<T&>(t); }

  void bar() { T t; f(t); }
};

Это законно;обе перегрузки потенциально могут быть вызваны и выполняют почти одно и то же действие.Актерский состав в volatile перегрузка является законной, поскольку мы знаем, что bar не пропустит энергонезависимый T в любом случае.Тот Самый volatile версия, однако, строго хуже, поэтому никогда не выбирается при разрешении перегрузки, если энергонезависимый f доступен.

Обратите внимание, что код на самом деле никогда не зависит от volatile доступ к памяти.

  1. вы должны использовать его для реализации спин-блокировок, а также некоторых (всех?) структур данных без блокировок
  2. используйте его с атомарными операциями / инструкциями
  3. однажды помог мне преодолеть ошибку компилятора (неправильно сгенерированный код при оптимизации)

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

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

Рассмотрим следующие случаи

1) Глобальные переменные, измененные процедурой обслуживания прерываний вне области видимости.

2) Глобальные переменные в многопоточном приложении.

Если мы не используем определитель volatile, могут возникнуть следующие проблемы

1) Код может работать не так, как ожидалось, когда включена оптимизация.

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

Изменчивый:Лучший друг программиста

https://en.wikipedia.org/wiki/Volatile_ (компьютерное программирование)

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

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

Ваша программа, кажется, работает даже без volatile ключевое слово?Возможно, в этом и есть причина:

Как упоминалось ранее, volatile ключевое слово помогает в таких случаях, как

volatile int* p = ...;  // point to some memory
while( *p!=0 ) {}  // loop until the memory becomes zero

Но, похоже, при вызове внешней или нестроевой функции эффекта почти нет.Например.:

while( *p!=0 ) { g(); }

Тогда с кем или без кого volatile получается почти тот же результат.

Пока g() может быть полностью встроен, компилятор может видеть все, что происходит, и, следовательно, может оптимизировать.Но когда программа выполняет вызов в место, где компилятор не может видеть, что происходит, для компилятора больше небезопасно делать какие-либо предположения.Следовательно, компилятор будет генерировать код, который всегда считывается непосредственно из памяти.

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

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

На заре C компиляторы интерпретировали бы все действия, связанные с чтением и записью значений lvalues, как операции с памятью, которые должны выполняться в той же последовательности, в какой операции чтения и записи появляются в коде.Эффективность могла бы быть значительно повышена во многих случаях, если бы компиляторам была предоставлена определенная свобода для изменения порядка и консолидации операций, но с этим была проблема.Даже операции часто указывались в определенном порядке просто потому, что было необходимо указать их в некоторые заказ, и, таким образом, программист выбирал одну из многих одинаково хороших альтернатив, что не всегда было так.Иногда было бы важно, чтобы определенные операции выполнялись в определенной последовательности.

Какие именно детали последовательности важны, будет зависеть от целевой платформы и области применения.Вместо того чтобы обеспечивать особенно детальный контроль, Стандарт выбрал простую модель:если последовательность обращений выполняется с использованием значений lvalues, которые не являются квалифицированными volatile, компилятор может изменить порядок и консолидировать их по своему усмотрению.Если действие выполняется с помощью volatile-квалифицированное значение, качественная реализация должна предлагать любые дополнительные гарантии упорядочения, которые могут потребоваться для кода, ориентированного на предполагаемую платформу и область применения, без необходимости требовать использования нестандартного синтаксиса.

К сожалению, вместо того, чтобы определить, какие гарантии понадобятся программистам, многие компиляторы предпочли вместо этого предложить минимальные гарантии, предусмотренные Стандартом.Это делает volatile гораздо менее полезно, чем это должно быть.Например, в gcc или clang программист, которому необходимо реализовать базовый "мьютекс передачи данных" [тот, при котором задача, которая приобрела и выпустила мьютекс, не будет делать этого снова, пока это не сделает другая задача], должен выполнить одну из четырех вещей:

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

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

  3. Используйте уровень оптимизации 0, чтобы заставить компилятор генерировать код так, как будто все объекты, которые не являются квалифицированными register являются volatile.

  4. Используйте директивы, специфичные для gcc.

Напротив, при использовании компилятора более высокого качества, который больше подходит для системного программирования, такого как icc, у вас будет другой вариант:

  1. Убедитесь, что a volatile-квалифицированная запись выполняется везде, где требуется получение или выпуск.

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

Одно из применений, о котором я должен вам напомнить, заключается в том, что в функции обработчика сигналов, если вы хотите получить доступ / изменить глобальную переменную (например, пометьте ее как exit = true), вы должны объявить эту переменную как 'volatile'.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top