Почему летучий нужен в C?
Почему volatile
нужен в C? Для чего его используют? Что это будет делать?
17 ответов
Volatile говорит компилятору не оптимизировать ничего, что связано с переменной 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 только один раз и затем перейдет в бесконечный цикл. Это не то, что вы хотите.
Чтобы обойти это, нужно объявить гаджет-указатель как 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;
}
volatile
в C фактически возникла с целью не кэшировать значения переменной автоматически. Он скажет машине не кэшировать значение этой переменной. Таким образом, он будет принимать значение данного volatile
переменная из основной памяти каждый раз, когда он сталкивается с ней. Этот механизм используется потому, что в любое время значение может быть изменено ОС или любым прерыванием. Итак, используя volatile
поможет нам получить доступ к значению заново каждый раз.
Другое использование для volatile
это обработчики сигналов. Если у вас есть такой код:
quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
Компилятору разрешено замечать, что тело цикла не касается quit
переменная и преобразовать цикл в while (true)
петля. Даже если quit
переменная устанавливается в обработчике сигнала для SIGINT
а также SIGTERM
; у компилятора нет возможности узнать это.
Однако если quit
переменная объявлена volatile
, компилятор вынужден загружать его каждый раз, потому что он может быть изменен в другом месте. Это именно то, что вы хотите в этой ситуации.
volatile
сообщает компилятору, что ваша переменная может быть изменена другими способами, а не кодом, который обращается к ней. например, это может быть область памяти, отображаемая I/O. Если это не указано в таких случаях, некоторые переменные доступы могут быть оптимизированы, например, их содержимое может храниться в регистре, и ячейка памяти не будет считываться снова.
Смотрите эту статью Андрея Александреску, " volatile - лучший друг многопоточного программиста "
Ключевое слово volatile было разработано, чтобы предотвратить оптимизацию компилятора, которая может сделать код некорректным при наличии определенных асинхронных событий. Например, если вы объявляете примитивную переменную как volatile, компилятору не разрешается кэшировать ее в регистре - обычная оптимизация, которая была бы катастрофической, если бы эта переменная была разделена между несколькими потоками. Итак, общее правило: если у вас есть переменные примитивного типа, которые должны совместно использоваться несколькими потоками, объявляйте эти переменные переменными. Но вы можете сделать гораздо больше с этим ключевым словом: вы можете использовать его для перехвата кода, который не является потокобезопасным, и вы можете сделать это во время компиляции. Эта статья показывает, как это делается; решение включает в себя простой интеллектуальный указатель, который также упрощает сериализацию критических участков кода.
Статья относится как к C
а также C++
,
Также см. Статью " С ++ и опасности двойной проверки блокировки " Скотта Мейерса и Андрея Александреску:
Поэтому при работе с некоторыми областями памяти (например, с портами, отображаемыми в памяти, или с памятью, на которую ссылаются ISR [Программы обработки прерываний]), некоторые оптимизации должны быть приостановлены. Для определения специальной обработки для таких расположений существует volatile, а именно: (1) содержимое переменной volatile "нестабильно" (может изменяться неизвестными компилятору средствами), (2) все записи в volatile данные "наблюдаемы", поэтому они должны выполняться неукоснительно, и (3) все операции с изменчивыми данными выполняются в той последовательности, в которой они появляются в исходном коде. Первые два правила обеспечивают правильное чтение и письмо. Последний позволяет реализовать протоколы ввода / вывода, которые смешивают ввод и вывод. Это неофициально то, что C и 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)
все время, что приводит к бесконечному циклу.
Чтобы избежать подобных сценариев, мы объявляем флаг как volatile, мы сообщаем компилятору, что это значение может быть изменено внешним интерфейсом или другим модулем программы, т. Е. Пожалуйста, не оптимизируйте его. Это случай использования для летучих.
Предельное использование для volatile следующее. Допустим, вы хотите вычислить числовую производную функции 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, исключая возможную возможность оптимизации.
Есть два использования. Они особенно часто используются во встроенных разработках.
Компилятор не будет оптимизировать функции, которые используют переменные, которые определены с ключевым словом volatile
Volatile используется для доступа к точным ячейкам памяти в ОЗУ, ПЗУ и т. Д. Это чаще используется для управления отображаемыми в памяти устройствами, доступа к регистрам ЦП и определения местоположения определенных областей памяти.
Смотрите примеры со списком сборок. Re: использование C "volatile" ключевое слово в разработке встраиваемых
Я упомяну другой сценарий, где летучие вещества важны.
Предположим, что вы отображаете в памяти файл для более быстрого ввода-вывода, и этот файл может измениться за кулисами (например, файл не находится на вашем локальном жестком диске, а вместо этого обслуживается по сети другим компьютером).
Если вы обращаетесь к данным файла, отображенного в памяти, через указатели на энергонезависимые объекты (на уровне исходного кода), то код, сгенерированный компилятором, может извлекать одни и те же данные несколько раз, даже не зная об этом.
Если эти данные изменятся, ваша программа может использовать две или более разных версий данных и перейти в несогласованное состояние. Это может привести не только к логически некорректному поведению программы, но и к уязвимым местам в ней, если она обрабатывает ненадежные файлы или файлы из ненадежных местоположений.
Если вы заботитесь о безопасности, и вам следует, это важный сценарий для рассмотрения.
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
только гарантируется только в скомпилированном коде компилятором. Во время выполнения процессор все еще может переупорядочивать данные и назначение команд в соответствии с архитектурой процессора. Аппаратное обеспечение может получить неправильные данные (предположим, гаджет сопоставлен с аппаратным вводом / выводом). Необходим барьер памяти между данными и назначением команд.
На языке, разработанном Деннисом Ритчи, каждый доступ к любому объекту, кроме автоматических объектов, адрес которых не был взят, будет вести себя так, как если бы он вычислял адрес объекта, а затем считывал или записывал хранилище по этому адресу. Это сделало язык очень мощным, но сильно ограничило возможности оптимизации.
Хотя можно было бы добавить квалификатор, который бы пригласил компилятор предположить, что конкретный объект не будет изменен странным образом, такое предположение было бы уместно для подавляющего большинства объектов в программах на C, и это имело бы было непрактично добавлять классификатор ко всем объектам, для которых было бы целесообразно такое предположение. С другой стороны, некоторые программы должны использовать некоторые объекты, для которых такое предположение не будет выполнено. Чтобы решить эту проблему, Стандарт говорит, что компиляторы могут предполагать, что объекты, которые не объявлены volatile
их значение не будет наблюдаться или изменяться способами, которые находятся вне контроля компилятора, или будут вне разумного понимания компилятора.
Поскольку различные платформы могут по-разному наблюдать за объектами или изменять их вне контроля компилятора, целесообразно, чтобы качественные компиляторы для этих платформ отличались точной обработкой volatile
семантика. К сожалению, из-за того, что в стандарте не предлагалось обрабатывать качественные компиляторы, предназначенные для низкоуровневого программирования на платформе volatile
таким образом, чтобы распознавать любые и все соответствующие эффекты конкретной операции чтения / записи на этой платформе, многим компиляторам не удается сделать это способами, которые затрудняют обработку таких вещей, как фоновый ввод-вывод, способом, который эффективен, но не может быть сломан компилятором "оптимизации".
volatile означает, что хранилище может измениться в любое время и измениться, но что-то вне контроля пользовательской программы. Это означает, что если вы ссылаетесь на переменную, программа должна всегда проверять физический адрес (то есть отображенный ввод fifo), а не использовать его в кэшированном виде.
Проще говоря, он говорит компилятору не выполнять какую-либо оптимизацию для конкретной переменной. Переменные, которые отображаются в регистр устройства, изменяются устройством косвенно. В этом случае необходимо использовать энергозависимые.
Вики говорят все о volatile
:
И документ ядра Linux также делает отличную запись о volatile
:
Volatile может быть изменено извне скомпилированного кода (например, программа может отобразить переменную volatile в регистр отображения памяти). Компилятор не будет применять определенные оптимизации к коду, который обрабатывает переменную volatile - например, он выиграл ' загрузить его в регистр без записи в память. Это важно при работе с аппаратными регистрами.
Как справедливо предположили многие здесь, популярное использование ключевого слова volatile- пропускать оптимизацию переменной volatile.
Лучшее преимущество, которое приходит на ум и о котором стоит упомянуть после чтения о volatile, - это предотвращение отката переменной в случаеlongjmp
. Нелокальный прыжок.
Что это значит?
Это просто означает, что последнее значение будет сохранено после того, как вы выполните раскрутку стека, чтобы вернуться к некоторому предыдущему кадру стека; как правило, в случае ошибочного сценария.
Поскольку это выходит за рамки этого вопроса, я не буду вдаваться в подробности setjmp/longjmp
здесь, но об этом стоит прочитать; и как можно использовать функцию волатильности для сохранения последнего значения.
Это не позволяет компилятору автоматически изменять значения переменных. переменная переменная предназначена для динамического использования.