Рубріки: ОсновыТеория

Ключевое слово volatile в C/C++: пример, как и зачем его использовать

Семен Гринштейн

В языках С/С++ volatile занимает особое место: это ключевое слово заставляет компилятор при оптимизации исходного кода по-другому обходиться с переменными.

Добавляя это volatile к переменной, мы предупреждаем, что «потусторонние силы» (какие — расскажем дальше) могут модифицировать ее значение в любой момент. И компилятор никогда не сможет предвидеть или вычислить это заранее.

Когда нужно использовать volatile

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

  • операционной системы (модификация переменной при работе прерываний — IRQ);
  • сторонних процессов или потоков (они могут совместно использовать нашу переменную);
  • периферийных устройств и другого железа (взаимодействие через порты ввода/вывода).

Представим, что есть некое периферийное устройство с каким-то I/O- портом:

volatile uint32 * statusPtr = 0xF1230000;

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

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

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

Давайте рассмотрим более развернутый пример, как volatile переворачивает игру.

Как компилятор оптимизирует код с volatile

Преобразование исходного кода в файл

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

Вообще, типичный компилятор C/C++ много чего умеет. Например, перед оптимизацией он анализирует исходный код (синтаксис и семантику), а после оптимизации генерирует объектный код. Это все — машинный код со служебными данными, необходимыми для сборки исполняемого файла.

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

Две самые важные цели оптимизации — увеличить скорость работы и сократить размер кода.

Кроме того, не мешало бы снизить энергопотребление.

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

Рассмотрим два похожих фрагмента кода. Единственное отличие — это присутствие слова volatile во втором фрагменте.

Пример 1. Нет волатильной переменной:

int buffer_full;
int read_stream(void)
{
    int count = 0;
    while (!buffer_full)
    {
        count++;
    }
    return count;
}

Пример 2. Есть волатильная переменная:

volatile int buffer_full;
int read_stream(void)
{
    int count = 0;
    while (!buffer_full)
    {
        count++;
    }
    return count;
}

Оба фрагмента кода запускают цикл (спойлер: в одном из них он не прервется никогда). Он крутится до тех пор, пока флаг buffer_full не примет значение 1 (то есть true). Его значение асинхронно меняют другие (сторонние) процессы. А оба наших фрагмента кода одинаково ничего не знают об этом.

Но после оптимизации из маленькой разницы (то есть присутствия volatile в исходном коде) вырастает большая разница на уровне машинного кода.

Я использовал вот такой компилятор и такие флаги оптимизации:

armclang --target=arm-arm-none-eabi -march=armv8-a -Os -S

 Результат: пример 1 (без волатильной переменной):

read_stream:                            
        movw    r0, :lower16:buffer_full
        movt    r0, :upper16:buffer_full
        ldr     r1, [r0]
        mvn     r0, #0
.LBB0_1:                                
        add     r0, r0, #1
        cmp     r1, #0
        beq     .LBB0_1     ; infinite loop
        bx      lr

В листинге, сгенерированном из исходного кода без volatile, операция ldr r1, [r0] загружает значение buffer_full (из регистра r0) в регистр r1 перед началом цикла.

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

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

 Результат: пример 2 (есть волатильная переменная):

read_stream:                            
        movw    r1, :lower16:buffer_full
        mvn     r0, #0
        movt    r1, :upper16:buffer_full
.LBB1_1:                                
        ldr     r2, [r1]     ; buffer_full
        add     r0, r0, #1
        cmp     r2, #0
        beq     .LBB1_1
        bx      lr

В листинге, сгенерированном из исходного кода с volatile операция ldr r2, [r1] расположена внутри цикла. Там актуальное значение переменной buffer_full на каждой итерации загружается в регистр r2. Если, например, какой-то сторонний процесс изменит его на true, цикл будет завершен. Что и требовалось показать!

Меры и предосторожности

Переменные в таких ситуациях (как в этом примере с buffer_full) не должны участвовать в оптимизации: мы показали, что компилятор не может и не должен делать какие-либо предположения о них. Это опасно.

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

Останні статті

Что такое прокси-сервер: пояснение простыми словами, зачем нужны прокси

Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…

21.11.2024

Что такое PWA приложение? Зачем необходимо прогрессивное веб-приложение

Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…

19.11.2024

Как создать игру на телефоне: программирование с помощью конструктора

Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…

17.11.2024

Google Bard: эффективный аналог ChatGPT

В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…

14.11.2024

Скрипт и программирование: что это такое простыми словами

Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…

12.11.2024

Дедлайн в разработке: что это такое простыми словами

Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…

11.11.2024