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


Ответы:


425

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;
    }

46
Лично я предпочел бы, чтобы целочисленный размер был явным, например, int8 / int16 / int32 при работе с оборудованием. Хороший ответ, хотя;)
Тонило

22
да, вы должны объявлять вещи с фиксированным размером регистра, но эй - это всего лишь пример.
Нильс Пипенбринк

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

14
Читать спецификацию C сложнее. Volatile имеет определенное поведение только для устройств ввода-вывода с отображением в памяти или памяти, к которой применяется асинхронная функция прерывания. Он ничего не говорит о многопоточности, и компилятор, который оптимизирует удаленный доступ к памяти, к которой обращаются несколько потоков, является совместимым.
Эфимент

17
@tolomea: совершенно неправильно. грустные 17 человек не знают этого. Летучий не забор памяти. это связано только с тем, чтобы избежать оптимизации кода при оптимизации на основе предположения о невидимых побочных эффектах .
v.oddou

188

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


Возникли? Разве «volatile» изначально не был заимствован из C ++? Ну, кажется, я помню ...
syntaxerror

Все это не является изменчивым - оно также запрещает некоторые переупорядочения, если указано как изменчивое ..
FaceBro

4
@FaceBro: Цель volatileсостояла в том, чтобы позволить компиляторам оптимизировать код, в то же время позволяя программистам достигать семантики, которая была бы достигнута без такой оптимизации. Авторы Стандарта ожидали, что качественные реализации будут поддерживать любую семантику, полезную с учетом их целевых платформ и областей применения, и не ожидали, что разработчики компиляторов будут стремиться предлагать семантику самого низкого качества, которая соответствует Стандарту и не была на 100%. глупо (обратите внимание, что авторы Стандарта прямо признают это в обосновании ...
суперкат

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

1
@syntaxerror, как его можно позаимствовать у C ++, когда C был более чем на десять лет старше C ++ (как в первых выпусках, так и в первых стандартах)?
phuclv

178

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

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

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

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


когда вы говорите «компилятор вынужден загружать его каждый раз, это как когда компилятор решает оптимизировать определенную переменную, а мы не объявляем переменную как volatile, во время выполнения эта переменная загружается в регистры ЦП, не находящиеся в памяти ?
Амит Сингх Томар

1
@AmitSinghTomar Это означает, что он говорит: каждый раз, когда код проверяет значение, он перезагружается. В противном случае компилятору разрешается предполагать, что функции, которые не принимают ссылку на переменную, не могут ее изменить, поэтому, предполагая, что CesarB предполагал, что вышеуказанный цикл не устанавливается quit, компилятор может оптимизировать его в постоянный цикл, предполагая, что что нет способа quitизмениться между итерациями. NB: Это не обязательно хорошая замена для реального программирования с защитой потоков.
underscore_d

если quit является глобальной переменной, то компилятор не должен оптимизировать цикл while, правильно?
Пьер Г.

2
@PierreG. Нет, компилятор всегда может предположить, что код является однопоточным, если не указано иное. То есть в отсутствие volatileили других маркеров предполагается, что ничто вне цикла не изменяет эту переменную, как только она входит в цикл, даже если это глобальная переменная.
CesarB

1
@PierreG. Да, попробуйте, например , составление 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и увидеть разницу.
CesarB

60

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


30

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

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

Статья относится как к, так Cи к C++.

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

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


Указывает ли стандарт, считается ли чтение «наблюдаемым поведением», если значение никогда не используется? У меня сложилось впечатление, что так и должно быть, но когда я заявил, что это было в другом месте, кто-то вызвал меня на цитату. Мне кажется, что на любой платформе, где чтение изменчивой переменной могло бы иметь какой-либо эффект, компилятор должен был бы генерировать код, который выполняет каждое указанное чтение только один раз; без этого требования было бы сложно написать код, который генерировал бы предсказуемую последовательность операций чтения.
суперкат

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

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

«компилятору не разрешено кэшировать его в регистре» - большинство архитектур RISC являются машинами регистров, поэтому любые операции чтения-изменения-записи имеет кэшировать объект в регистрах. volatileне гарантирует атомарность.
слишком честно для этого сайта

1
@Olaf: загрузка чего-либо в регистр - это не то же самое, что кеширование. Кэширование повлияет на количество загрузок или магазинов или их время.
суперкат

28

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

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


19

Предельное использование для 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?
Сергей Жуков

2
Разница между hи hhзаключается в том hh, что операция усекается до некоторой отрицательной степени двух x + h - x. В этом случае x + hhи xотличаются ровно на hh. Вы также можете взять свою формулу, она даст тот же результат, так как x + hи x + hhравны (это знаменатель, который здесь важен).
Александр С.

3
Разве не более читабельный способ написать это будет x1=x+h; d = (f(x1)-f(x))/(x1-x)? без использования летучих.
Сергей Жуков

Любая ссылка, что компилятор может уничтожить эту вторую строку функции?
CoffeeTableEspresso

@CoffeeTableEspresso: Нет, извините. Чем больше я знаю о числах с плавающей запятой, тем больше я верю, что компилятору разрешено оптимизировать его только в том случае, если явно указано так, с -ffast-mathили эквивалентно.
Александр С.

11

Есть два использования. Они особенно часто используются во встроенных разработках.

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

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

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


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

10

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


10

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

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

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

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

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


7

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


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


5

На мой взгляд, не стоит ожидать слишком многого от 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это ухудшает производительность без всякой причины. Что касается того, достаточно ли это, это будет зависеть от других аспектов системы, о которых программист может знать больше, чем компилятор. С другой стороны, если процессор гарантирует, что инструкция для записи по определенному адресу очистит кэш-память ЦП, но компилятор не предоставил способа очистить переменные с кэшем регистров, о которых ЦП ничего не знает, очистка кеша была бы бесполезной.
суперкат

5

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

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

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


5

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


1
Есть ли в этом ответе что-то новое, что не было упомянуто ранее?
slfan

3

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


0

Как справедливо предлагают многие здесь, популярное использование ключевого слова volatile - пропустить оптимизацию переменной volatile.

Самое лучшее преимущество, которое приходит на ум и стоит упомянуть после прочтения о volatile - это предотвращение отката переменной в случае a longjmp. Нелокальный прыжок.

Что это значит?

Это просто означает, что последнее значение будет сохранено после разматывания стека , чтобы вернуться к некоторому предыдущему кадру стека; как правило, в случае какого-либо ошибочного сценария.

Поскольку это выходит за рамки этого вопроса, я не буду вдаваться в подробности setjmp/longjmp, но о нем стоит прочитать; и как функция волатильности может использоваться для сохранения последнего значения.


-2

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.