для чего нужен модификатор volatile

volatile для «чайников»

Виктор Тимофеев, июнь, 2010 osa@pic24.ru

Вступление

Определение

( volatile в переводе с английского означает «нестабильный», «изменчивый»)

Что это значит? Известно, что одной из характеристик компиляторов, говорящих за их качество, является способность оптимизировать генерируемый объектный код. Для этого они объединяют повторяющиеся конструкции, сохраняют в регистрах общего назначения промежуточные результаты вычислений, выстраивают последовательность команд так, чтобы минимизировать долго выполняющиеся фрагменты кода (например, обращение через косвенную адресацию), и т.д. Выполняя такую оптимизацию, они немного преобразует наш код, подменяя его идентичным с точки зрения алгоритма, но более быстрым и/или компактным. Но такую подмену можно делать не всегда. Рассмотрим пример:

С точки зрения алгоритма устанавливаются два младших разряда в переменной a. Оптимизатор может сделать подмену такого кода одним оператором:

выиграв таким образом пару тактов и пару ячеек ROM. Но представим себе, что эти же действия мы выполняем не над какой-то абстрактной переменной, а над периферийным регистром:

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

Ошибки, связанные с volatile

Есть три основных типа ошибок, касающихся квалификатора volatile :

Источник

Ключевое слово «volatile» C/C++

Оптимизация кода компилятором

«компилятор не будет оптимизировать эту переменную» — что означает оптимизировать? Наверное очень много людей, когда только начинали программировать задавались этим вопросом, не так ли? Думаю лучше продемонстрировать все на примерах, нежели рассказывать термины, которые большинству останутся не понятными.

Ну давайте начнем, к примеру имеем простой массив(правда не с простым размером), в цикле с которым выполняем какое-либо действие:

Самая затратная операция в этом примере не присваивание ячейке массива какого-либо значения и не инкремент счетчика, а именно операция сравнения, поэтому компилятор оптимизирует это примерно вот так:

Еще очень простой пример, в котором имеем массив символов, с помощью цикла проходим по всей строке и выполняем какие-то действия с символами:

В этом случае компилятор вынесет вызов strlen() в отдельную переменную:

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

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

Источник

Скользкая дорожка для поклонников volatile

для чего нужен модификатор volatile

для чего нужен модификатор volatile

Прошло уже практически 30 лет с тех пор, как в стандарте языка C появился модификатор volatile, но в наши дни это ключевое слово вызывает лишь больше вопросов и непонимания даже среди программистов, общающихся с железом на “ты”. Сейчас уже никого не удивишь многоядерным мобильным телефоном или компьютером, умещающимся в одном чипе. Прогресс не стоит на месте, компиляторы умнеют, задачи программистов усложняются, вынуждая помнить о барьерах компиляции и барьерах памяти работая на многопроцессорных системах, только volatile по-прежнему остается темным уголком стандарта, в котором лишь сказано, что доступ к такой изменчивой переменной “implementation-defined” (Стандарт C, 6.7.3/7), т.е. как решат ребята, разрабатывающие компилятор, так и будет.

Disclaimer

В данной статье я буду говорить об использовании volatile в языке С в контексте определения переменных, не касаясь ассемблерных вставок, помеченных как volatile. Так же я буду приводить примеры кода, генерируемого компилятором gcc для архитектуры x86-64, но все сказанное в полной мере относится вообще к любым современным компиляторам и архитектурам. А если ваш компилятор генерирует другой код, который работает, то это вовсе не значит, что со следующей версией поведение вашей программы, использующей volatile, не поменяется. Да и код, скорее всего, будет непереносим на другие платформы.

Зачем нужен volatile?

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

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

Ключевое слово volatile и является одним из звеньев, связывающих абстрактную машину и реальную. В USENET много постов (1, 2, 3) 20-летней давности, проливающих свет на темную историю появления этого модификатора. Приведу хороший перевод одного из писем, взятый здесь:

На уровне железа многие процессоры просто резервируют блок адресов памяти для портов ввода-вывода. Большинство процессоров имеют отдельное пространство адресов ввода-вывода, со специальными инструкциями для доступа к ним, но это не универсально (на PDP-11 такого не было, например) и даже сейчас, производители железа могут предпочесть использовать для этого адресное пространство памяти, по разным причинам. Я сомневаюсь, что кто-то так делает на архитектуре 8086 — различные адресные ограничения делают это очень сложным. Я видел это на 8080, это очень частое решение на старой TI 9900. И это был единственный способ организовать ввод-вывод на наборе инструкций PDP-11, там просто не было отдельного адресного пространства ввода-вывода (Я думаю, то же самое верно и для VAX. И не забывайте, что большинство работы на С раньше проходило именно на этих двух процессорах).

Теперь рассмотрим один из первых последовательных портов, что я использовал: Intel 8051. Нормальным способом его инициализации было записать 0 три раза в порт управления. Если у вас MMIO, то код на С мог бы выглядеть примерно так:

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

Таким образом, volatile обязывает компилятор каждый раз обращаться к памяти, избегая возможности кеширования значения в регистрах. Для разработчиков UNIX в 80-х этот модификатор был единственным способом борьбы с компилятором, который норовил соптимизировать и выкинуть действительно нужный код.

Распространенные заблуждения при использовании volatile

1. Обращение к volatile переменной атомарны

Рассмотрим такой код:

что создает компилятор:

2. volatile помогает создать lockless код

Это наиболее распространненное и опасное заблуждение, которое кочует из исходника в исходник. Рассмотрим код:

Программист уверен, что такое простое решение, основанное на volatile флаге, позволит ему избежать «ненужного» и «долгого» lock’а (под lock’ом я подразумеваю вызов mutex, если речь идет о userspace приложении, или запрет прерывания, если речь идет о низкоуровневом коде). А вот что создает компилятор:

Ожидания программиста не оправдались, вместо мифической экономии на lock’ах получилась редкая проблема, воспроизведение которой зависит от:

Как бороться? Использовать барьер компилятора. Это вот такая вот ассемблерная вставка, которая поддерживается большинством современных компиляторов (а если в вашем компиляторе такой инструкции нет, то значит есть какая-то другая):

которая является барьером, говорящая компилятору «сбрось все регистры в RAM до барьера и перечитай после».

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

Кстати говоря, барьером компилятора являются вызовы функций, if/for/while конструкции, return, etc. Для детального погружения в тему смотреть Стандарт C, Annex C Sequence points, 438 p.

3. volatile нужно использовать всегда, если переменная может измениться из нескольких контекстов выполнения

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

Наверное, программист надеется, что при обращении к членам данной структуры, компилятор сделает всю работу, подумав об атомарности, «правильном» порядке доступа к переменным и необходимых lock’ах. Ничего подобного не будет. На выходе будет неэффективный код, который ничего «правильного» делать не будет, так как в стандарте C ничего не сказано о том, что вообще должен делать компилятор при доступе к volatile переменной. Если код работает так, как ожидается, то это случайность. Более того, компиляторы тоже содержат баги, которые проявляются при обращении к volatile переменным, а как результат — создается неверный ассемблерный код.

Как правильно использовать volatile?

1. в контексте «asm volatile»

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

2. приведение к volatile указателю там, где нужно

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

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

В остальных случаях вы получаете лишь баги с редким и сложным воспроизведением.

Эпилог

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

Источник

volatile (C++)

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

Синтаксис

Примечания

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

volatile Ключевое слово может не влиять на поле, если выполняется одно из следующих условий.

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

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

Соответствие стандартам ISO

Окончание соответствия ISO

Блок, относящийся только к системам Microsoft

Когда используется параметр компилятора /volatile: MS — по умолчанию, если нацелены архитектуры, отличные от ARM, компилятор создает дополнительный код для поддержания упорядочения между ссылками на переменные объекты, а также для поддержания порядка ссылок на другие глобальные объекты. В частности:

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

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

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

Завершение блока, относящегося только к системам Майкрософт

Источник

Однажды вы читали о ключевом слове volatile…

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

Сегодня рассмотрим менее экзотический сценарий использования ключевого слова volatile.

Стандарт C++ определяет так называемое наблюдаемое поведение как последовательность операций ввода-вывода и чтения-записи данных, объявленных как volatile (1.9/6). В пределах сохранения наблюдаемого поведения компилятору позволено оптимизировать код как угодно.

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

Этот код проходит по всей области и читает по одному байту из каждой страницы памяти. Одна проблема – компилятор этот код оптимизирует и полностью удалит. Имеет полное право – этот код не влияет на наблюдаемое поведение. Ваши переживания о выделении страниц операционной системой и вызванных этим задержке к наблюдаемому поведению не относятся.

Что же делать, что же делать… А, точно! Давайте мы запретим компилятору оптимизировать этот код.

Отлично, в результате…

1. использована #pragma, которая делает код плохо переносимым, плюс…
2. оптимизация выключается полностью, а это увеличивает объем машинного кода в три раза, плюс в Visual C++, например, эта #pragma может быть использована только снаружи функции, соответственно, рассчитывать на встраивание этого кода в вызывающий код и дальнейшую оптимизацию тоже не приходится.

Здесь отлично помогло бы ключевое слово volatile:

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

Теперь попробуем перезаписать память во имя безопасности и паранойи (это не бред, вот как это бывает в реальной жизни). В том посте упоминается некая волшебная функция SecureZeroMemory(), которая якобы гарантированно перезаписывает нулями указанную область памяти. Если вы используете memset() или эквивалентный ей написанный самостоятельно цикл, например, такой:

для локальной переменной, то есть риск, что компилятор удалит этот цикл, потому что цикл не влияет на наблюдаемое поведение (доводы в том посте к наблюдаемому поведению тоже не относятся).

Что же делать, что же делать… А, мы «обманем» компилятор… Вот что можно найти по запросу “prevent memset optimization”:

1. замена локальной переменной на переменную в динамической памяти со всеми вытекающими накладными расходами и риском утечки (сообщение в архиве рассылки linux-kernel)
2. макрос с ассемблерной магией (сообщение в архиве рассылки linux-kernel)
3. предложение использовать специальный символ препроцессора, который запрещает встраивание memset() по месту и затрудняет компилятору оптимизацию (естественно, такая возможность должна быть поддержана в используемой версии библиотеки, плюс Visual C++ 10 умеет оптимизировать даже код функций, помеченных как не подлежащие встраиванию)
4. всевозможные последовательности чтения-записи с использованием глобальных переменных (кода становится заметно больше и такой код не потокобезопасен)
5. последующее чтение с сообщением об ошибке в случае, если считаны не те данные, что были записаны (компилятор имеет право заметить, что «не тех» данных оказаться не может, и удалить этот код)

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

Вы можете скомпилировать функцию перезаписи в отдельную единицу трансляции, чтобы компилятор «не увидел», что она делает. После очередной смены компилятора в игру вступит генерация кода линкером (LTCG в Visual C++, LTO в gcc или как это называется в используемом вами компилятором) – и компилятор прозреет и увидит, что перезапись памяти «не имеет смысла», и удалит ее.

Не зря появилась поговорка you can’t lie to a compiler.

А что если посмотреть на типичную реализацию SecureZeroMemory()? Она по сути такая:

И все – компилятор более не имеет права удалять запись…

КРАЙНЕ НЕОЖИДАННО… вопреки всем суевериям зачеркнутое утверждение выше неверно.

На самом деле – имеет. Стандарт говорит, что последовательность чтения-записи должна сохраняться только для данных с квалификатором volatile. Вот для таких:

Если сами данные не имеют квалификатора volatile, а квалификатор volatile добавляется указателю на эти данные, чтение-запись этих данных уже не относится к наблюдаемому поведению:

Вся надежда на разработчиков компилятора – в настоящий момент и Visual C++, и gcc не оптимизируют обращения к памяти через указатели с квалификатором volatile – в том числе потому, что это один из важных сценариев использования таких указателей.

Не существует гарантированного Стандартом способа перезаписать данные функцией, эквивалентной SecureZeroMemory(), если переменная с этими данными не имеет квалификатора volatile. Точно так же невозможно кодом как в самом начале поста гарантированно прочитать память. Все возможные решения не являются абсолютно переносимыми.

Причина этому банальна – это «не нужно».

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

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

Использование указателей на volatile является, скорее всего, самым эффективным способом решения проблемы. Во-первых, разработчики компиляторов обычно сознательно выключают оптимизацию доступа к памяти. Во-вторых, накладные расходы минимальны. В-третьих, относительно легко проверить, работает этот способ или нет на конкретной реализации, – достаточно посмотреть, какой машинный код будет сгенерирован для тривиальных примеров выше из этого поста.

volatile – не только для драйверов и операционных систем.

Дмитрий Мещеряков,
департамент продуктов для разработчиков

Источник


Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *