В этой статье приведен перевод раздела "Compiler optimization -> Problems with reordering code" руководства [1], автор Jan Waclawek. Рассматривается вопрос нежелательного изменения порядка выполнения скомпилированного кода AVR, когда применяется оптимизация.
Программы содержат последовательности операторов, и "наивный" (когда отключена оптимизация) компилятор выполнил бы их точно в том порядке, в котором эти операторы записаны. Но оптимизирующий компилятор свободен перетасовывать операторы - или даже их части - если результат ("net effect" или результирующий эффект) с его точки зрения получится тот же самый. "Мера" этого "результирующего эффекта это то, что стандарт называет "побочными эффектами" ("side effects"), и этот результат достигается исключительно через доступ (на чтение и запись) к переменным, квалифицированным как volatile. Таким образом пока все чтения и записи к volatile-переменным происходят к одному и тому же адресу и в том же самом порядке (и запись запишет те же самые величины), программа считается работающей корректно. (Следует отметить важный момент - интервалы времени между последовательными volatile-доступами при этом анализе никак не учитываются.)
volatile переводится с английского языка как "изменчивый", "летучий". Такое имя для ключевого слова выбрано выбрано, чтобы лучше передать смысл переменной - она может быть изменена в любой момент какими-то невидимыми для компилятора причинами. Поэтому компилятор при включенной оптимизации не должен предполагать, что при отсутствии видимого доступа к переменной её значение осталось неизменным. Поэтому при необходимости выполнения операции над переменной или анализе её значения необходимо выполнить дополнительное чтение (или запись, в зависимости от вида операции) переменной.
Ключевое слово volatile имеет смысл только при включенной оптимизации для компилятора.
Подробнее про volatile см. [4, 5].
К сожалению, имеются также операторы, которые не покрываются volatile доступом. В качестве примера на платформе avr-gcc/avr-libc можно привести макросы cli() и sei(), определенные в заголовочном файле < avr/interrupt.h >, которые напрямую преобразовываются в соответствующие мнемоники команд ассемблера через оператор __asm__(). Они не представляют никакой доступ к переменным, даже доступ volatile, так что компилятор свободен их перемещать по коду. Хотя квалификатор "volatile" можно подцеплять к оператору __asm__(), его влияние на порядок следования (или переупорядочивание) кода не определяется четко в документации (и скорее всего перетасовка кода может быть устранена только полным отключением оптимизатора, см. также [2]), так что могут происходить (среди прочих) состояния:
Все равно инструкция volatile asm может быть перемещена компилятором отностительно другого кода, включая пересечение инструкций jump instructions. [...] Точно так же Вы не можете ожидать, что последовательность volatile asm инструкций останется совершенно последовательной. (См. также [3].)
Примечание: к сожалению, управлять оптимизацией с AVR GCC можно только на уровне целого модуля кода. Другие компиляторы, такие как IAR, позволяют отключать оптимизацию как на отдельных модулях кода, так и на отдельных функциях.
Есть другой механизм, который можно использовать для достижения кое-чего подобного: барьеры памяти *memory barriers). Это осуществляется через специальный "memory" clobber в оператор встраивания кода ассемблера (inline asm statement), и гарантирует, что все переменные будут сброшены из регистров в память до этого оператора, и будут заново вычитаны из памяти после этого оператора. Назначение барьеров памяти несколько отличается от организации порядка выполнения операторов кода: предполагается, что при использовании барьеров нет переменных, которые закэшированы в регистрах, так что можно безопасно изменять содержимое этих регистров например при переключении контекста в многозадачных операционных системах (на "больших" процессорах, где может быть выполнение кода с изменением порядка инструкций, эта технология подразумевает неявное использование специальных инструкций, которые принудительно переводят процессор в "упорядоченное" состояния исполнения кода; само собой, это не случай AVR-ок).
Однако барьер памяти хорошо работает для гарантии, что все volatile-доступы до барьера и после него произойдут в том порядке, в каком они находятся относительно барьера. Однако это все-таки не гарантирует, что компилятор не перенесет не связанные с volatile переменными операторы через этот барьер. Peter Dannegger предоставил хороший пример такого эффекта:
В этом примере потенциально медленная операция деления была перемещена через cli(), в результате прерывания были запрещены дольше, чем требовалось. Однако volatile-доступ произошел в нужном порядке относительно cli() или sei(); так что требуемый "результирующий эффект", требуемый стандартом, был достигнут, но только компиляция повлияла на время выполнения кода. Имейте в виду, что для многих встраиваемых систем время выполения кода может играть очень важное значение, поэтому здесь могла бы быть критическая ошибка.
К сожалению, в настоящий момент в avr-gcc (ни даже вообще в стандарте языка C), нет механизма принудительно заставить компилятор генерировать код 1-к-1 в контексте порядка выполнения кода (т. е. нельзя гарантировать, что в каком порядке операторы написаны, в таком они и выполняются) - хотя возможно исключение составит полное отключение оптимизации (-O0), либо написание критических секций кода на ассемблере (последний вариант самый лучший).
Выводы:
• Барьеры памяти (memory barriers) гарантируют правильный порядок доступа к переменным с атрибутом volatile.
• Барьеры памяти не гарантируют, что операторы с доступом к обычным переменным (которые не volatile), не будут произвольно расставлены относительно барьера.