Почему летучие существуют?

Что это 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 доступ к памяти.

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

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

  • использовать регистры с отображением в память (или "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 программист, которому нужно реализовать базовый "мьютекс передачи" [тот, в котором задача, которая получила и освободил мьютекс, не будет делать это до тех пор, пока другая задача не выполнит это], должен выполнить один из четырех вещей:

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

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

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

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

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

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

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

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

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