Почему существует изменчивый?

Что делает ключевое слово volatile? В C++ какую проблему он решает?

В моем случае я сознательно никогда не нуждался в этом.

Существует интригующий метод, который заставляет ваш компилятор обнаруживать возможные состояния гонки, который сильно зависит от ключевого слова volatile, вы можете прочитать об этом в http://www.ddj.com/cpp/184403766.

Neno Ganchev 16.09.2008 18:44

Вот интересное обсуждение volatile в отношении паттерна Singleton: aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf

chessguy 16.09.2008 18:05

Это хороший ресурс с примером того, как можно эффективно использовать volatile, сложенный в довольно непрофессиональных терминах. Ссылка: Publications.gbdirect.co.uk/c_book/chapter8/…

Optimized Coder 11.11.2011 10:48
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
238
3
46 027
19
Перейти к ответу Данный вопрос помечен как решенный

Ответы 19

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

Это справедливо не только для встроенных систем, но и для разработки всех драйверов устройств.

Mladen Janković 16.09.2008 18:11

Единственный раз, когда мне это понадобилось на 8-битной шине ISA, где вы дважды читали один и тот же адрес - у компилятора была ошибка, и она игнорировалась (ранний Zortech C++)

Martin Beckett 13.01.2009 08:13

Volatile очень редко бывает достаточно для управления внешними устройствами. Его семантика неверна для современного MMIO: приходится делать слишком много объектов изменчивыми, а это вредит оптимизации. Но современный MMIO ведет себя как обычная память до тех пор, пока не будет установлен флаг, поэтому volatile не требуется. Многие драйверы никогда не используют volatile.

curiousguy 23.11.2019 01:56
Ответ принят как подходящий

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

Раньше я работал с двухпортовой оперативной памятью в многопроцессорной системе на прямом C. Мы использовали аппаратно управляемое 16-битное значение в качестве семафора, чтобы узнать, когда другой парень закончил. По сути, мы сделали это:

void waitForSemaphore()
{
   volatile uint16_t* semPtr = WELL_KNOWN_SEM_ADDR;/*well known address to my semaphore*/
   while ((*semPtr) != IS_OK_FOR_ME_TO_PROCEED);
}

Без volatile оптимизатор видит цикл как бесполезный (этот парень никогда не устанавливает значение! Он чокнутый, избавьтесь от этого кода!), И мой код продолжился бы, не получив семафор, что впоследствии вызовет проблемы.

В таком случае, что бы произошло, если бы вместо него был написан uint16_t* volatile semPtr? Это должно пометить указатель как изменчивый (вместо указанного значения), чтобы проверять сам указатель, например semPtr == SOME_ADDR не может быть оптимизирован. Однако это также подразумевает изменчивое указанное значение. Нет?

Zyl 26.08.2014 00:54

@Zyl Нет, это не так. На практике то, что вы предлагаете, скорее всего и произойдет. Но теоретически можно было бы получить компилятор, который оптимизирует доступ к значениям, потому что он решил, что ни одно из этих значений никогда не изменяется. И если бы вы имели в виду volatile для применения к значению, а не к указателю, вы бы ошиблись. Опять же, маловероятно, но лучше ошибиться, поступая правильно, чем воспользоваться поведением, которое сегодня работает.

iheanyi 24.06.2015 00:56

Как бы это не быть гонкой на *semPtr?

Baum mit Augen 17.10.2015 15:14

@Doug T. Лучшее объяснение - это

machineaddict 18.02.2016 10:57

@iheanyi "было решено, что ни одно из этих значений никогда не изменялось" Решили неправильно. Вы только что описали компилятор с ошибками.

curiousguy 15.07.2018 07:16

@BaummitAugen: Этот ответ (и, очевидно, код) предшествовал C++ 11. Без std::atomic единственным вариантом было взломать все самостоятельно с помощью встроенных asm или вызовов библиотечных функций для устранения любых необходимых препятствий. Вы можете обернуть энергозависимый доступ функцией или макросом read_once(), как это делает ядро ​​Linux, но это все равно будет сводиться к этому, чтобы получить asm, который вы хотите, в любой разумной реализации, где выровненный volatile uint16_t может быть прочитан / записан атомарно. (т.е. на большинстве конкретных платформ фактическое поведение, которое вы получаете от этого, четко определено.)

Peter Cordes 15.07.2018 16:06

@PeterCordes Учитывая, что C++ 11 существует уже некоторое время, этот ответ следует обновить, чтобы хотя бы явно указать на его устаревшую природу, не так ли? В нынешнем виде я считаю этот ответ несколько вводящим в заблуждение (конечно, не намеренно, но времена изменились).

Baum mit Augen 15.07.2018 16:16

@BaummitAugen: да, наверное. Хотя, если бы это было аппаратное устройство, изменяющее известную ячейку памяти, а не другой поток в той же программе, этот код все равно был бы на 100% подходящим. (Если только этот семафор не контролирует доступ к какой-либо другой памяти, в этом случае вам нужно, чтобы atomic<uint16_t> или volatile atomic<uint16_t> выполняли загрузку-загрузку. В этом случае компилятор не может поднять нагрузку, потому что это сделает цикл бесконечным, поэтому volatile не здесь не нужно. Может и оптимизирует ли компилятор две атомные нагрузки?)

Peter Cordes 15.07.2018 16:20

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

iheanyi 17.07.2018 22:54

@iheanyi Нет, это компилятор решил игнорировать ключевое слово volatile, которое присутствует в коде.

curiousguy 18.07.2018 01:17

@curiousguy нет, только потому, что ключевое слово volatile появляется один раз, не означает, что все внезапно становится нестабильным. Я привел сценарий, в котором компилятор поступает правильно и достигает результата, который противоречит ошибочным ожиданиям программиста. Точно так же, как "самый неприятный синтаксический анализ" не является признаком ошибки компилятора, и здесь тоже.

iheanyi 18.07.2018 02:54

@iheanyi Можете ли вы показать псевдокод, который учитывает изменчивость, что приведет к нежелательному результату?

curiousguy 18.07.2018 22:14

@curiousguy зацените этот вопрос - этого достаточно?

iheanyi 18.07.2018 23:27

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

David Schwartz 18.10.2018 02:55

@DavidSchwartz - не могли бы вы уточнить этот пример? Что бы сделал atomic_int? В моем мире мы в основном используем «атомарные» для описания серии операций, которые необходимо выполнять последовательно, без прерывания другими операциями. [продолжение ...]

ysap 24.05.2020 04:38

[продолжение ...] Например, при обновлении битового поля некоторых регистров отображаемого в память устройства для этого адреса используется схема чтения-изменения-записи. Важно защитить доступ от прерывания и воздействия 2-го потока или обработчика IRQ. Таким образом, доступ ограничен «атомарными» ключевыми словами для отключения IRQ. Я не уверен, что будет делать ваш предлагаемый тип и как это связано с предотвращением оптимизации при последовательном доступе к одному и тому же изменчивому ресурсу.

ysap 24.05.2020 04:38

@ysap Предположим, что платформа предлагает тип под названием atomic_int, который, как указано в документации, ведет себя точно так же, как volatile int. В этом случае вам не нужно использовать volatile на этой платформе, поскольку вы можете использовать atomic_int. Но в этом ответе говорится, что необходим volatile. Это не правильно. Большинство платформ предлагают такие вещи, как atomic_int, с гарантированной семантикой без необходимости использования volatile.

David Schwartz 24.05.2020 06:56

@DavidSchwartz - так, если я вас правильно понял, вы предлагаете своего рода псевдоним для volatile int, как если бы был typedef volatile int atomic_int, а затем говорите, что использование volatile не обязательно? Если это так, то тот же аргумент можно использовать, чтобы сказать, что если система предоставляет тип с именем whole, который ведет себя как int, тогда использование int не обязательно ???! Кроме того, я думаю, что в моем мире это не будет подходящим использованием слова atomic, как описано выше. Или я полностью упустил вашу точку зрения?

ysap 24.05.2020 17:13

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

David Schwartz 25.05.2020 22:13
  1. вы должны использовать его для реализации спин-блокировок, а также некоторых (всех?) структур данных без блокировки
  2. использовать его с атомарными операциями / инструкциями
  3. однажды помог мне преодолеть ошибку компилятора (неправильно сгенерированный код при оптимизации)

Лучше использовать библиотеку, встроенные функции компилятора или встроенный ассемблерный код. Неустойчивый - ненадежный.

Zan Lynx 03.05.2009 21:16

1 и 2 оба используют атомарные операции, но volatile не обеспечивает атомарной семантики, а реализации atomic для конкретной платформы заменят необходимость использования volatile, поэтому для 1 и 2 я не согласен, для них volatile НЕ нужен.

user21037 13.02.2011 19:01

Кто что-нибудь говорит о volatile, обеспечивающем атомарную семантику? Я сказал, что вам нужно ИСПОЛЬЗОВАТЬ volatile С атомарными операциями, и если вы не думаете, что это правда, посмотрите объявления заблокированных операций Win32 API (этот парень также объяснил это в своем ответе)

Mladen Janković 17.02.2011 16:05

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

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

Из статьи Дэна Сакса «Неустойчивый как обещание»:

(...) a volatile object is one whose value might change spontaneously. That is, when you declare an object to be volatile, you're telling the compiler that the object might change state even though no statements in the program appear to change it."

Вот ссылки на три его статьи о ключевом слове volatile:

Некоторые процессоры имеют регистры с плавающей запятой, точность которых превышает 64 бита (например, 32-битный x86 без SSE, см. Комментарий Питера). Таким образом, если вы выполните несколько операций с числами с двойной точностью, вы фактически получите ответ с более высокой точностью, чем если бы вы усекали каждый промежуточный результат до 64 бит.

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

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

При вычислении числовых производных также полезно убедиться, что x + h - x == h вы определяете hh = x + h - x как изменчивый, чтобы можно было вычислить правильную дельту.

Alexandre C. 30.06.2010 15:36

+1, действительно, по моему опыту, был случай, когда вычисления с плавающей запятой давали разные результаты в отладке и выпуске, поэтому модульные тесты, написанные для одной конфигурации, терпели неудачу для другой. Мы решили эту проблему, объявив одну переменную с плавающей запятой как volatile double, а не просто double, чтобы гарантировать, что она усечена с точности FPU до точности 64-битной (RAM) перед продолжением дальнейших вычислений. Результаты были существенно другими из-за дальнейшего преувеличения ошибки с плавающей запятой.

Serge Rogatch 06.08.2015 10:05

Ваше определение «современного» немного неверно. Это затронуло только 32-битный код x86, который избегает SSE / SSE2, и он не был «современным» даже 10 лет назад. MIPS / ARM / POWER имеют 64-битные аппаратные регистры, как и x86 с SSE2. Реализации C++ x86-64 всегда используют SSE2, и у компиляторов есть такие опции, как g++ -mfpmath=sse, чтобы использовать его и для 32-битных x86. Вы можете использовать gcc -ffloat-store для принудительного округления повсюду даже при использовании x87, или вы можете установить точность x87 на 53-битную мантиссу: randomascii.wordpress.com/2012/03/21/….

Peter Cordes 15.07.2018 16:13

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

Peter Cordes 15.07.2018 16:14

@PeterCordes Тот факт, что пользовательские регистры имеют точность, равную двойной точности, не означает, что внутренние регистры, используемые для промежуточных результатов (которые никогда не называются в asm), не имеют большей точности /

curiousguy 23.11.2019 01:50

@curiousguy: на нормальных ISA это означает делает. Ваша идея привела бы к ЦП, который дает разные математические результаты в зависимости от того, где произошло прерывание + переключение контекста (и принудительное округление до ширины архитектурного регистра путем сохранения / восстановления регистров FP). Всем нужен один и тот же машинный код с одинаковыми входными данными, чтобы каждый раз выдавать один и тот же точный побитовый результат.

Peter Cordes 23.11.2019 02:11

@PeterCordes FPU instr прерываемы?

curiousguy 23.11.2019 02:15

@curiousguy: внутренние буферы, используемые во время оценки одной инструкции, являются чисто реализацией, и volatile на них не влияет. Если это микрокодированная инструкция, такая как x87 fsin, теоретически вы можете сохранить дополнительную точность между упс. Но нет, он не будет возобновлен, если его можно прервать, он будет прерван.

Peter Cordes 23.11.2019 02:22

Я думал, вы имели в виду что-то вроде MIPS f0, внутренне сохраняющего более 64-битную точность в цепочке инструкций FP, поэтому volatile для принудительного сохранения / перезагрузки будет иметь эффект. Большинство FPU (кроме 387) предоставляют только операции IEEE Basic + -/ sqrt, которые необходимы для получения правильно округленных результатов (ошибка <= 0,5ulp). Таким образом, если вы не сохраните дополнительную внутреннюю точность между инструкциями, результаты будут полностью определены. Интересный факт: AMD * делает сохраняют дополнительные внутренние данные между инструкциями FP, но не дополнительную точность; наверное что-то вроде распаковки экспоненты / мантиссы.

Peter Cordes 23.11.2019 02:25

@PeterCordes С volatile вы можете разложить вычисления и дать гарантированный эффективный тип double промежуточным значениям C++. Volatile очень надежен для этой цели: double x,y,z,a; volatile double r; r=y*z; a=x+r; (Ppl говорит, что приведение имеет тот же эффект: x+(double)(y*z), но он полагается на интерфейс компилятора для преобразования в эффективную двойную точность выражения статического типа double, что было ненадежным по крайней мере на один популярный компилятор.)

curiousguy 23.11.2019 02:33

@curiousguy: О чём ты говоришь? Раньше вы говорили о том, что архитектурные регистры являются 64-битными, а не 80-битными. Ваш последний комментарий имеет смысл, например, 32-разрядный x86 с использованием x87, где компилятор использует 80-разрядный x87 для временных файлов, таких как y*z. Как и FP_CONTRACT, gcc по умолчанию оптимизирует между операторами, а не только внутри выражений с округлением до фактического double, принудительно назначенным и приведенным, хотя FLT_EVAL_METHOD = 2 говорит, что это должно быть. Это будет медленно. Но опять же проблема только с> 64-битными регистрами.

Peter Cordes 23.11.2019 02:44

Позвольте нам продолжить обсуждение в чате.

curiousguy 23.11.2019 02:49

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

user10957435 16.12.2019 08:40

Или я путаю неточное с несогласованным?

user10957435 16.12.2019 08:40

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

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

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

Terminus 25.09.2008 14:22

По моему опыту, 99,9% всех "ошибок" оптимизации в gcc arm - это ошибки со стороны программиста. Не знаю, относится ли это к этому ответу. Просто напыщенная речь на общую тему

odinthenerd 31.12.2013 04:11

@Terminus "Это ошибочное предположение, что компилятор не оптимизирует доступ к летучим файлам." Источник?

curiousguy 15.07.2018 07:21

Большое приложение, над которым я работал в начале 1990-х, содержало обработку исключений на основе C с использованием setjmp и longjmp. Ключевое слово volatile было необходимо для переменных, значения которых необходимо было сохранить в блоке кода, который служил предложением «catch», чтобы эти переменные не сохранялись в регистрах и не стирались с помощью longjmp.

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

Помимо использования по назначению, volatile используется в (шаблонном) метапрограммировании. Его можно использовать для предотвращения случайной перегрузки, поскольку атрибут volatile (например, const) участвует в разрешении перегрузки.

template <typename T> 
class Foo {
  std::enable_if_t<sizeof(T)==4, void> f(T& t) 
  { std::cout << 1 << t; }
  void f(T volatile& t) 
  { std::cout << 2 << const_cast<T&>(t); }

  void bar() { T t; f(t); }
};

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

Обратите внимание, что код на самом деле никогда не зависит от доступа к памяти volatile.

Не могли бы вы пояснить это на примере? Это действительно помогло бы мне лучше понять. Спасибо!

batbrat 06.02.2018 18:39

«Бросок в летучей перегрузке» Приведение - это явное преобразование. Это конструкция SYNTAX. Многие люди вводят в заблуждение (даже стандартные авторы).

curiousguy 28.06.2018 04:50

Вы ДОЛЖНЫ использовать volatile при реализации структур данных без блокировки. В противном случае компилятор может оптимизировать доступ к переменной, что изменит семантику.

Другими словами, volatile сообщает компилятору, что доступ к этой переменной должен соответствовать операции чтения / записи физической памяти.

Например, вот как InterlockedIncrement объявляется в Win32 API:

LONG __cdecl InterlockedIncrement(
  __inout  LONG volatile *Addend
);

Вам абсолютно НЕ нужно объявлять переменную volatile, чтобы иметь возможность использовать InterlockedIncrement.

curiousguy 28.06.2018 05:05

Этот ответ устарел теперь, когда C++ 11 предоставляет std::atomic<LONG>, поэтому вы можете писать код без блокировки более безопасно, без проблем с оптимизацией чистых загрузок / чистых хранилищ, или переупорядочением, или чем-то еще.

Peter Cordes 15.07.2018 16:17

В стандарте C одно из мест использования volatile - это обработчик сигналов. Фактически, в стандарте C все, что вы можете безопасно сделать в обработчике сигналов, - это изменить переменную volatile sig_atomic_t или быстро выйти. Действительно, AFAIK, это единственное место в стандарте C, в котором требуется использование volatile, чтобы избежать неопределенного поведения.

ISO/IEC 9899:2011 §7.14.1.1 The signal function

¶5 If the signal occurs other than as the result of calling the abort or raise function, the behavior is undefined if the signal handler refers to any object with static or thread storage duration that is not a lock-free atomic object other than by assigning a value to an object declared as volatile sig_atomic_t, or the signal handler calls any function in the standard library other than the abort function, the _Exit function, the quick_exit function, or the signal function with the first argument equal to the signal number corresponding to the signal that caused the invocation of the handler. Furthermore, if such a call to the signal function results in a SIG_ERR return, the value of errno is indeterminate.252)

252) If any signal is generated by an asynchronous signal handler, the behavior is undefined.

Это означает, что в стандарте C вы можете написать:

static volatile sig_atomic_t sig_num = 0;

static void sig_handler(int signum)
{
    signal(signum, sig_handler);
    sig_num = signum;
}

и не более того.

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

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

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

Рассмотрим следующие случаи

1) Глобальные переменные, измененные подпрограммой обслуживания прерывания вне области видимости.

2) Глобальные переменные в многопоточном приложении.

Если мы не используем квалификатор volatile, могут возникнуть следующие проблемы

1) Код может работать не так, как ожидалось, когда включена оптимизация.

2) Код может работать не так, как ожидалось, когда прерывания разрешены и используются.

Неустойчивый: лучший друг программиста

https://en.wikipedia.org/wiki/Volatile_(computer_programming)

Размещенная вами ссылка сильно устарела и не отражает текущие передовые практики.

Tim Seguine 05.10.2016 16:48

Я должен вам напомнить, что в функции обработчика сигналов, если вы хотите получить доступ / изменить глобальную переменную (например, пометить ее как exit = true), вы должны объявить эту переменную как «изменчивую».

Кажется, ваша программа работает даже без ключевого слова volatile? Возможно, причина в этом:

Как упоминалось ранее, ключевое слово volatile помогает в таких случаях, как

volatile int* p = ...;  // point to some memory
while( *p!=0 ) {}  // loop until the memory becomes zero

Но, похоже, почти нет эффекта после вызова внешней или не встроенной функции. Например.:

while( *p!=0 ) { g(); }

Тогда с volatile или без него будет получен почти такой же результат.

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

Но будьте осторожны, когда ваша функция g () станет встроенной (либо из-за явных изменений, либо из-за хитрости компилятора / компоновщика), ваш код может сломаться, если вы забыли ключевое слово volatile!

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

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

curiousguy 08.01.2019 00:30

Избежать переносимости встраивания вызова (или «встраивания» его семантики) функцией, тело которой видно компилятору (даже во время компоновки с глобальной оптимизацией), возможно с помощью указателя функции, квалифицированного volatile: void (* volatile fun_ptr)() = fun; fun_ptr();

curiousguy 08.01.2019 00:32

В первые дни C компиляторы интерпретировали все действия, которые считывают и записывают lvalue, как операции с памятью, которые должны выполняться в той же последовательности, что и операции чтения и записи, появляющиеся в коде. Эффективность можно было бы значительно повысить во многих случаях, если бы компиляторам была предоставлена ​​определенная свобода переупорядочивания и консолидации операций, но с этим была проблема. Хотя операции часто указывались в определенном порядке просто потому, что их необходимо было указывать в порядке немного, и, таким образом, программист выбирал одну из многих не менее хороших альтернатив, это не всегда имело место. Иногда бывает важно, чтобы определенные операции выполнялись в определенной последовательности.

Какие именно детали секвенирования важны, зависит от целевой платформы и области применения. Вместо того, чтобы предоставлять особенно подробный контроль, Стандарт выбрал простую модель: если последовательность обращений выполняется с lvalue, не отвечающим требованиям volatile, компилятор может переупорядочить и объединить их по своему усмотрению. Если действие выполняется с lvalue, соответствующим volatile, качественная реализация должна предлагать любые дополнительные гарантии упорядочения, которые могут потребоваться для кода, нацеленного на ее предполагаемую платформу и поле приложения, не требуя от программистов использования нестандартного синтаксиса.

К сожалению, вместо того, чтобы определять, какие гарантии понадобятся программистам, многие компиляторы вместо этого предпочли предложить минимум гарантий, предусмотренных Стандартом. Это делает volatile гораздо менее полезным, чем следовало бы. В gcc или clang, например, программист, которому необходимо реализовать базовый «мьютекс передачи обслуживания» [такой, когда задача, которая получила и освободила мьютекс, не будет делать этого снова, пока это не сделает другая задача], должен выполнить из четырех вещей:

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

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

  3. Используйте уровень оптимизации 0, чтобы компилятор сгенерировал код, как если бы все объекты, не отвечающие требованиям register, были volatile.

  4. Используйте специфичные для gcc директивы.

Напротив, при использовании более качественного компилятора, который больше подходит для системного программирования, такого как icc, у вас будет другой вариант:

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

Для получения базового «мьютекса передачи» требуется чтение volatile (чтобы увидеть, готов ли он), и также не требуется запись volatile (другая сторона не будет пытаться повторно получить его, пока он не будет возвращен), но необходимость выполнять бессмысленную запись volatile все же лучше, чем любой из вариантов, доступных в gcc или clang.

В других ответах уже упоминается об избежании некоторой оптимизации, чтобы:

  • использовать регистры с отображением в память (или "MMIO")
  • писать драйверы устройств
  • облегчить отладку программ
  • сделать вычисления с плавающей запятой более детерминированными

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

Энергозависимое чтение похоже на операцию ввода (например, scanf или использование cin): значение, кажется, приходит извне программы, поэтому любое вычисление, зависящее от значения, должно начинаться после него..

Энергозависимая запись похожа на операцию вывода (например, printf или использование cout): значение, кажется, передается вне программы, поэтому, если значение зависит от вычисления, его необходимо завершить до.

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

Без volatile ваши вычисления могут быть запущены компилятором раньше, так как ничто не помешало бы переупорядочить вычисления с такими функциями, как измерение времени.

Все ответы отличные. Но в довершение всего я хотел бы привести пример.

Ниже представлена ​​небольшая программа cpp:

#include <iostream>

int x;

int main(){
    char buf[50];
    x = 8;

    if (x == 8)
        printf("x is 8\n");
    else
        sprintf(buf, "x is not 8\n");

    x=1000;
    while(x > 5)
        x--;
    return 0;
}

Теперь давайте сгенерируем сборку приведенного выше кода (и я вставлю только те части сборки, которые здесь важны):

Команда для генерации сборки:

g++ -S -O3 -c -fverbose-asm -Wa,-adhln assembly.cpp

И сборка:

main:
.LFB1594:
    subq    , %rsp    #,
    .seh_stackalloc 40
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC0(%rip), %rcx     #,
 # assembly.cpp:7:     x = 8;
    movl    , x(%rip)  #, x
 # assembly.cpp:10:         printf("x is 8\n");
    call    _ZL6printfPKcz.constprop.0   #
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    movl    , x(%rip)  #, x
    addq    , %rsp    #,
    ret 
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

Вы можете видеть в сборке, что код сборки не был сгенерирован для sprintf, потому что компилятор предположил, что x не будет изменяться вне программы. То же самое и с петлей while. Цикл while был полностью удален из-за оптимизации, поскольку компилятор увидел в нем бесполезный код и, таким образом, напрямую присвоил 5x (см. movl , x(%rip)).

Проблема возникает, если внешний процесс / оборудование изменит значение x где-то между x = 8; и if (x == 8). Мы ожидали, что блок else будет работать, но, к сожалению, компилятор вырезал эту часть.

Теперь, чтобы решить эту проблему, в assembly.cpp давайте изменим int x; на volatile int x; и быстро увидим сгенерированный ассемблерный код:

main:
.LFB1594:
    subq    4, %rsp   #,
    .seh_stackalloc 104
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:7:     x = 8;
    movl    , x(%rip)  #, x
 # assembly.cpp:9:     if (x == 8)
    movl    x(%rip), %eax    # x, x.1_1
 # assembly.cpp:9:     if (x == 8)
    cmpl    , %eax     #, x.1_1
    je  .L11     #,
 # assembly.cpp:12:         sprintf(buf, "x is not 8\n");
    leaq    32(%rsp), %rcx   #, tmp93
    leaq    .LC0(%rip), %rdx     #,
    call    _ZL7sprintfPcPKcz.constprop.0    #
.L7:
 # assembly.cpp:14:     x=1000;
    movl    00, x(%rip)   #, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_15
    cmpl    , %eax     #, x.3_15
    jle .L8  #,
    .p2align 4,,10
.L9:
 # assembly.cpp:16:         x--;
    movl    x(%rip), %eax    # x, x.4_3
    subl    , %eax     #, _4
    movl    %eax, x(%rip)    # _4, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_2
    cmpl    , %eax     #, x.3_2
    jg  .L9  #,
.L8:
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    addq    4, %rsp   #,
    ret 
.L11:
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC1(%rip), %rcx     #,
    call    _ZL6printfPKcz.constprop.1   #
    jmp .L7  #
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

Здесь вы можете видеть, что коды сборки для цикла sprintf, printf и while были сгенерированы. Преимущество состоит в том, что если переменная x изменяется какой-либо внешней программой или оборудованием, часть кода sprintf будет выполнена. Точно так же цикл while теперь можно использовать для ожидания занятости.

Я хотел бы процитировать слова Херба Саттера из его GotW # 95, которые могут помочь понять значение переменных volatile:

C++volatile variables (which have no analog in languages like C# and Java) are always beyond the scope of this and any other article about the memory model and synchronization. That’s because C++volatile variables aren’t about threads or communication at all and don’t interact with those things. Rather, a C++volatile variable should be viewed as portal into a different universe beyond the language — a memory location that by definition does not obey the language’s memory model because that memory location is accessed by hardware (e.g., written to by a daughter card), have more than one address, or is otherwise “strange” and beyond the language. So C++volatile variables are universally an exception to every guideline about synchronization because are always inherently “racy” and unsynchronizable using the normal tools (mutexes, atomics, etc.) and more generally exist outside all normal of the language and compiler including that they generally cannot be optimized by the compiler (because the compiler isn’t allowed to know their semantics; a volatile int vi; may not behave anything like a normal int, and you can’t even assume that code like vi = 5; int read_back = vi; is guaranteed to result in read_back == 5, or that code like int i = vi; int j = vi; that reads vi twice will result in i == j which will not be true if vi is a hardware counter for example).

Другие вопросы по теме