Почему существует volatile?
Вопрос
Что делает 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
доступ к памяти.
- вы должны использовать его для реализации спин-блокировок, а также некоторых (всех?) структур данных без блокировок
- используйте его с атомарными операциями / инструкциями
- однажды помог мне преодолеть ошибку компилятора (неправильно сгенерированный код при оптимизации)
Тот Самый 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 программист, которому необходимо реализовать базовый "мьютекс передачи данных" [тот, при котором задача, которая приобрела и выпустила мьютекс, не будет делать этого снова, пока это не сделает другая задача], должен выполнить одну из четырех вещей:
Поместите получение и освобождение мьютекса в функцию, которую компилятор не может встроить и к которой он не может применить оптимизацию всей программы.
Квалифицируйте все объекты, охраняемые мьютексом, как
volatile
--что-то, что не должно быть необходимым, если все обращения происходят после получения мьютекса и до его освобождения.Используйте уровень оптимизации 0, чтобы заставить компилятор генерировать код так, как будто все объекты, которые не являются квалифицированными
register
являютсяvolatile
.Используйте директивы, специфичные для gcc.
Напротив, при использовании компилятора более высокого качества, который больше подходит для системного программирования, такого как icc, у вас будет другой вариант:
- Убедитесь, что a
volatile
-квалифицированная запись выполняется везде, где требуется получение или выпуск.
Получение базового "мьютекса передачи данных" требует volatile
прочитайте (чтобы увидеть, готово ли оно), и не должно требовать volatile
напишите также (другая сторона не будет пытаться повторно получить его, пока оно не будет возвращено обратно), но при этом ей придется выполнять бессмысленную volatile
write по-прежнему лучше, чем любая из опций, доступных в gcc или clang.
Одно из применений, о котором я должен вам напомнить, заключается в том, что в функции обработчика сигналов, если вы хотите получить доступ / изменить глобальную переменную (например, пометьте ее как exit = true), вы должны объявить эту переменную как 'volatile'.