Зачем volatile
нужен в С? Для чего его используют? Что это будет делать?
Зачем volatile
нужен в С? Для чего его используют? Что это будет делать?
Ответы:
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
состояла в том, чтобы позволить компиляторам оптимизировать код, в то же время позволяя программистам достигать семантики, которая была бы достигнута без такой оптимизации. Авторы Стандарта ожидали, что качественные реализации будут поддерживать любую семантику, полезную с учетом их целевых платформ и областей применения, и не ожидали, что разработчики компиляторов будут стремиться предлагать семантику самого низкого качества, которая соответствует Стандарту и не была на 100%. глупо (обратите внимание, что авторы Стандарта прямо признают это в обосновании ...
Другое использование для volatile
это обработчики сигналов. Если у вас есть такой код:
int 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
или других маркеров предполагается, что ничто вне цикла не изменяет эту переменную, как только она входит в цикл, даже если это глобальная переменная.
extern int global; void fn(void) { while (global != 0) { } }
с gcc -O3 -S
и посмотреть на полученный файл сборки, на моей машине это делает movl global(%rip), %eax
; testl %eax, %eax
; je .L1
; .L4: jmp .L4
то есть бесконечный цикл, если глобальное не ноль. Затем попробуйте добавить volatile
и увидеть разницу.
volatile
сообщает компилятору, что ваша переменная может быть изменена другими способами, а не кодом, который обращается к ней. например, это может быть область памяти, отображаемая I / O. Если это не указано в таких случаях, некоторые переменные доступы могут быть оптимизированы, например, их содержимое может храниться в регистре, и ячейка памяти не будет считываться снова.
Смотрите эту статью Андрея Александреску, " volatile - лучший друг многопоточного программиста "
Летучее ключевое слово было разработано , чтобы предотвратить оптимизацию компилятора , который может лишить код неправильного в присутствии некоторых асинхронных событий. Например, если вы объявляете примитивную переменную как volatile , компилятору не разрешается кэшировать ее в регистре - обычная оптимизация, которая была бы катастрофической, если бы эта переменная была распределена между несколькими потоками. Таким образом , общее правило, если у вас есть переменные примитивного типа , которые должны быть разделены между несколькими потоками, объявить эти переменные летучий, Но вы можете сделать гораздо больше с этим ключевым словом: вы можете использовать его для перехвата кода, который не является потокобезопасным, и вы можете сделать это во время компиляции. Эта статья показывает, как это делается; Решение включает в себя простой интеллектуальный указатель, который также упрощает сериализацию критических участков кода.
Статья относится как к, так C
и к C++
.
Также см. Статью « С ++ и опасности двойной проверки блокировки » Скотта Мейерса и Андрея Александреску:
Таким образом, при работе с некоторыми областями памяти (например, с отображенными в память портами или памятью, на которую ссылаются ISR [Interrupt Service Routines]), некоторые оптимизации должны быть приостановлены. volatile существует для указания специального режима для таких расположений, а именно: (1) содержимое переменной volatile «нестабильно» (может изменяться неизвестным для компилятора способом), (2) все записи в volatile данные «наблюдаемы», поэтому они должны выполняться неукоснительно, и (3) все операции с изменчивыми данными выполняются в той последовательности, в которой они появляются в исходном коде. Первые два правила обеспечивают правильное чтение и письмо. Последний позволяет реализовать протоколы ввода / вывода, которые смешивают ввод и вывод. Это неофициально то, что C и C ++ изменчиво гарантирует.
volatile
не гарантирует атомарность.
Мое простое объяснение:
В некоторых сценариях, основанных на логике или коде, компилятор выполняет оптимизацию переменных, которые, по его мнению, не изменяются. В 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, исключая возможную возможность оптимизации.
h
или hh
в производной формуле? Когда hh
вычисляется последняя формула использует его, как первая, без разницы. Может быть так и должно быть (f(x+h) - f(x))/hh
?
h
и hh
заключается в том hh
, что операция усекается до некоторой отрицательной степени двух x + h - x
. В этом случае x + hh
и x
отличаются ровно на hh
. Вы также можете взять свою формулу, она даст тот же результат, так как x + h
и x + hh
равны (это знаменатель, который здесь важен).
x1=x+h; d = (f(x1)-f(x))/(x1-x)
? без использования летучих.
-ffast-math
или эквивалентно.
Есть два использования. Они особенно часто используются во встроенных разработках.
Компилятор не будет оптимизировать функции, которые используют переменные, которые определены с ключевым словом volatile
Volatile используется для доступа к точным ячейкам памяти в ОЗУ, ПЗУ и т. Д. Это чаще используется для управления отображаемыми в памяти устройствами, доступа к регистрам ЦП и определения местоположения определенных областей памяти.
Смотрите примеры со списком сборок. Re: использование C "volatile" ключевое слово в разработке встраиваемых
Volatile также полезно, когда вы хотите заставить компилятор не оптимизировать конкретную последовательность кода (например, для написания микропроцессора).
Я упомяну другой сценарий, где летучие вещества важны.
Предположим, что вы отображаете в памяти файл для более быстрого ввода-вывода, и этот файл может измениться за кулисами (например, файл не находится на вашем локальном жестком диске, а вместо этого обслуживается по сети другим компьютером).
Если вы обращаетесь к данным файла, отображенного в памяти, через указатели на энергонезависимые объекты (на уровне исходного кода), то код, сгенерированный компилятором, может извлекать одни и те же данные несколько раз, даже не зная об этом.
Если эти данные изменятся, ваша программа может использовать две или более разных версий данных и перейти в несогласованное состояние. Это может привести не только к логически некорректному поведению программы, но и к уязвимым местам в ней, если она обрабатывает ненадежные файлы или файлы из ненадежных расположений.
Если вы заботитесь о безопасности, и вам следует, это важный сценарий для рассмотрения.
volatile означает, что хранилище может измениться в любое время и измениться, но что-то вне контроля пользовательской программы. Это означает, что если вы ссылаетесь на переменную, программа должна всегда проверять физический адрес (то есть отображенный ввод fifo), а не использовать его в кэшированном виде.
Вики говорят все о 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 может быть изменено извне скомпилированного кода (например, программа может отобразить переменную volatile в регистр с отображением в памяти.) Компилятор не будет применять определенные оптимизации к коду, который обрабатывает переменную volatile - например, он выиграл ' загрузить его в регистр без записи в память. Это важно при работе с аппаратными регистрами.
Как справедливо предлагают многие здесь, популярное использование ключевого слова volatile - пропустить оптимизацию переменной volatile.
Самое лучшее преимущество, которое приходит на ум и стоит упомянуть после прочтения о volatile - это предотвращение отката переменной в случае a longjmp
. Нелокальный прыжок.
Что это значит?
Это просто означает, что последнее значение будет сохранено после разматывания стека , чтобы вернуться к некоторому предыдущему кадру стека; как правило, в случае какого-либо ошибочного сценария.
Поскольку это выходит за рамки этого вопроса, я не буду вдаваться в подробности setjmp/longjmp
, но о нем стоит прочитать; и как функция волатильности может использоваться для сохранения последнего значения.