static volatile что это
volatile для «чайников»
Виктор Тимофеев, июнь, 2010 osa@pic24.ru
Вступление
Определение
( volatile в переводе с английского означает «нестабильный», «изменчивый»)
Что это значит? Известно, что одной из характеристик компиляторов, говорящих за их качество, является способность оптимизировать генерируемый объектный код. Для этого они объединяют повторяющиеся конструкции, сохраняют в регистрах общего назначения промежуточные результаты вычислений, выстраивают последовательность команд так, чтобы минимизировать долго выполняющиеся фрагменты кода (например, обращение через косвенную адресацию), и т.д. Выполняя такую оптимизацию, они немного преобразует наш код, подменяя его идентичным с точки зрения алгоритма, но более быстрым и/или компактным. Но такую подмену можно делать не всегда. Рассмотрим пример:
С точки зрения алгоритма устанавливаются два младших разряда в переменной a. Оптимизатор может сделать подмену такого кода одним оператором:
выиграв таким образом пару тактов и пару ячеек ROM. Но представим себе, что эти же действия мы выполняем не над какой-то абстрактной переменной, а над периферийным регистром:
(в этом можно убедиться, заглянув в заголовочный файл для конкретного контроллера, поставляемый с компилятором). Квалификатор volatile запрещает производить оптимизацию кода, выполняющего действия над регистром PORTB. Поэтому даже взаимообратные действия останутся нетронутыми оптимизатором, и мы можем быть уверенны в том, что на выходе сформируется импульс.
Ошибки, связанные с volatile
Есть три основных типа ошибок, касающихся квалификатора volatile :
В чем разница volatile и static?
Если я правильно понял, то static хранит только последнее записанное в него значение, без разницы сколько потоков обращаются к этой переменной, каждый кто обратиться к ней получит последнее записанное значение.
А volatile в свою очередь создается с новым созданным объектом и при обращении к ней из разных потоков он возвращает самое свежее значение своего объекта.
Запутанно объяснил наверное, но вот как то так я понял.
Не знаю насколько принципиальная разница.
1 ответ 1
volatile означает, что чтение и запись переменной с данным квалификатором является атомарной операцией, при этом любое изменение такой переменной будет видно из других потоков.
Всё ещё ищете ответ? Посмотрите другие вопросы с метками java или задайте свой вопрос.
Похожие
Подписаться на ленту
Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.
дизайн сайта / логотип © 2021 Stack Exchange Inc; материалы пользователей предоставляются на условиях лицензии cc by-sa. rev 2021.11.23.40817
Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.
Скользкая дорожка для поклонников 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
Прошло уже практически 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 и 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 – не только для драйверов и операционных систем.
Дмитрий Мещеряков,
департамент продуктов для разработчиков