Перейти к содержанию

Почему программа работает с -O0 и ломается с -O2?

Обновлено Редакция GAW

Оптимизатор не меняет смысл правильной программы на C. Стандарт (раздел 5.1.2.3 C11) описывает поведение через абстрактную машину и разрешает не вычислять то, что не влияет на наблюдаемый результат. Наблюдаемы только обращения к volatile-объектам, ввод-вывод и запись в файлы. Всё, что программа «имела в виду», но не выразила так, исчезает при -O2, а при -O0 случайно работало.

Три частые причины

КодС -O0С -O2Исправление
while (!flag); где flag меняет прерываниечитает flag каждый разчитает один раз — вечный циклvolatile у flag
for (i = 0; i < 100000; i++); как задержкакрутитсяцикл без эффекта удалёнтаймер или _delay_ms
чтение регистра через обычный указательчитает каждый разчтение объединено или выброшеноуказатель на volatile

Первый случай avr-libc разбирает в своём FAQ как самый частый вопрос про прерывания; почему компилятор так поступает — в статье о volatile. Пустой цикл задержки ничего наблюдаемого не делает, и по правилу «как если бы» (5.1.2.3) компилятор вправе удалить его целиком, поэтому для задержек нужен таймер или функция из библиотеки — их возможности описаны в калькуляторе задержек AVR.

Неопределённое поведение

Хуже всего ошибки, о которых компилятор не предупреждает: переполнение знакового целого, чтение за границей массива, обращение к одному объекту через указатели разных типов. С -O0 такой код обычно делает «очевидное», а оптимизатор вправе считать, что такого не бывает, и строить код на этом допущении. Как писать на C для микроконтроллеров без этих ловушек, рассказывает статья «Язык C для микроконтроллеров».

Если переменную делят основная программа и обработчик, одного volatile мало: многобайтовое значение нужно читать атомарно. Об этом — статья «Прерывания и общие данные».

Похожие вопросы

Почему отладчик показывает, что переменная «optimized out»?

При оптимизации переменная может жить только в регистре или быть вычислена заранее, и в памяти её нет. Это не ошибка. Для отладки собирают с -Og или временно объявляют переменную volatile, но рабочую сборку проверяют с той же оптимизацией, что пойдёт в изделие.

Не проще ли выпускать прошивку с -O0?

Ошибка от этого не исчезнет, а только спрячется до следующего компилятора или изменения кода. К тому же -O0 даёт заметно больший и медленный код, а функции задержки avr-libc без оптимизации вообще не обеспечивают заданное время.

Как искать такую ошибку?

Включите предупреждения -Wall -Wextra и проверьте все переменные, которые меняют обработчики прерываний или DMA, — у них должен быть volatile. Затем ищите пустые циклы задержки, приведения указателей между типами и арифметику со знаковыми переполнениями — типичные случаи неопределённого поведения.

Источники

  1. ISO/IEC 9899:201x (C11), Committee Draft N1570, April 2011 — 5.1.2.3p4 — реализация может не вычислять часть выражения, если его значение не используется и нет нужных побочных эффектов; обращения к volatile-объектам — побочные эффекты, их выполняют строго по абстрактной машине
  2. GCC 16.2 Manual: When is a Volatile Object Accessed?; Options That Control Optimization — обращения к volatile-объектам сохраняются и упорядочиваются относительно друг друга; обращения к обычным объектам компилятор может переставлять и объединять; -Og — оптимизация с сохранением удобства отладки
  3. AVR-LibC 2.3.2: doc/api/faq.dox.in — «Why doesn't my program recognize a variable updated in an interrupt routine?» — с оптимизацией флаг без volatile читается один раз, и цикл while (flag == 0) никогда не завершается; «Which -O flag to use?» — больший номер -O не означает лучшую оптимизацию; include/util/delay.h — функции задержки работают как задумано только с включённой оптимизацией, без неё выдаётся предупреждение «Compiler optimizations disabled»

Нашли ошибку?