AVR GCC: оптимизация и проблема перетасовки кода |
![]() |
Добавил(а) microsin |
В этой статье приведен перевод раздела "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 предоставил хороший пример такого эффекта: #define cli() __asm volatile( "cli" ::: "memory" )
#define sei() __asm volatile( "sei" ::: "memory" )
unsigned int ivar; void test2( unsigned int val ) { val = 65535U / val; cli(); ivar = val; sei(); } Этот C-код с включенной оптимизацией (-Os) скомпилируется в следующий код ассемблера: 00000112 < test2 >: 112: bc 01 movw r22, r24 114: f8 94 cli 116: 8f ef ldi r24, 0xFF ; 255 118: 9f ef ldi r25, 0xFF ; 255 11a: 0e 94 96 00 call 0x12c ; 0x12c < __udivmodhi4 > 11e: 70 93 01 02 sts 0x0201, r23 122: 60 93 00 02 sts 0x0200, r22 126: 78 94 sei 128: 08 95 ret В этом примере потенциально медленная операция деления была перемещена через cli(), в результате прерывания были запрещены дольше, чем требовалось. Однако volatile-доступ произошел в нужном порядке относительно cli() или sei(); так что требуемый "результирующий эффект", требуемый стандартом, был достигнут, но только компиляция повлияла на время выполнения кода. Имейте в виду, что для многих встраиваемых систем время выполения кода может играть очень важное значение, поэтому здесь могла бы быть критическая ошибка. К сожалению, в настоящий момент в avr-gcc (ни даже вообще в стандарте языка C), нет механизма принудительно заставить компилятор генерировать код 1-к-1 в контексте порядка выполнения кода (т. е. нельзя гарантировать, что в каком порядке операторы написаны, в таком они и выполняются) - хотя возможно исключение составит полное отключение оптимизации (-O0), либо написание критических секций кода на ассемблере (последний вариант самый лучший). Выводы: • Барьеры памяти (memory barriers) гарантируют правильный порядок доступа к переменным с атрибутом volatile. • Барьеры памяти не гарантируют, что операторы с доступом к обычным переменным (которые не volatile), не будут произвольно расставлены относительно барьера. [Ссылки] 1. AVR Libc Reference Manual Compiler optimization site:atmel.com. |