Почему летучие существуют?
Что это volatile
ключевое слово делать? В C++ какую проблему это решает?
В моем случае я никогда сознательно не нуждался в этом.
19 ответов
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
оптимизатор видит цикл как бесполезный (парень никогда не устанавливает значение! Он с ума сошел, избавьтесь от этого кода!), и мой код продолжит работу, не получив семафор, что вызовет проблемы в дальнейшем.
volatile
необходим при разработке встроенных систем или драйверов устройств, где необходимо читать или записывать отображаемое в память аппаратное устройство. Содержимое определенного регистра устройства может измениться в любое время, поэтому вам нужно volatile
ключевое слово, чтобы компилятор не оптимизировал такой доступ.
Большинство современных процессоров имеют регистры с плавающей запятой, которые имеют точность более 64 бит. Таким образом, если вы выполняете несколько операций над числами двойной точности, вы на самом деле получаете ответ с более высокой точностью, чем если бы вы усекали каждый промежуточный результат до 64 бит.
Обычно это замечательно, но это означает, что в зависимости от того, как компилятор назначил регистры и выполнил оптимизацию, у вас будут разные результаты для одинаковых операций с одинаковыми входными данными. Если вам нужна согласованность, вы можете заставить каждую операцию вернуться в память, используя ключевое слово volatile.
Это также полезно для некоторых алгоритмов, которые не имеют алгебраического смысла, но уменьшают ошибку с плавающей запятой, например, суммирование по Кахану. Алгебраически это не nop, поэтому он часто будет неправильно оптимизирован, если некоторые промежуточные переменные не являются изменчивыми.
Из статьи о встроенных системах Дэна Сакса:
"Под изменчивым объектом понимается объект, значение которого может меняться самопроизвольно. То есть, когда вы объявляете объект изменчивым, вы сообщаете компилятору, что объект может изменить состояние, даже если в программе нет операторов, которые могли бы его изменить".
Ссылки на 2 замечательные статьи г-на Сакса, касающиеся ключевого слова volatile:
http://www.embedded.com/columns/programmingpointers/174300478 http://www.embedded.com/columns/programmingpointers/175801310
Вы ДОЛЖНЫ использовать volatile при реализации структур данных без блокировки. В противном случае компилятор может оптимизировать доступ к переменной, что изменит семантику.
Другими словами, volatile сообщает компилятору, что доступ к этой переменной должен соответствовать операции чтения / записи в физической памяти.
Например, вот как InterlockedIncrement объявляется в Win32 API:
LONG __cdecl InterlockedIncrement(
__inout LONG volatile *Addend
);
Большое приложение, над которым я работал в начале 1990-х годов, содержало обработку исключений на основе C с использованием setjmp и longjmp. Ключевое слово volatile было необходимо для переменных, чьи значения должны были быть сохранены в блоке кода, который служил в качестве предложения "catch", чтобы эти переменные не сохранялись в регистрах и не уничтожались longjmp.
В стандарте C, одно из мест для использования volatile
с обработчиком сигнала. На самом деле, в стандарте C все, что вы можете безопасно сделать в обработчике сигналов, это изменить volatile sig_atomic_t
переменная или быстро выйти. Действительно, AFAIK, это единственное место в стандарте C, где использование volatile
требуется, чтобы избежать неопределенного поведения.
ISO / IEC 9899: 2011 §7.14.1.1
signal
функцияIf5 Если сигнал возникает не в результате вызова
abort
или жеraise
функция, поведение не определено, если обработчик сигнала ссылается на любой объект со статической или продолжительностью хранения потока, который не является атомарным объектом без блокировки, кроме как путем присвоения значения объекту, объявленному какvolatile sig_atomic_t
или обработчик сигнала вызывает любую функцию в стандартной библиотеке, кромеabort
функция,_Exit
функция,quick_exit
функция илиsignal
функция с первым аргументом, равным номеру сигнала, соответствующему сигналу, вызвавшему вызов обработчика. Кроме того, если такой вызовsignal
функция приводит к возврату SIG_ERR, значениеerrno
является неопределенным.252)252) Если какой-либо сигнал генерируется асинхронным обработчиком сигнала, поведение не определено.
Это означает, что в стандарте C вы можете написать:
static volatile sig_atomic_t sig_num = 0;
static void sig_handler(int signum)
{
signal(signum, sig_handler);
sig_num = signum;
}
и не намного больше.
POSIX гораздо более снисходительно относится к тому, что вы можете сделать в обработчике сигналов, но есть все еще ограничения (и одно из ограничений заключается в том, что библиотека Standard I/O - printf()
и др. - не может использоваться безопасно).
Я использовал его в отладочных сборках, когда компилятор настаивает на оптимизации переменной, которую я хочу видеть при выполнении кода.
Разрабатывая для встроенного, у меня есть цикл, который проверяет переменную, которая может быть изменена в обработчике прерываний. Без "volatile" цикл превращается в петлю - насколько может сказать компилятор, переменная никогда не меняется, поэтому она оптимизирует проверку.
То же самое относится и к переменной, которая может быть изменена в другом потоке в более традиционной среде, но там мы часто выполняем вызовы синхронизации, поэтому компилятор не так свободен с оптимизацией.
Помимо использования по назначению, 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
доступ к памяти.
- Вы должны использовать его для реализации спин-блокировок, а также некоторых (всех?) структур данных без блокировки
- используйте его с атомарными операциями / инструкциями
- помог мне однажды преодолеть ошибку компилятора (неправильно сгенерированный код во время оптимизации)
В других ответах уже упоминается об избежании некоторой оптимизации, чтобы:
- использовать регистры с отображением в память (или "MMIO")
- писать драйверы устройств
- облегчить отладку программ
- сделать вычисления с плавающей запятой более детерминированными
Изменчивость важна всякий раз, когда вам нужно, чтобы значение выглядело извне и было непредсказуемым и избегало оптимизации компилятора на основе известного значения, и когда результат фактически не используется, но вам нужно его вычислить, или он используется, но вы хотите вычислить его несколько раз для эталонного теста, и вам нужно, чтобы вычисления начинались и заканчивались в точных точках.
Неустойчивое чтение похоже на операцию ввода (например, scanf
или использование cin
): кажется, что значение приходит извне программы, поэтому любое вычисление, зависящее от значения, должно начинаться после него.
Неустойчивая запись похожа на операцию вывода (например, printf
или использование cout
): кажется, что значение передается вне программы, поэтому, если значение зависит от вычисления, его нужно завершить раньше.
Таким образом, пару энергозависимых операций чтения / записи можно использовать для укрощения тестов и повышения значимости измерения времени.
Без volatile ваши вычисления могут быть запущены компилятором раньше, поскольку ничто не помешает переупорядочить вычисления с помощью таких функций, как измерение времени.
volatile
Ключевое слово предназначено для предотвращения применения компилятором каких-либо оптимизаций к объектам, которые могут изменяться способами, которые не могут быть определены компилятором.
Объекты, объявленные как volatile
не включены в оптимизацию, потому что их значения могут быть изменены кодом вне области текущего кода в любое время. Система всегда читает текущее значение volatile
объект из ячейки памяти вместо сохранения его значения во временном регистре в той точке, в которой он запрашивается, даже если предыдущая инструкция запрашивала значение из того же объекта.
Рассмотрим следующие случаи
1) Глобальные переменные, измененные подпрограммой обработки прерываний вне области действия.
2) Глобальные переменные в многопоточном приложении.
Если мы не используем volatile квалификатор, могут возникнуть следующие проблемы
1) Код может не работать должным образом при включенной оптимизации.
2) Код может не работать должным образом, когда прерывания включены и используются.
Изменчивый: лучший друг программиста
https://en.wikipedia.org/wiki/Volatile_(computer_programming)
Все ответы отличные. Но в довершение всего я хотел бы привести пример.
Ниже представлена небольшая программа 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 $40, %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 $8, x(%rip) #, x
# assembly.cpp:10: printf("x is 8\n");
call _ZL6printfPKcz.constprop.0 #
# assembly.cpp:18: }
xorl %eax, %eax #
movl $5, x(%rip) #, x
addq $40, %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
цикл был полностью удален из-за оптимизации, потому что компилятор увидел в нем бесполезный код и, таким образом, напрямую назначил 5
к x
(увидеть movl $5, x(%rip)
).
Проблема возникает, когда что, если внешний процесс / оборудование изменит значение x
где-то между x = 8;
а также if(x == 8)
. Мы ожидалиelse
блок для работы, но, к сожалению, компилятор вырезал эту часть.
Теперь, чтобы решить эту проблему, в assembly.cpp
, давайте изменим int x;
к volatile int x;
и быстро увидеть сгенерированный код сборки:
main:
.LFB1594:
subq $104, %rsp #,
.seh_stackalloc 104
.seh_endprologue
# assembly.cpp:5: int main(){
call __main #
# assembly.cpp:7: x = 8;
movl $8, x(%rip) #, x
# assembly.cpp:9: if(x == 8)
movl x(%rip), %eax # x, x.1_1
# assembly.cpp:9: if(x == 8)
cmpl $8, %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 $1000, x(%rip) #, x
# assembly.cpp:15: while(x > 5)
movl x(%rip), %eax # x, x.3_15
cmpl $5, %eax #, x.3_15
jle .L8 #,
.p2align 4,,10
.L9:
# assembly.cpp:16: x--;
movl x(%rip), %eax # x, x.4_3
subl $1, %eax #, _4
movl %eax, x(%rip) # _4, x
# assembly.cpp:15: while(x > 5)
movl x(%rip), %eax # x, x.3_2
cmpl $5, %eax #, x.3_2
jg .L9 #,
.L8:
# assembly.cpp:18: }
xorl %eax, %eax #
addq $104, %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 , которые могут помочь понять значение переменных:
переменные (не имеющие аналога в таких языках, как
C#
а такжеJava
) всегда выходят за рамки этой и любой другой статьи о модели памяти и синхронизации. Это потому, что переменные вообще не связаны с потоками или общением и не взаимодействуют с этими вещами. Скорее, переменную следует рассматривать как портал в другую вселенную за пределами языка — область памяти, которая по определению не подчиняется модели памяти языка, поскольку доступ к этой области памяти осуществляется аппаратно (например, запись в нее осуществляется дочерней картой). более одного адреса или иначе является «странным» и выходит за рамки языка. ТакC++
volatile
переменные повсеместно являются исключением из всех рекомендаций по синхронизации, потому что они всегда по своей сути «красочны» и не синхронизируются с использованием обычных инструментов (мьютексы, атомарные и т. д.) и, в более общем случае, существуют за пределами всех норм языка и компилятора, включая то, что они обычно не могут быть оптимизированы компилятором (поскольку компилятору не разрешено знать их семантику;volatile int vi;
может вести себя не так, как обычноint
, и вы даже не можете предположить, что такой код, какvi = 5; int read_back = vi;
гарантированно приведет кread_back == 5
или такой код, какint i = vi; int j = vi;
который читает vi дважды, приведет кi == j
что не будет правдой, еслиvi
например, аппаратный счетчик).
Помимо того, что ключевое слово volatile используется для указания компилятору не оптимизировать доступ к некоторой переменной (которая может быть изменена потоком или подпрограммой прерывания), оно также может быть использовано для удаления некоторых ошибок компилятора - ДА, это может быть ---
Например, я работал над встроенной платформой, где компилятор делал неверные предположения относительно значения переменной. Если код не был оптимизирован, программа будет работать нормально. С оптимизацией (которая была действительно необходима, потому что это была критическая процедура) код не работал бы правильно. Единственное решение (хотя и не очень правильное) состояло в том, чтобы объявить переменную "неисправный" как volatile.
Кажется, ваша программа работает даже без 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
ключевое слово, даже если ваша программа, кажется, работает без. Это делает намерение более ясным и надежным в отношении будущих изменений.
В первые дни C компиляторы интерпретировали все действия, которые читают и записывают значения l, как операции с памятью, которые должны выполняться в той же последовательности, в которой операции чтения и записи появились в коде. Во многих случаях эффективность могла бы быть значительно улучшена, если бы компиляторам была предоставлена определенная свобода для переупорядочения и консолидации операций, но с этим была проблема. Даже операции часто указывались в определенном порядке только потому, что было необходимо указывать их в каком-то порядке, и, таким образом, программист выбрал одну из многих одинаково хороших альтернатив, что не всегда имело место. Иногда было бы важно, чтобы определенные операции происходили в определенной последовательности.
Какие именно детали последовательности важны, будет зависеть от целевой платформы и области применения. Вместо того, чтобы предоставлять особенно подробный контроль, Стандарт выбрал простую модель: если последовательность обращений выполняется с l-значениями, которые не квалифицированы volatile
Компилятор может переупорядочивать и объединять их по своему усмотрению. Если действие сделано с volatile
-квалифицированная lvalue, качественная реализация должна предлагать любые дополнительные гарантии упорядочения, если код ориентирован на предполагаемую платформу и область приложения, без необходимости использования нестандартного синтаксиса.
К сожалению, вместо того, чтобы определить, какие гарантии понадобятся программистам, многие компиляторы предпочли предложить минимальные гарантии, предусмотренные стандартом. Это делает volatile
гораздо менее полезный, чем должен быть. Например, в gcc или clang программист, которому нужно реализовать базовый "мьютекс передачи" [тот, в котором задача, которая получила и освободил мьютекс, не будет делать это до тех пор, пока другая задача не выполнит это], должен выполнить один из четырех вещей:
Поместите получение и освобождение мьютекса в функцию, которую компилятор не может встроить и к которой он не может применить Оптимизацию всей программы.
Определите все объекты, охраняемые мьютексом, как
volatile
- то, что не должно быть необходимым, если все обращения происходят после получения мьютекса и перед его освобождением.Используйте уровень оптимизации 0, чтобы заставить компилятор генерировать код, как если бы все объекты не были квалифицированы
register
являютсяvolatile
,Используйте специфичные для gcc директивы.
Напротив, при использовании более качественного компилятора, который больше подходит для системного программирования, такого как icc, можно было бы выбрать другой вариант:
- Убедитесь, что
volatile
-качественная запись выполняется везде, где требуется приобретение или выпуск.
Приобретение базового "мьютекса передачи" требует volatile
читать (чтобы увидеть, готов ли он) и не должен требовать volatile
напишите также (другая сторона не будет пытаться повторно получить это, пока это не будет возвращено), но необходимость выполнить бессмысленное volatile
Писать по-прежнему лучше, чем любой из вариантов, доступных в gcc или clang.
Я должен напомнить вам, что в функции-обработчике сигналов вы можете использовать глобальную переменную (например, пометить ее как exit = true) и объявить ее как 'volatile'.