Почему volatile
требуется в C? Для чего его используют? Что он будет делать?
Volatile сообщает компилятору не оптимизировать что-либо, что связано с изменчивой переменной.
Существует только одна причина его использования: при взаимодействии с оборудованием.
Скажем, у вас есть небольшое количество аппаратного обеспечения, которое где-то отображается в ОЗУ и имеет два адреса: командный порт и порт данных:
typedef struct
{
int command;
int data;
int isbusy;
} MyHardwareGadget;
Теперь вы хотите отправить некоторую команду:
void SendCommand (MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
Выглядит легко, но он может выйти из строя, потому что компилятор может свободно изменять порядок, в котором записываются данные и команды. Это приведет к тому, что наш маленький гаджет выдаст команды с предыдущим значением данных. Также взгляните на ожидание во время цикла занятости. Это будет оптимизировано. Компилятор попытается быть умным, сразу прочитайте значение isbusy, а затем перейдите в бесконечный цикл. Это не то, что вы хотите.
Способ обойти это - объявить гаджет указателя как изменчивый. Таким образом, компилятор вынужден делать то, что вы написали. Он не может удалить назначения памяти, он не может кэшировать переменные в регистрах и не может изменить порядок присвоений:
Это правильная версия:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
Другим использованием для volatile
являются обработчики сигналов. Если у вас есть такой код:
quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
Компилятор может заметить, что тело цикла не касается переменной quit
и преобразует цикл в цикл while (true)
. Даже если переменная quit
установлена на обработчик сигнала для SIGINT
и SIGTERM
; компилятор не знает этого.
Однако, если переменная quit
объявлена volatile
, компилятор вынужден загружать ее каждый раз, потому что ее можно изменить в другом месте. Это именно то, что вы хотите в этой ситуации.
quit
, компилятор может оптимизировать его в постоянный цикл, предполагая, что нет никакого способа, чтобы quit
был изменен между итерациями. NB: Это не обязательно хорошая замена для реального программирования с защитой потоков.
volatile
в C фактически появился с целью не кэшировать значения переменной автоматически. Он покажет машине, что она не кэширует значение этой переменной. Таким образом, каждый раз, когда он сталкивается с ней, он принимает значение данной переменной volatile
из основной памяти. Этот механизм используется, поскольку в любое время значение может быть изменено ОС или любым прерыванием. Поэтому использование volatile
поможет нам каждый раз получать доступ к значению.
volatile
сообщает компилятору, что ваша переменная может быть изменена другими способами, чем код, к которому он обращается. например, это может быть местоположение памяти с отображением ввода-вывода. Если это не указано в таких случаях, некоторые переменные обращения могут быть оптимизированы, например, его содержимое может храниться в регистре, а ячейка памяти не будет снова возвращена.
См. эту статью Андрея Александреску, " volatile - Лучший друг с несколькими программистами
Ключевое слово volatileразработан для предотвращения компиляции оптимизация, которая может отображать код неверно в присутствии определенных асинхронные события. Например, если вы объявляете примитивную переменную как volatile, компилятор не разрешено кэшировать его в реестре - общая оптимизация, которая была бы катастрофически, если эта переменная была разделяемый между несколькими потоками. Итак общее правило: если у вас есть переменные примитивного типа, который должен быть общим среди нескольких потоков, объявите эти переменные volatile. Но вы можете на самом деле делают гораздо больше с этим ключевое слово: вы можете использовать его, чтобы поймать код который не является потокобезопасным, и вы можете делать это во время компиляции. Эта статья показывает, как это делается; решение включает в себя простой умный указатель, который также упрощает сериализацию критические разделы кода.
Статья применяется как к C
, так и к C++
.
Также см. статью " С++ и опасения блокировки с двойным проверкой" Скотта Мейерса и Андрея Александреску:
Таким образом, при работе с некоторыми ячейками памяти (например, с отображенными в памяти портами или памятью, на которые ссылаются ISR [Процедуры обслуживания прерываний]), некоторые оптимизации должны быть приостановлены. (1) содержание изменчивой переменной "неустойчиво" (может быть изменено неизвестным компилятору способом), (2) все записи в изменчивые данные "наблюдаемы", поэтому они должны выполняться религиозно, и (3) все операции с изменчивыми данными выполняются в последовательности, в которой они отображаются в исходном коде. Первые два правила обеспечивают правильное чтение и письмо. Последний позволяет реализовать протоколы ввода/вывода, которые смешивают ввод и вывод. Это неофициально то, что нестабильные гарантии C и С++.
Мое простое объяснение:
В некоторых сценариях, основанных на логике или коде, компилятор будет оптимизировать переменные, которые, по его мнению, не меняются. Ключевое слово volatile
предотвращает оптимизацию переменной.
Например:
bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
// execute logic for the scenario where the USB isn't connected
}
Из приведенного выше кода компилятор может думать, что usb_interface_flag
определяется как 0, а в цикле while он будет равен нулю навсегда. После оптимизации компилятор будет обрабатывать его как while(true)
все время, что приведет к бесконечному циклу.
Чтобы избежать подобных сценариев, мы объявляем флаг изменчивым, мы сообщаем компилятору, что это значение может быть изменено внешним интерфейсом или другим модулем программы, т.е. не оптимизировать его. Это вариант использования для волатильности.
Маржинальное использование для неустойчивого заключается в следующем. Скажем, вы хотите вычислить численную производную функции f
:
double der_f(double x)
{
static const double h = 1e-3;
return (f(x + h) - f(x)) / h;
}
Проблема заключается в том, что x+h-x
обычно не равно h
из-за ошибок округления. Подумайте об этом: когда вы вычитаете очень близкие цифры, вы теряете много значительных цифр, которые могут испортить вычисление производной (подумайте 1.00001 - 1). Возможным обходным решением может быть
double der_f2(double x)
{
static const double h = 1e-3;
double hh = x + h - x;
return (f(x + hh) - f(x)) / hh;
}
но в зависимости от вашей платформы и коммутаторов компилятора вторая строка этой функции может быть уничтожена агрессивно оптимизирующим компилятором. Поэтому вместо этого вы пишете
volatile double hh = x + h;
hh -= x;
чтобы заставить компилятор прочитать ячейку памяти, содержащую hh, лишив возможную возможность оптимизации.
h
или hh
в производной формуле? Когда вычисляется hh
, последняя формула использует ее как первую, без разницы. Может быть, это должно быть (f(x+h) - f(x))/hh
?
h
и hh
состоит в том, что hh
усекается до некоторой отрицательной степени двух операцией x + h - x
. В этом случае x + hh
и x
отличаются точно на hh
. Вы также можете взять свою формулу, она даст тот же результат, так как x + h
и x + hh
равны (это знаменатель, который важен здесь).
Существует два варианта использования. Они особенно часто используются во встроенной разработке.
Компилятор не будет оптимизировать функции, которые используют переменные, которые определены с помощью volatile keyword
Volatile используется для доступа к точным ячейкам памяти в ОЗУ, ПЗУ и т.д. Это чаще используется для управления устройствами с отображением памяти, доступа к регистрам ЦП и определения местоположения конкретной ячейки памяти.
См. примеры с листингом сборки. Re: Использование C "volatile" ключевого слова в Embedded Development
Я расскажу о другом сценарии, в котором важны летучие.
Предположим, что вы скопировали карту памяти для более быстрого ввода-вывода, и этот файл может меняться за кулисами (например, файл не находится на вашем локальном жестком диске, а вместо этого используется через сеть другим компьютером).
Если вы получаете доступ к файлам данных с отображением памяти через указатели на энергонезависимые объекты (на уровне исходного кода), тогда код, сгенерированный компилятором, может извлекать одни и те же данные несколько раз, не зная об этом.
Если эти данные могут измениться, ваша программа может использовать две или более разные версии данных и перейти в непоследовательное состояние. Это может привести не только к логически неправильному поведению программы, но и к уязвимым местам безопасности, если она обрабатывает ненадежные файлы или файлы из ненадежных мест.
Если вы заботитесь о безопасности, и вы должны, это важный сценарий для рассмотрения.
Volatile также полезен, если вы хотите заставить компилятор не оптимизировать определенную последовательность кода (например, для записи микро-теста).
volatile означает, что хранилище, вероятно, изменится в любое время и будет изменено, но что-то вне контроля пользовательской программы. Это означает, что если вы ссылаетесь на переменную, программа всегда должна проверять физический адрес (т.е. Отображаемый ввод fifo) и не использовать его кэшированным способом.
В Wiki говорится обо всем volatile
:
И документ ядра Linux также отлично описывает нотацию volatile
:
Изменчивость может быть изменена извне скомпилированного кода (например, программа может отображать изменчивую переменную в регистр, привязанный к памяти). Компилятор не будет применять определенные оптимизации для кода, который обрабатывает изменчивую переменную - например, он не будет загружать его в регистр, не записывая его в память. Это важно при работе с аппаратными регистрами.
На мой взгляд, вы не должны ожидать слишком многого от volatile
. Чтобы проиллюстрировать это, посмотрите на Нилс Пипенбринк с высоким рейтингом.
Я бы сказал, что его пример не подходит для volatile
. volatile
используется только для:
запретить компилятору делать полезные и желательные оптимизации. Это ничего не касается безопасного потока, атомарного доступа или даже порядка памяти.
В этом примере:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
gadget->data = data
до gadget->command = command
только гарантируется только скомпилированным кодом компилятором. Во время работы процессор все еще может переупорядочить данные и назначение команд в отношении архитектуры процессора. Аппаратное обеспечение может получить неверные данные (предположим, что гаджет отображается на аппаратный ввод-вывод). Необходим барьер памяти между данными и назначением команд.
volatile
ухудшает производительность без причины. Что касается того, достаточно ли это, это будет зависеть от других аспектов системы, о которых программист может знать больше, чем компилятор. С другой стороны, если процессор гарантирует, что инструкция для записи по определенному адресу очистит кэш-память ЦП, но компилятор не предоставил никакого способа сбрасывать переменные с кэшем регистров, о которых ЦП ничего не знает, очистка кеша была бы бесполезной.
В языке, разработанном Деннисом Ритчи, каждый доступ к любому объекту, кроме автоматических объектов, адрес которых не был взят, будет вести себя так, как если бы он вычислил адрес объекта, а затем прочитал или написал хранилище по этому адресу. Это сделало язык очень мощным, но сильно ограниченным возможностями оптимизации.
Хотя возможно было бы добавить квалификатор, который пригласил бы компилятор предположить, что какой-то конкретный объект не будет изменен в странных путях, такое предположение было бы подходящим для подавляющего большинства объектов в программах на C, и оно имело бы было нецелесообразно добавлять квалификатор ко всем объектам, для которых такое предположение было бы уместным. С другой стороны, некоторым программам необходимо использовать некоторые объекты, для которых такое предположение не будет выполнено. Чтобы устранить эту проблему, в Стандарте говорится, что компиляторы могут предполагать, что объекты, которые не объявлены volatile
, не будут иметь своего значения, наблюдаемого или измененного способами, которые находятся вне элемента управления компилятором, или будут находиться вне разумного понимания компилятора.
Поскольку различные платформы могут иметь разные способы, в которых объекты могут наблюдаться или модифицироваться вне элемента управления компилятором, уместно, чтобы качественные компиляторы для этих платформ должны отличаться в своей точной обработке volatile
семантикой. К сожалению, поскольку стандарт не смог предположить, что качественные компиляторы, предназначенные для низкоуровневого программирования на платформе, должны обрабатывать volatile
таким образом, чтобы распознавать любые и все соответствующие эффекты конкретной операции чтения/записи на этой платформе, многие компиляторы не соответствуют делая это таким образом, что усложняет обработку таких вещей, как фоновый ввод-вывод, эффективным, но не может быть нарушена оптимизацией компилятора.
он не позволяет компилятору автоматически изменять значения переменных. переменная volatile используется для динамического использования.