Что делает ключевое слово volatile? В C++ какую проблему он решает?
В моем случае я сознательно никогда не нуждался в этом.
Вот интересное обсуждение volatile в отношении паттерна Singleton: aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
Это хороший ресурс с примером того, как можно эффективно использовать volatile, сложенный в довольно непрофессиональных терминах. Ссылка: Publications.gbdirect.co.uk/c_book/chapter8/…





volatile необходим при разработке встроенных систем или драйверов устройств, когда вам нужно читать или записывать аппаратное устройство с отображением памяти. Содержимое конкретного регистра устройства может измениться в любое время, поэтому вам понадобится ключевое слово volatile, чтобы гарантировать, что такой доступ не будет оптимизирован компилятором.
Это справедливо не только для встроенных систем, но и для разработки всех драйверов устройств.
Единственный раз, когда мне это понадобилось на 8-битной шине ISA, где вы дважды читали один и тот же адрес - у компилятора была ошибка, и она игнорировалась (ранний Zortech C++)
Volatile очень редко бывает достаточно для управления внешними устройствами. Его семантика неверна для современного MMIO: приходится делать слишком много объектов изменчивыми, а это вредит оптимизации. Но современный MMIO ведет себя как обычная память до тех пор, пока не будет установлен флаг, поэтому volatile не требуется. Многие драйверы никогда не используют volatile.
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 Нет, это не так. На практике то, что вы предлагаете, скорее всего и произойдет. Но теоретически можно было бы получить компилятор, который оптимизирует доступ к значениям, потому что он решил, что ни одно из этих значений никогда не изменяется. И если бы вы имели в виду volatile для применения к значению, а не к указателю, вы бы ошиблись. Опять же, маловероятно, но лучше ошибиться, поступая правильно, чем воспользоваться поведением, которое сегодня работает.
Как бы это не быть гонкой на *semPtr?
@Doug T. Лучшее объяснение - это
@iheanyi "было решено, что ни одно из этих значений никогда не изменялось" Решили неправильно. Вы только что описали компилятор с ошибками.
@BaummitAugen: Этот ответ (и, очевидно, код) предшествовал C++ 11. Без std::atomic единственным вариантом было взломать все самостоятельно с помощью встроенных asm или вызовов библиотечных функций для устранения любых необходимых препятствий. Вы можете обернуть энергозависимый доступ функцией или макросом read_once(), как это делает ядро Linux, но это все равно будет сводиться к этому, чтобы получить asm, который вы хотите, в любой разумной реализации, где выровненный volatile uint16_t может быть прочитан / записан атомарно. (т.е. на большинстве конкретных платформ фактическое поведение, которое вы получаете от этого, четко определено.)
@PeterCordes Учитывая, что C++ 11 существует уже некоторое время, этот ответ следует обновить, чтобы хотя бы явно указать на его устаревшую природу, не так ли? В нынешнем виде я считаю этот ответ несколько вводящим в заблуждение (конечно, не намеренно, но времена изменились).
@BaummitAugen: да, наверное. Хотя, если бы это было аппаратное устройство, изменяющее известную ячейку памяти, а не другой поток в той же программе, этот код все равно был бы на 100% подходящим. (Если только этот семафор не контролирует доступ к какой-либо другой памяти, в этом случае вам нужно, чтобы atomic<uint16_t> или volatile atomic<uint16_t> выполняли загрузку-загрузку. В этом случае компилятор не может поднять нагрузку, потому что это сделает цикл бесконечным, поэтому volatile не здесь не нужно. Может и оптимизирует ли компилятор две атомные нагрузки?)
@curiousguy не ошиблись. Он сделал правильный вывод на основании предоставленной информации. Если вы не можете отметить что-то изменчивое, компилятор может предположить, что это не летучий. Это то, что делает компилятор при оптимизации кода. Если есть дополнительная информация, а именно, что указанные данные на самом деле изменчивы, ответственность за предоставление этой информации лежит на программисте. То, что вы утверждаете, используя компилятор с ошибками, на самом деле является просто плохим программированием.
@iheanyi Нет, это компилятор решил игнорировать ключевое слово volatile, которое присутствует в коде.
@curiousguy нет, только потому, что ключевое слово volatile появляется один раз, не означает, что все внезапно становится нестабильным. Я привел сценарий, в котором компилятор поступает правильно и достигает результата, который противоречит ошибочным ожиданиям программиста. Точно так же, как "самый неприятный синтаксический анализ" не является признаком ошибки компилятора, и здесь тоже.
@iheanyi Можете ли вы показать псевдокод, который учитывает изменчивость, что приведет к нежелательному результату?
@curiousguy зацените этот вопрос - этого достаточно?
Первое предложение этого ответа в корне неверно в очень тонком, но важном смысле. Он утверждает, что volatile является нужный при некоторых условиях. Но это не так. Например, рассмотрим платформу, которая предоставляет собственный тип, скажем atomic_int, который документирован как пригодный для чтения из памяти, в которую могут записывать полностью отдельные устройства. Конечно, на этой платформе volatile не понадобится. Поскольку это очень часто бывает, на практике volatile требуется очень редко, даже когда вам нужно такое поведение.
@DavidSchwartz - не могли бы вы уточнить этот пример? Что бы сделал atomic_int? В моем мире мы в основном используем «атомарные» для описания серии операций, которые необходимо выполнять последовательно, без прерывания другими операциями. [продолжение ...]
[продолжение ...] Например, при обновлении битового поля некоторых регистров отображаемого в память устройства для этого адреса используется схема чтения-изменения-записи. Важно защитить доступ от прерывания и воздействия 2-го потока или обработчика IRQ. Таким образом, доступ ограничен «атомарными» ключевыми словами для отключения IRQ. Я не уверен, что будет делать ваш предлагаемый тип и как это связано с предотвращением оптимизации при последовательном доступе к одному и тому же изменчивому ресурсу.
@ysap Предположим, что платформа предлагает тип под названием atomic_int, который, как указано в документации, ведет себя точно так же, как volatile int. В этом случае вам не нужно использовать volatile на этой платформе, поскольку вы можете использовать atomic_int. Но в этом ответе говорится, что необходим volatile. Это не правильно. Большинство платформ предлагают такие вещи, как atomic_int, с гарантированной семантикой без необходимости использования volatile.
@DavidSchwartz - так, если я вас правильно понял, вы предлагаете своего рода псевдоним для volatile int, как если бы был typedef volatile int atomic_int, а затем говорите, что использование volatile не обязательно? Если это так, то тот же аргумент можно использовать, чтобы сказать, что если система предоставляет тип с именем whole, который ведет себя как int, тогда использование int не обязательно ???! Кроме того, я думаю, что в моем мире это не будет подходящим использованием слова atomic, как описано выше. Или я полностью упустил вашу точку зрения?
@ysap Нет, ты понял. Неверно говорить, что volatile необходим, потому что другие вещи могут предоставить гарантии, и на каждой реалистичной платформе на самом деле есть другие вещи, которые предоставляют эти гарантии, а volatile фактически не используется. Очень ошибочно говорить, что что-то «необходимо» (на самом деле, совершенно неверно), когда это даже не самое распространенное решение.
Лучше использовать библиотеку, встроенные функции компилятора или встроенный ассемблерный код. Неустойчивый - ненадежный.
1 и 2 оба используют атомарные операции, но volatile не обеспечивает атомарной семантики, а реализации atomic для конкретной платформы заменят необходимость использования volatile, поэтому для 1 и 2 я не согласен, для них volatile НЕ нужен.
Кто что-нибудь говорит о volatile, обеспечивающем атомарную семантику? Я сказал, что вам нужно ИСПОЛЬЗОВАТЬ volatile С атомарными операциями, и если вы не думаете, что это правда, посмотрите объявления заблокированных операций Win32 API (этот парень также объяснил это в своем ответе)
При разработке для встраиваемых систем у меня есть цикл, который проверяет переменную, которая может быть изменена в обработчике прерывания. Без «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 как изменчивый, чтобы можно было вычислить правильную дельту.
+1, действительно, по моему опыту, был случай, когда вычисления с плавающей запятой давали разные результаты в отладке и выпуске, поэтому модульные тесты, написанные для одной конфигурации, терпели неудачу для другой. Мы решили эту проблему, объявив одну переменную с плавающей запятой как volatile double, а не просто double, чтобы гарантировать, что она усечена с точности FPU до точности 64-битной (RAM) перед продолжением дальнейших вычислений. Результаты были существенно другими из-за дальнейшего преувеличения ошибки с плавающей запятой.
Ваше определение «современного» немного неверно. Это затронуло только 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/….
Но все же хороший ответ, для устаревшего генератора кода x87 вы можете использовать volatile для принудительного округления в нескольких конкретных местах, не теряя при этом преимущества повсюду.
@PeterCordes Тот факт, что пользовательские регистры имеют точность, равную двойной точности, не означает, что внутренние регистры, используемые для промежуточных результатов (которые никогда не называются в asm), не имеют большей точности /
@curiousguy: на нормальных ISA это означает делает. Ваша идея привела бы к ЦП, который дает разные математические результаты в зависимости от того, где произошло прерывание + переключение контекста (и принудительное округление до ширины архитектурного регистра путем сохранения / восстановления регистров FP). Всем нужен один и тот же машинный код с одинаковыми входными данными, чтобы каждый раз выдавать один и тот же точный побитовый результат.
@PeterCordes FPU instr прерываемы?
@curiousguy: внутренние буферы, используемые во время оценки одной инструкции, являются чисто реализацией, и volatile на них не влияет. Если это микрокодированная инструкция, такая как x87 fsin, теоретически вы можете сохранить дополнительную точность между упс. Но нет, он не будет возобновлен, если его можно прервать, он будет прерван.
Я думал, вы имели в виду что-то вроде MIPS f0, внутренне сохраняющего более 64-битную точность в цепочке инструкций FP, поэтому volatile для принудительного сохранения / перезагрузки будет иметь эффект. Большинство FPU (кроме 387) предоставляют только операции IEEE Basic + -/ sqrt, которые необходимы для получения правильно округленных результатов (ошибка <= 0,5ulp). Таким образом, если вы не сохраните дополнительную внутреннюю точность между инструкциями, результаты будут полностью определены. Интересный факт: AMD * делает сохраняют дополнительные внутренние данные между инструкциями FP, но не дополнительную точность; наверное что-то вроде распаковки экспоненты / мантиссы.
@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: О чём ты говоришь? Раньше вы говорили о том, что архитектурные регистры являются 64-битными, а не 80-битными. Ваш последний комментарий имеет смысл, например, 32-разрядный x86 с использованием x87, где компилятор использует 80-разрядный x87 для временных файлов, таких как y*z. Как и FP_CONTRACT, gcc по умолчанию оптимизирует между операторами, а не только внутри выражений с округлением до фактического double, принудительно назначенным и приведенным, хотя FLT_EVAL_METHOD = 2 говорит, что это должно быть. Это будет медленно. Но опять же проблема только с> 64-битными регистрами.
Позвольте нам продолжить обсуждение в чате.
Но в любом случае математика с плавающей запятой всегда немного неточна, поэтому я не понимаю, почему это имеет значение. В любом случае, не могут ли одни и те же операции давать одинаковые точные результаты?
Или я путаю неточное с несогласованным?
Помимо того факта, что ключевое слово volatile используется для указания компилятору не оптимизировать доступ к некоторой переменной (которая может быть изменена потоком или подпрограммой прерывания), это также может быть используется для удаления некоторых ошибок компилятора - ДА это может быть ---.
Например, я работал на встроенной платформе, где компилятор делал некоторые неправильные предположения относительно значения переменной. Если код не был оптимизирован, программа работала нормально. При оптимизации (которая действительно была необходима, потому что это была критическая процедура) код не работал бы правильно. Единственным решением (хотя и не очень правильным) было объявить «неисправную» переменную изменчивой.
Это ошибочное предположение о том, что компилятор не оптимизирует доступ к летучим файлам. Стандарт ничего не знает об оптимизации. От компилятора требуется соблюдение требований стандарта, но он может выполнять любые оптимизации, которые не мешают нормальному поведению.
По моему опыту, 99,9% всех "ошибок" оптимизации в gcc arm - это ошибки со стороны программиста. Не знаю, относится ли это к этому ответу. Просто напыщенная речь на общую тему
@Terminus "Это ошибочное предположение, что компилятор не оптимизирует доступ к летучим файлам." Источник?
Большое приложение, над которым я работал в начале 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.
Не могли бы вы пояснить это на примере? Это действительно помогло бы мне лучше понять. Спасибо!
«Бросок в летучей перегрузке» Приведение - это явное преобразование. Это конструкция SYNTAX. Многие люди вводят в заблуждение (даже стандартные авторы).
Вы ДОЛЖНЫ использовать volatile при реализации структур данных без блокировки. В противном случае компилятор может оптимизировать доступ к переменной, что изменит семантику.
Другими словами, volatile сообщает компилятору, что доступ к этой переменной должен соответствовать операции чтения / записи физической памяти.
Например, вот как InterlockedIncrement объявляется в Win32 API:
LONG __cdecl InterlockedIncrement(
__inout LONG volatile *Addend
);
Вам абсолютно НЕ нужно объявлять переменную volatile, чтобы иметь возможность использовать InterlockedIncrement.
Этот ответ устарел теперь, когда C++ 11 предоставляет std::atomic<LONG>, поэтому вы можете писать код без блокировки более безопасно, без проблем с оптимизацией чистых загрузок / чистых хранилищ, или переупорядочением, или чем-то еще.
В стандарте C одно из мест использования volatile - это обработчик сигналов. Фактически, в стандарте C все, что вы можете безопасно сделать в обработчике сигналов, - это изменить переменную volatile sig_atomic_t или быстро выйти. Действительно, AFAIK, это единственное место в стандарте C, в котором требуется использование volatile, чтобы избежать неопределенного поведения.
ISO/IEC 9899:2011 §7.14.1.1 The
signalfunction¶5 If the signal occurs other than as the result of calling the
abortorraisefunction, 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 asvolatile sig_atomic_t, or the signal handler calls any function in the standard library other than theabortfunction, the_Exitfunction, thequick_exitfunction, or thesignalfunction 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 thesignalfunction results in a SIG_ERR return, the value oferrnois 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)
Размещенная вами ссылка сильно устарела и не отражает текущие передовые практики.
Я должен вам напомнить, что в функции обработчика сигналов, если вы хотите получить доступ / изменить глобальную переменную (например, пометить ее как 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, даже если ваша программа работает без него. Это делает намерение более ясным и надежным в отношении будущих изменений.
Обратите внимание, что функция может иметь встроенный код, в то же время генерируя ссылку (разрешенную во время компоновки) на функцию структуры; это будет случай частично встроенной рекурсивной функции. Семантика функции также может быть «встроена» компилятором, то есть компилятор предполагает, что побочные эффекты и результат находятся в пределах возможных побочных эффектов и результатов, возможных в соответствии с ее исходным кодом, но все же не встраивают его. Это основано на «действующем правиле одного определения», которое гласит, что все определения объекта должны быть фактически эквивалентными (если не полностью идентичными).
Избежать переносимости встраивания вызова (или «встраивания» его семантики) функцией, тело которой видно компилятору (даже во время компоновки с глобальной оптимизацией), возможно с помощью указателя функции, квалифицированного volatile: void (* volatile fun_ptr)() = fun; fun_ptr();
В первые дни C компиляторы интерпретировали все действия, которые считывают и записывают lvalue, как операции с памятью, которые должны выполняться в той же последовательности, что и операции чтения и записи, появляющиеся в коде. Эффективность можно было бы значительно повысить во многих случаях, если бы компиляторам была предоставлена определенная свобода переупорядочивания и консолидации операций, но с этим была проблема. Хотя операции часто указывались в определенном порядке просто потому, что их необходимо было указывать в порядке немного, и, таким образом, программист выбирал одну из многих не менее хороших альтернатив, это не всегда имело место. Иногда бывает важно, чтобы определенные операции выполнялись в определенной последовательности.
Какие именно детали секвенирования важны, зависит от целевой платформы и области применения. Вместо того, чтобы предоставлять особенно подробный контроль, Стандарт выбрал простую модель: если последовательность обращений выполняется с lvalue, не отвечающим требованиям volatile, компилятор может переупорядочить и объединить их по своему усмотрению. Если действие выполняется с lvalue, соответствующим volatile, качественная реализация должна предлагать любые дополнительные гарантии упорядочения, которые могут потребоваться для кода, нацеленного на ее предполагаемую платформу и поле приложения, не требуя от программистов использования нестандартного синтаксиса.
К сожалению, вместо того, чтобы определять, какие гарантии понадобятся программистам, многие компиляторы вместо этого предпочли предложить минимум гарантий, предусмотренных Стандартом. Это делает volatile гораздо менее полезным, чем следовало бы. В gcc или clang, например, программист, которому необходимо реализовать базовый «мьютекс передачи обслуживания» [такой, когда задача, которая получила и освободила мьютекс, не будет делать этого снова, пока это не сделает другая задача], должен выполнить из четырех вещей:
Поместите получение и освобождение мьютекса в функцию, которую компилятор не может встроить и к которой он не может применить оптимизацию всей программы.
Квалифицируйте все объекты, охраняемые мьютексом, как volatile - в этом нет необходимости, если все обращения происходят после получения мьютекса и до его освобождения.
Используйте уровень оптимизации 0, чтобы компилятор сгенерировал код, как если бы все объекты, не отвечающие требованиям register, были volatile.
Используйте специфичные для gcc директивы.
Напротив, при использовании более качественного компилятора, который больше подходит для системного программирования, такого как icc, у вас будет другой вариант:
volatile, выполняется везде, где требуется получение или выпуск.Для получения базового «мьютекса передачи» требуется чтение volatile (чтобы увидеть, готов ли он), и также не требуется запись volatile (другая сторона не будет пытаться повторно получить его, пока он не будет возвращен), но необходимость выполнять бессмысленную запись volatile все же лучше, чем любой из вариантов, доступных в gcc или clang.
В других ответах уже упоминается об избежании некоторой оптимизации, чтобы:
Изменчивость важна всякий раз, когда вам нужно, чтобы значение выглядело поступающим извне и было непредсказуемым и избегало оптимизаций компилятора на основе известного значения, и когда результат на самом деле не используется, но вам нужно его вычислить, или он используется, но вы хотите вычислить его несколько раз для эталонного теста, и вам нужно, чтобы вычисления начинались и заканчивались в точных точках.
Энергозависимое чтение похоже на операцию ввода (например, 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++volatilevariables (which have no analog in languages likeC#andJava) are always beyond the scope of this and any other article about the memory model and synchronization. That’s becauseC++volatilevariables aren’t about threads or communication at all and don’t interact with those things. Rather, aC++volatilevariable 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. SoC++volatilevariables 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; avolatile int vi;may not behave anything like a normalint, and you can’t even assume that code likevi = 5; int read_back = vi;is guaranteed to result inread_back == 5, or that code likeint i = vi; int j = vi;that reads vi twice will result ini == jwhich will not be true ifviis a hardware counter for example).
Существует интригующий метод, который заставляет ваш компилятор обнаруживать возможные состояния гонки, который сильно зависит от ключевого слова volatile, вы можете прочитать об этом в http://www.ddj.com/cpp/184403766.