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

Ключевое слово 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) не должны участвовать в оптимизации: мы показали, что компилятор не может и не должен делать какие-либо предположения о них. Это опасно.

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

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

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023