Volatile c что это

Volatile c что это

Ключевое слово volatile пишется до или после типа данных объявляемой переменной.

volatile int foo;
int volatile foo;

Указатели на volatile переменные объявляются так.

volatile int * pReg;
int volatile * pReg;

Volatile указатели на не volatile переменные встречаются крайне редко (думаю, я использовал их лишь один раз), но я на всякий случай дам их синтаксис:

int * volatile p;

И, для полноты, если вам понадобится volatile указатель на volatile переменную, следует написать:

int volatile * volatile p;

Если вы используете volatile для структуры (struct) или объединения (union), действие спецификатора будет распространяться на все содержимое структуры/объединения. Если вы не хотите этого, то можете применить спецификатор volatile к отдельным элементам структуры/объединения.

Правильное использование спецификатора VOLATILE

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

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

Далее мы поговорим о каждом из этих случаев.

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

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

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

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

int ar[1024]; for(size_t i = 0; i

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

int ar[1024]; for(size_t i = 0; i < 1024 / 4; i += 4)

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

Уроки С++. Изучай и оптимизируй! Советы С++. volatile


сhar str[125]; for(size_t i = 0; i

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

сhar str[125]; size_t length = strlen(str); for(size_t i = 0; i

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

const MAX_COUNT_PEOPLE = 4; size_t countPeole = 0; . if(countPeople > MAX_COUNT_PEOPLE) < // Выдаем предупреждение >// Значение переменной countPeople к примеру будет менять с другого потока

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

const MAX_COUNT_PEOPLE = 4; volatile size_t countPeole = 0; . if(countPeople > MAX_COUNT_PEOPLE) < // Выдаем предупреждение >// Значение переменной countPeople к примеру будет менять с другого потока

[edit] Keywords

demonstrates the use of volatile to disable optimizations

Run this code
#include #include int main(void) { clock_t t = clock(); double d = 0.0; for (int n = 0; n 10000; ++n) for (int m = 0; m 10000; ++m) d += d * n * m; // reads from and writes to a non-volatile printf(«Modified a non-volatile variable 100m times. » «Time used: %.2f secondsn», (double)(clock() — t)/CLOCKS_PER_SEC); t = clock(); volatile double vd = 0.0; for (int n = 0; n 10000; ++n) for (int m = 0; m 10000; ++m) { double prod = vd * n * m; // reads from a volatile vd += prod; // reads from and writes to a volatile } printf(«Modified a volatile variable 100m times. » «Time used: %.2f secondsn», (double)(clock() — t)/CLOCKS_PER_SEC); }
Modified a non-volatile variable 100m times. Time used: 0.00 seconds Modified a volatile variable 100m times. Time used: 0.79 seconds

[edit] References

  • C17 standard (ISO/IEC 9899:2018):
  • 6.7.3 Type qualifiers (p: 87-90)
  • C11 standard (ISO/IEC 9899:2011):
  • 6.7.3 Type qualifiers (p: 121-123)
  • C99 standard (ISO/IEC 9899:1999):
  • 6.7.3 Type qualifiers (p: 108-110)
  • C89/C90 standard (ISO/IEC 9899:1990):
  • 6.5.3 Type qualifiers

Прерывания

Обработчики прерываний часто изменяют состояние переменных, которое проверяется в основной ветви кода. К примеру, обработчик прерываний на последовательном порту проверяет каждый новый символ на равенство EXT (скорее всего, он сигнализирует об окончании сообщения). Если символ равен EXT, то ISR выставляет глобальный флаг. Неверная реализация

int etx_rcvd = FALSE; void main() < . while (!ext_rcvd) < // Wait >. > interrupt void rx_isr(void) < . if (ETX == rx_char) < etx_rcvd = TRUE; >. >

Если оптимизации компилятора выключены, то такой код может работать. Тем не менее, даже плохонький компилятор сломает код. Дело в том, что компилятор понятия не имеет, что переменная ext_rcvd может быть изменена в ISR (так как функция rx_isr нигде в коде не вызывается явно. прим. пер.). Компилятор решит, что значение !ext_rcvd всегда истина, и выйти из цикла невозможно. Следовательно, весь код после цикла может быть удалён за ненадобностью. Если повезёт, то компилятор пожалуется на этот участок кода. Если не повезёт (или вы легкомысленно относитесь к предостережениям компилятора) – ваш код с треском провалится. Естественно, во всём будет виноват «отвратительный оптимизатор».

Решение – объявить переменную ext_rcvd волатильной. Тогда все ваши проблемы (ну точно часть из них) исчезнут.

Многопоточные приложения

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

int cntr; void task1(void) < cntr = 0; while (cntr == 0) < sleep(1); >. > void task2(void)

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

Барьеры памяти

По определению David Howells и David Howells в статье LINUX KERNEL MEMORY BARRIERS:

Independent memory operations are effectively performed in random order, but this can be a problem for CPU-CPU interaction and for I/O. What is required is some way of intervening to instruct the compiler and the CPU to restrict the order.

Memory barriers are such interventions. They impose a perceived partial ordering over the memory operations on either side of the barrier.

Such enforcement is important because the CPUs and other devices in a system can use a variety of tricks to improve performance, including reordering, deferral and combination of memory operations; speculative loads; speculative branch prediction and various types of caching. Memory barriers are used to override or suppress these tricks, allowing the code to sanely control the interaction of multiple CPUs and/or devices.

Или в моей расслабленной интерпретации: барьеры памяти — это инструкции, способные заставить компилятор и даже процессор прекратить выполнять оптимизации и гарантировать, что определённые операции чтения и записи могут остаться с какой-либо из сторон барьера памяти.

Использование ключевого слова volatile или методов Volatile.Read/Write — это один из способов установить барьер памяти, Thread.MemoryBarier — другой.

Статья на эту тему Memory Barriers in .NET Nadeem Afana.

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

Мои замечания к статье:

  • Автор тоже упоминает, что существует модель памяти .NET в которой запрещены перестановки запись-запись.
  • Автор упоминает, что для lock, Interlocked и прочих вещей генерируется полный барьер памяти — ECMA-335 говорит нам другое в разделе I.12.6.5 Locks and threads.

Если статья вас заинтересовала, но некоторые слова вы не поняли, например, такие STORE Buffer и Cache Coherence, и есть желание разобраться дальше, то читайте статью Memory Barriers: a Hardware View for Software Hackers Paul E. McKenney (или русский перевод первой части статьи) — тут всё прямо с алгоритмами того, как процесс происходит внутри процессора.

Дополнительный материал по барьерам памяти

Волатильное чтение и запись на архитектуре процессора x86

Во многих статьях пишут, что на архитектуре процессора x86 все операции чтения и записи осуществляются как волатильное чтение и волатильная запись, поэтому использование волатильного чтения и записи в коде программы будет иметь влияние только на компилятор, но не на инструкции процессора. К сожалению, никто не даёт ссылок на источник этого утверждения, я попытался найти этот источник в итоге нашёл только описание модели памяти x86: Intel® 64 and IA-32 Architectures Software Developer’s Manual (раздел 8.2) и в нём нет формулировки про волатильное чтение и запись, есть только список разрешённых перестановок и фактически разрешена только перестановка запись и последующее чтение, что совпадает с разрешёнными перестановками при волатильных чтениях и записях (волатильная запись и последующее волатильное чтение могут быть переставлены) — видимо из-за этого совпадения разрешённых/запрещённых перестановок и возникла формулировка про то что операции чтения/записи на архитектуре x86 волатильные.

  • ECMA-335 Common Language Infrastructure (CLI)
  • ECMA-334 C# Language Specification
  • What Every Programmer Should Know About Memory Ulrich Drepper

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

Volatile c что это

2018-02-13 в 12:54, admin , рубрики: C, c++

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

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

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

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

int ar[1024]; for(size_t i = 0; i

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

int ar[1024]; for(size_t i = 0; i < 1024 / 4; i += 4)

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

сhar str[125]; for(size_t i = 0; i

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

сhar str[125]; size_t length = strlen(str); for(size_t i = 0; i

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

const MAX_COUNT_PEOPLE = 4; size_t countPeole = 0; . if(countPeople > MAX_COUNT_PEOPLE) < // Выдаем предупреждение >// Значение переменной countPeople к примеру будет менять с другого потока

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

const MAX_COUNT_PEOPLE = 4; volatile size_t countPeole = 0; . if(countPeople > MAX_COUNT_PEOPLE) < // Выдаем предупреждение >// Значение переменной countPeople к примеру будет менять с другого потока

Квалификатор типа volatile.

Этот замечательный спецификатор позволяет нам использовать переменную, значение которой может изменяться неявно в процессе выполнения программы. Что же это значит? А вот пример:

bool test = FALSE; if (test)

Так как наша переменная test нигде явно не изменяется (то есть не стоит слева от оператора присваивания) то заботливый компилятор в результате оптимизации прочитает ее значение только один раз и больше не станет, переменная то на его взгляд нигде не изменяется! А эта переменная на самом деле может принять другое значение в результате выполнения другого потока, либо изменить свое значение неявно в прерывании. А это уже катастрофа. И для того, чтобы ее предотвратить как раз и используется спецификатор volatile. То есть объявить нашу переменную мы должны так:

volatile bool test = FALSE;
Продолжаем.

Спецификатор класса памяти extern.

Если программа состоит из нескольких файлов, как почти всегда и бывает, то без ключевого слова extern не обойтись. Пусть в разных файлах используется одна и та же переменная x. Значит каждый файл должен знать о существовании этой переменной, но просто объявить переменную во всех файлах не прокатит — компилятор этого не допустит. Тут то и придет на помощь спецификатор extern. Вот пример: пусть программа состоит из двух файлов, в каждом из которых есть функции, использующие переменную x: Файл 1:

unsigned char x; void main()
extern unsigned char x; void testFunction()

При таком объявлении переменной ошибки не будет и все скомпилируется успешно. Тут есть важный момент — а именно разница между понятиями определить и объявить переменную. При объявлении переменной ей присваивается определенный тип и значение, а при определении — для нее выделяется память. Таким образом, если перед именем переменной стоит спецификатор extern, то мы объявляем переменную, не определяя ее. Вот собственно и все. Получилась небольшая памятка, которая позволит сразу же и без проблем разобраться с этими ключевыми словами без путаницы.

Оцените статью
TutShema
Добавить комментарий