Почему летучий нужен в C?

355

Почему volatile требуется в C? Для чего его используют? Что он будет делать?

Теги:
declaration
volatile

16 ответов

317

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;
    }
  • 40
    Лично я предпочел бы, чтобы целочисленный размер был явным, например, int8 / int16 / int32 при работе с оборудованием. Хороший ответ, хотя;)
  • 16
    да, вы должны объявлять вещи с фиксированным размером регистра, но эй - это всего лишь пример.
Показать ещё 14 комментариев
152

Другим использованием для volatile являются обработчики сигналов. Если у вас есть такой код:

quit = 0;
while (!quit)
{
    /* very small loop which is completely visible to the compiler */
}

Компилятор может заметить, что тело цикла не касается переменной quit и преобразует цикл в цикл while (true). Даже если переменная quit установлена ​​на обработчик сигнала для SIGINT и SIGTERM; компилятор не знает этого.

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

  • 0
    когда вы говорите «компилятор вынужден загружать его каждый раз, это как когда компилятор решает оптимизировать определенную переменную, а мы не объявляем переменную как volatile, во время выполнения эта переменная загружается в регистры ЦП, не находящиеся в памяти ?
  • 1
    @AmitSinghTomar Это означает, что он говорит: каждый раз, когда код проверяет значение, он перезагружается. В противном случае компилятор позволили предположить , что функции , которые не принимают ссылку на переменный не могут изменить его, так что если предположить , как CesarB Подразумевается , что выше цикл не установлен quit , компилятор может оптимизировать его в постоянный цикл, предполагая, что нет никакого способа, чтобы quit был изменен между итерациями. NB: Это не обязательно хорошая замена для реального программирования с защитой потоков.
Показать ещё 5 комментариев
133

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

  • 16
    простое и лучшее объяснение.
  • 0
    Возникли? Разве «volatile» изначально не был заимствован из C ++? Ну, кажется, я помню ...
Показать ещё 4 комментария
54

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

25

См. эту статью Андрея Александреску, " volatile - Лучший друг с несколькими программистами

Ключевое слово volatileразработан для предотвращения компиляции оптимизация, которая может отображать код неверно в присутствии определенных асинхронные события. Например, если вы объявляете примитивную переменную как volatile, компилятор не разрешено кэшировать его в реестре - общая оптимизация, которая была бы катастрофически, если эта переменная была разделяемый между несколькими потоками. Итак общее правило: если у вас есть переменные примитивного типа, который должен быть общим среди нескольких потоков, объявите эти переменные volatile. Но вы можете на самом деле делают гораздо больше с этим ключевое слово: вы можете использовать его, чтобы поймать код который не является потокобезопасным, и вы можете делать это во время компиляции. Эта статья показывает, как это делается; решение включает в себя простой умный указатель, который также упрощает сериализацию критические разделы кода.

Статья применяется как к C, так и к C++.

Также см. статью " С++ и опасения блокировки с двойным проверкой" Скотта Мейерса и Андрея Александреску:

Таким образом, при работе с некоторыми ячейками памяти (например, с отображенными в памяти портами или памятью, на которые ссылаются ISR [Процедуры обслуживания прерываний]), некоторые оптимизации должны быть приостановлены. (1) содержание изменчивой переменной "неустойчиво" (может быть изменено неизвестным компилятору способом), (2) все записи в изменчивые данные "наблюдаемы", поэтому они должны выполняться религиозно, и (3) все операции с изменчивыми данными выполняются в последовательности, в которой они отображаются в исходном коде. Первые два правила обеспечивают правильное чтение и письмо. Последний позволяет реализовать протоколы ввода/вывода, которые смешивают ввод и вывод. Это неофициально то, что нестабильные гарантии C и С++.

  • 0
    Указывает ли стандарт, считается ли чтение «наблюдаемым поведением», если значение никогда не используется? У меня сложилось впечатление, что так и должно быть, но когда я заявил, что это было в другом месте, кто-то вызвал меня на цитату. Мне кажется, что на любой платформе, где чтение изменчивой переменной могло бы иметь какой-либо эффект, компилятор должен был бы генерировать код, который выполняет каждое указанное чтение только один раз; без этого требования было бы сложно написать код, который генерировал бы предсказуемую последовательность операций чтения.
  • 0
    @supercat: Согласно первой статье: «Если вы используете модификатор volatile для переменной, компилятор не будет кэшировать эту переменную в регистрах - каждый доступ будет попадать в фактическую ячейку памяти этой переменной». Кроме того, в разделе §6.7.3.6 стандарта c99 говорится: «Объект, имеющий тип с квалификацией volatile, может быть изменен способами, неизвестными для реализации, или иметь другие неизвестные побочные эффекты». Это также подразумевает, что изменяемые переменные не могут кэшироваться в регистрах и что все операции чтения и записи должны выполняться в порядке относительно точек последовательности, что они фактически наблюдаемы.
Показать ещё 6 комментариев
21

Мое простое объяснение:

В некоторых сценариях, основанных на логике или коде, компилятор будет оптимизировать переменные, которые, по его мнению, не меняются. Ключевое слово 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) все время, что приведет к бесконечному циклу.

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

17

Маржинальное использование для неустойчивого заключается в следующем. Скажем, вы хотите вычислить численную производную функции 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, лишив возможную возможность оптимизации.

  • 0
    В чем разница между использованием h или hh в производной формуле? Когда вычисляется hh , последняя формула использует ее как первую, без разницы. Может быть, это должно быть (f(x+h) - f(x))/hh ?
  • 2
    Разница между h и hh состоит в том, что hh усекается до некоторой отрицательной степени двух операцией x + h - x . В этом случае x + hh и x отличаются точно на hh . Вы также можете взять свою формулу, она даст тот же результат, так как x + h и x + hh равны (это знаменатель, который важен здесь).
Показать ещё 3 комментария
10

Существует два варианта использования. Они особенно часто используются во встроенной разработке.

  • Компилятор не будет оптимизировать функции, которые используют переменные, которые определены с помощью volatile keyword

  • Volatile используется для доступа к точным ячейкам памяти в ОЗУ, ПЗУ и т.д. Это чаще используется для управления устройствами с отображением памяти, доступа к регистрам ЦП и определения местоположения конкретной ячейки памяти.

См. примеры с листингом сборки. Re: Использование C "volatile" ключевого слова в Embedded Development

  • 0
    «Компилятор не будет оптимизировать функции, которые используют переменные, которые определены с ключевым словом volatile» - это совершенно неправильно.
9

Я расскажу о другом сценарии, в котором важны летучие.

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

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

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

Если вы заботитесь о безопасности, и вы должны, это важный сценарий для рассмотрения.

9

Volatile также полезен, если вы хотите заставить компилятор не оптимизировать определенную последовательность кода (например, для записи микро-теста).

6

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

  • 0
    Ни один компилятор не использует volatile для обозначения «физического адреса в ОЗУ» или «обхода кеша».
4

В Wiki говорится обо всем volatile:

И документ ядра Linux также отлично описывает нотацию volatile:

3

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

2

На мой взгляд, вы не должны ожидать слишком многого от 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 только гарантируется только скомпилированным кодом компилятором. Во время работы процессор все еще может переупорядочить данные и назначение команд в отношении архитектуры процессора. Аппаратное обеспечение может получить неверные данные (предположим, что гаджет отображается на аппаратный ввод-вывод). Необходим барьер памяти между данными и назначением команд.

  • 2
    Я бы сказал, что volatile используется для предотвращения оптимизации, которая обычно полезна и желательна для компилятора. Как написано, похоже, что volatile ухудшает производительность без причины. Что касается того, достаточно ли это, это будет зависеть от других аспектов системы, о которых программист может знать больше, чем компилятор. С другой стороны, если процессор гарантирует, что инструкция для записи по определенному адресу очистит кэш-память ЦП, но компилятор не предоставил никакого способа сбрасывать переменные с кэшем регистров, о которых ЦП ничего не знает, очистка кеша была бы бесполезной.
1

В языке, разработанном Деннисом Ритчи, каждый доступ к любому объекту, кроме автоматических объектов, адрес которых не был взят, будет вести себя так, как если бы он вычислил адрес объекта, а затем прочитал или написал хранилище по этому адресу. Это сделало язык очень мощным, но сильно ограниченным возможностями оптимизации.

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

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

-2

он не позволяет компилятору автоматически изменять значения переменных. переменная volatile используется для динамического использования.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню