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

<p>что делает ключевое слово <code>volatile</code>? в c++ какую проблему это решает?</p><p>в моем случае я никогда не нуждался в этом сознательно.</p>

<p><code>volatile</code> необходим, если вы читаете из какой-то области памяти, скажем, совершенно отдельного процесса/устройства/того, что может записать.</p><p>раньше я работал с двухпортовым озу в многопроцессорной системе на прямом c. мы использовали 16-битное значение с аппаратным управлением как семафор, чтобы знать, когда другой парень закончил. по сути, мы сделали это:</p><pre><code>void waitforsemaphore() { volatile uint16\_t* semptr = well\_known\_sem\_addr;/*well known address to my semaphore*/ while ((*semptr) != is\_ok\_for\_me\_to\_proceed); } </code></pre><p>без <code>volatile</code> оптимизатор считает цикл бесполезным (парень никогда не устанавливает значение! он с ума сошел, избавьтесь от этого кода!), и мой код продолжит работу, не получив семафор, что впоследствии вызовет проблемы.</p>

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

<p>некоторые процессоры имеют регистры с плавающей запятой, которые имеют точность более 64 бит (например, 32-битный x86 без sse, см. комментарий питера). таким образом, если вы выполняете несколько операций над числами с двойной точностью, вы на самом деле получаете ответ с более высокой точностью, чем если бы вы усекали каждый промежуточный результат до 64 бит.</p><p>обычно это замечательно, но это означает, что в зависимости от того, как компилятор назначил регистры и выполнил оптимизацию, вы получите разные результаты для одинаковых операций с одинаковыми входными данными. если вам нужна согласованность, то вы можете заставить каждую операцию вернуться в память, используя ключевое слово volatile.</p><p>это также полезно для некоторых алгоритмов, которые не имеют алгебраического смысла, но уменьшают ошибку с плавающей запятой, например, суммирование по кахану. алгебраически это не nop, поэтому он часто будет неправильно оптимизирован, если некоторые промежуточные переменные не являются изменчивыми.</p>

<p>из <em> статьи дэна сакса "летучий как обещание" </em>:</p><blockquote> <p>(...) изменчивый объект - это объект, значение которого может меняться самопроизвольно. то есть, когда вы объявляете объект энергозависимым, вы сообщаете компилятору, что объект может изменить состояние, даже если никакие операторы в программе не изменяют его ".</p> </blockquote><p>вот ссылки на три его статьи относительно ключевого слова <code>volatile</code>:</p><ul> <li> <a href="https://www.embedded.com/electronics-blogs/programming-pointers/4025583/use-volatile-judiciously">используйте изменчиво разумно</a> </li> <li> <a href="https://www.embedded.com/electronics-blogs/programming-pointers/4025609/place-volatile-accurately">точно изменяемое место</a> </li> <li> <a href="https://www.embedded.com/electronics-blogs/programming-pointers/4025624/volatile-as-a-promise">изменчиво как обещание</a> </li> </ul>

<p>вы должны использовать volatile при реализации структур данных без блокировки. в противном случае компилятор может оптимизировать доступ к переменной, что изменит семантику.</p><p>иными словами, volatile сообщает компилятору, что доступ к этой переменной должен соответствовать операции чтения/записи в физической памяти.</p><p>например, вот как interlockedincrement объявляется в win32 api:</p><pre><code>long \_\_cdecl interlockedincrement( \_\_inout long volatile *addend ); </code></pre>

<p>большое приложение, над которым я работал в начале 1990-х, содержало обработку исключений на основе c с использованием setjmp и longjmp. ключевое слово volatile было необходимо для переменных, чьи значения необходимо было сохранить в блоке кода, который служил в качестве предложения "catch", чтобы эти переменные не сохранялись в регистрах и не уничтожались longjmp.</p>

<p>в стандарте c одно из мест, где можно использовать <code>volatile</code>, - это обработчик сигнала. на самом деле, в стандарте c все, что вы можете безопасно сделать в обработчике сигналов, это изменить переменную <code>volatile sig\_atomic\_t</code> или быстро выйти. действительно, afaik, это единственное место в стандарте c, где использование <code>volatile</code> необходимо, чтобы избежать неопределенного поведения.</p><blockquote> <h3>iso/iec 9899: 2011 §7.14.1.1 функция <code>signal</code></h3> <p>if5 если сигнал возникает не в результате вызова функции <code>abort</code> или <code>raise</code>, поведение не определено, если обработчик сигнала ссылается на какой-либо объект со статическим или потоковым сроком хранения, который не является атомарным объектом без блокировки, кроме как путем назначения значение объекта, объявленного как <code>volatile sig\_atomic\_t</code>, или обработчик сигнала вызывает любую функцию в стандартной библиотеке, кроме функции <code>abort</code>, функции <code>\_exit</code>, функции <code>quick\_exit</code> или <code>signal</code>, причем первый аргумент равен номеру сигнала, соответствующему сигнал, вызвавший вызов обработчика. кроме того, если такой вызов функции <code>signal</code> приводит к возвращению sig\_err, значение <code>errno</code> является неопределенным.<sup>252)</sup></p> <p><sup>252)</sup> если какой-либо сигнал генерируется асинхронным обработчиком сигнала, поведение не определено.</p> </blockquote><p>это означает, что в стандарте c вы можете написать:</p><pre><code>static volatile sig\_atomic\_t sig\_num = 0; static void sig\_handler(int signum) { signal(signum, sig\_handler); sig\_num = signum; } </code></pre><p>и не намного больше.</p><p>posix гораздо более снисходительно относится к тому, что вы можете делать в обработчике сигналов, но все еще существуют ограничения (и одно из ограничений заключается в том, что стандартную библиотеку ввода-вывода - <code>printf()</code> и др. - нельзя использовать безопасно).</p>

<p>помимо использования по назначению, volatile используется в (шаблонном) метапрограммировании. его можно использовать для предотвращения случайной перегрузки, поскольку атрибут volatile (например, const) участвует в разрешении перегрузки.</p><pre><code>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); } }; </code></pre><p>это законно; обе перегрузки потенциально могут быть вызваны и почти одинаковы. приведение в перегрузке <code>volatile</code> допустимо, так как мы знаем, что бар в любом случае не пропустит энергонезависимое <code>t</code>. версия <code>volatile</code> строго хуже, однако, поэтому никогда не выбирается в разрешении перегрузки, если доступно энергонезависимое <code>f</code>.</p><p>обратите внимание, что код никогда не зависит от доступа к памяти <code>volatile</code>.</p>

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

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

<ol> <li>вы должны использовать его для реализации спин-блокировок, а также некоторых (всех?) структур данных без блокировки</li> <li>используйте его с атомарными операциями/инструкциями</li> <li>помог мне однажды преодолеть ошибку компилятора (неправильно сгенерированный код во время оптимизации)</li> </ol>

<p>ключевое слово <code>volatile</code> предназначено для предотвращения применения компилятором каких-либо оптимизаций к объектам, которые могут изменяться способами, которые не могут быть определены компилятором.</p><p>объекты, объявленные как <code>volatile</code>, не включаются в оптимизацию, поскольку их значения могут быть изменены кодом за пределами области действия текущего кода в любое время. система всегда считывает текущее значение объекта <code>volatile</code> из памяти, а не сохраняет его значение во временном регистре в той точке, в которой оно запрашивается, даже если предыдущая инструкция запрашивала значение из того же объекта.</p><p><strong> рассмотрим следующие случаи </strong></p><p>1) глобальные переменные, измененные подпрограммой обработки прерываний вне области действия.</p><p>2) глобальные переменные в многопоточном приложении.</p><p><strong> если мы не используем volatile квалификатор, могут возникнуть следующие проблемы </strong></p><p>1) код может не работать должным образом при включенной оптимизации.</p><p>2) код может работать не так, как ожидалось, когда прерывания включены и используются.</p><p> <a href="http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766">изменчивый: лучший друг программиста</a> </p><p> <a href="https://en.wikipedia.org/wiki/volatile\_(computer\_programming)">https://en.wikipedia.org/wiki/volatile\_ (computer\_programming)</a> </p>

<p>ваша программа работает даже без ключевого слова <code>volatile</code>? возможно, это причина</p><p>как упоминалось ранее, ключевое слово <code>volatile</code> помогает в таких случаях, как</p><pre><code>volatile int* p = ...; // point to some memory while( *p!=0 ) {} // loop until the memory becomes zero </code></pre><p>но, похоже, эффект почти не проявляется после вызова внешней или не встроенной функции. например.:</p><pre><code>while( *p!=0 ) { g(); } </code></pre><p>затем с или без <code>volatile</code> генерируется почти одинаковый результат.</p><p>пока g() может быть полностью встроенным, компилятор может видеть все, что происходит, и поэтому может оптимизировать. но когда программа обращается к месту, где компилятор не может видеть, что происходит, компилятору небезопасно делать какие-либо предположения. следовательно, компилятор будет генерировать код, который всегда читает непосредственно из памяти.</p><p>но остерегайтесь того дня, когда ваша функция g() станет встроенной (либо из-за явных изменений, либо из-за хитрости компилятора/компоновщика), тогда ваш код может сломаться, если вы забудете ключевое слово <code>volatile</code>!</p><p>поэтому я рекомендую добавить ключевое слово <code>volatile</code>, даже если ваша программа работает без него. это делает намерение более ясным и надежным в отношении будущих изменений.</p>

<p>в первые дни c компиляторы интерпретировали все действия, которые читают и записывают значения l, как операции с памятью, которые должны выполняться в той же последовательности, в которой операции чтения и записи появились в коде. во многих случаях эффективность могла бы быть значительно улучшена, если бы компиляторам была предоставлена определенная свобода для переупорядочения и консолидации операций, но с этим была проблема. даже операции часто указывались в определенном порядке только потому, что необходимо было указывать их в <em>некоторых</em> порядке, и, таким образом, программист выбрал одну из многих одинаково хороших альтернатив, что было не всегда так. иногда было бы важно, чтобы определенные операции происходили в определенной последовательности.</p><p>какие именно детали последовательности важны, будет зависеть от целевой платформы и области применения. вместо того, чтобы предоставлять особо подробный контроль, стандарт выбрал простую модель: если последовательность обращений выполняется с l-значениями, которые не определены <code>volatile</code>, компилятор может переупорядочить и объединить их, как он считает нужным. если действие выполняется с <code>volatile</code>- квалифицированным lvalue, качественная реализация должна предлагать любые дополнительные гарантии упорядочения, которые могут потребоваться при коде, нацеленном на его предполагаемую платформу и область приложения, без необходимости использования нестандартного синтаксиса.</p><p>к сожалению, вместо того, чтобы определить, какие гарантии понадобятся программистам, многие компиляторы предпочли вместо этого предложить минимальные гарантии, предусмотренные стандартом. это делает <code>volatile</code> гораздо менее полезным, чем должно быть. например, в gcc или clang программист, которому нужно реализовать базовый "мьютекс передачи" [тот, в котором задача, которая получила и освободил мьютекс, не будет делать это до тех пор, пока другая задача не выполнит это], должен выполнить один из четырех вещей:</p><ol> <li> <p>поместите получение и освобождение мьютекса в функцию, которую компилятор не может встроить и к которой он не может применить оптимизацию всей программы.</p> </li> <li> <p>определите все объекты, защищенные мьютексом, как <code>volatile</code>-- что-то, что не должно быть необходимым, если все обращения происходят после получения мьютекса и перед его освобождением.</p> </li> <li> <p>используйте уровень оптимизации 0, чтобы заставить компилятор генерировать код, как если бы все объекты, которые не определены <code>register</code>, были <code>volatile</code>.</p> </li> <li> <p>используйте специфичные для gcc директивы.</p> </li> </ol><p>напротив, при использовании более качественного компилятора, который больше подходит для системного программирования, такого как icc, можно было бы выбрать другой вариант:</p><ol start="5"> <li>убедитесь, что <code>volatile</code>- квалифицированная запись выполняется везде, где требуется приобретение или выпуск.</li> </ol><p>для получения базового "мьютекса передачи обслуживания" требуется чтение <code>volatile</code> (чтобы узнать, готово ли оно), и не требуется также запись <code>volatile</code> (другая сторона не будет пытаться повторно получить ее, пока она не будет возвращена), но необходимость выполнять бессмысленную запись <code>volatile</code> все же лучше, чем любая из опций, доступных в gcc или clang.</p>

<p>помимо того, что ключевое слово volatile используется для указания компилятору не оптимизировать доступ к некоторой переменной (которая может быть изменена потоком или подпрограммой прерывания), оно также может быть <strong>используется для удаления некоторых ошибок компилятора</strong> - <em> да, это может быть </em> ---.</p><p>например, я работал над встроенной платформой, где компилятор делал некоторые неверные предположения относительно значения переменной. если код не был оптимизирован, программа будет работать нормально. с оптимизацией (которая была действительно необходима, потому что это была критическая процедура) код не работал бы правильно. единственное решение (хотя и не очень правильное) состояло в том, чтобы объявить переменную "неисправный" как volatile.</p>

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