Программирование AVR AVR GCC: оптимизация и проблема перетасовки кода Fri, December 13 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


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.
2. Options That Control Optimization site:gcc.gnu.org.
3. Assembler Instructions with C Expression Operands site:gcc.gnu.org.
4. Как использовать ключевое слово volatile на языке C.
5. 9 способов испортить код с помощью volatile.

 

Добавить комментарий


Защитный код
Обновить

Top of Page