Почему программы на C/C++ часто отключают оптимизацию в режиме отладки?
В большинстве сред C или C++ существует режим "отладки" и компиляция в режиме "выпуска".
Глядя на разницу между ними, вы обнаружите, что режим отладки добавляет символы отладки (часто это опция -g на многих компиляторах), но также отключает большинство оптимизаций.
В режиме "релиз" у вас обычно включены все виды оптимизации.
Почему разница?
6 ответов
Без какой-либо оптимизации поток через ваш код является линейным. Если вы находитесь на 5-й строке и выполните один шаг, вы переходите к 6-й строке. При включенной оптимизации вы можете получить переупорядочение команд, развертывание цикла и все виды оптимизации.
Например:
void foo() {
1: int i;
2: for(i = 0; i < 2; )
3: i++;
4: return;
В этом примере без оптимизации вы можете пошагово пройти по коду и набрать строки 1, 2, 3, 2, 3, 2, 4
При включенной оптимизации вы можете получить путь выполнения, который выглядит следующим образом: 2, 3, 3, 4 или даже просто 4! (Функция ничего не делает в конце концов...)
В итоге, отладка кода с включенной оптимизацией может быть непростой задачей! Особенно если у вас большие функции.
Обратите внимание, что включение оптимизации меняет код! В определенной среде (системы, критичные для безопасности) это недопустимо, и отлаживаемый код должен быть доставлен. В этом случае нужно отладить с оптимизацией.
Хотя оптимизированный и неоптимизированный код должен быть "функционально" эквивалентным, при определенных обстоятельствах поведение изменится.
Вот упрощенный пример:
int* ptr = 0xdeadbeef; // some address to memory-mapped I/O device
*ptr = 0; // setup hardware device
while(*ptr == 1) { // loop until hardware device is done
// do something
}
С оптимизацией это просто, и вы знаете, чего ожидать. Однако, если вы включите оптимизацию, может произойти пара вещей:
- Компилятор может оптимизировать блок while (мы начинаем с 0, он никогда не будет равен 1)
- Вместо доступа к памяти, указатель доступа может быть перемещен в регистр-> Нет I/O Update
- доступ к памяти может быть кэширован (необязательно связан с оптимизацией компилятора)
Во всех этих случаях поведение было бы радикально другим и, скорее всего, неправильным.
Другое важное различие между отладкой и выпуском заключается в том, как хранятся локальные переменные. Концептуально локальные переменные выделяются для хранения в фрейме стека функций. Файл символов, сгенерированный компилятором, сообщает отладчику смещение переменной в кадре стека, поэтому отладчик может показать его вам. Отладчик заглядывает в область памяти, чтобы сделать это.
Однако это означает, что каждый раз, когда локальная переменная изменяется, сгенерированный код для этой исходной строки должен записать значение обратно в правильное место в стеке. Это очень неэффективно из-за нехватки памяти.
В сборке релиза компилятор может назначить локальную переменную регистру для части функции. В некоторых случаях он может вообще не назначать ему стековое хранилище (чем больше регистров у машины, тем проще это сделать).
Тем не менее, отладчик не знает, как регистры отображаются на локальные переменные для определенной точки в коде (я не знаю ни о каком формате символов, который включает эту информацию), поэтому он не может показать его вам точно, так как Не знаю, куда идти искать его.
Еще одна оптимизация - встроенная функция. В оптимизированных сборках компилятор может заменить вызов foo() реальным кодом для foo везде, где он используется, потому что функция достаточно мала. Однако, когда вы пытаетесь установить точку останова для foo(), отладчик хочет знать адрес инструкций для foo(), и на этот вопрос уже нет простого ответа - могут быть тысячи копий foo (байт кода распространяется по вашей программе. Отладочная сборка гарантирует, что у вас есть место для установки точки останова.
Оптимизация кода - это автоматизированный процесс, который повышает производительность кода во время выполнения при сохранении семантики. Этот процесс может удалить промежуточные результаты, которые не нужны для завершения выражения или оценки функции, но могут представлять интерес для вас при отладке. Аналогичным образом, оптимизация может изменить кажущийся поток управления, чтобы вещи могли происходить в несколько ином порядке, чем в исходном коде. Это сделано, чтобы пропустить ненужные или избыточные вычисления. Это перераспределение кода может привести к путанице между номерами строк исходного кода и адресами объектного кода, что затрудняет отладчик для отслеживания потока управления, как вы его написали.
Отладка в неоптимизированном режиме позволяет вам видеть все, что вы написали, так, как вы это написали, без оптимизатора, удаляющего или изменяющего порядок вещей.
Если вы довольны тем, что ваша программа работает правильно, вы можете включить оптимизацию для повышения производительности. Несмотря на то, что в наши дни оптимизаторы заслуживают доверия, все же хорошая идея - создать набор тестов хорошего качества, чтобы гарантировать, что ваша программа работает одинаково (с функциональной точки зрения, без учета производительности) как в оптимизированном, так и в неоптимизированном режиме.
Ожидается, что отладочная версия будет отлажена! Установка точек останова, пошаговое выполнение при просмотре переменных, трассировка стека и все остальное, что вы делаете в отладчике (IDE или иным образом), имеет смысл, если каждая строка непустого исходного кода без комментариев соответствует некоторой инструкции машинного кода.
Большинство оптимизаций путаются с порядком машинных кодов. Развертывание петли является хорошим примером. Общие подвыражения могут быть выведены из циклов. Когда оптимизация включена, даже на самом простом уровне, вы можете пытаться установить точку останова на строке, которой на уровне машинного кода не существует. Иногда вы не можете отслеживать локальную переменную из-за того, что она хранится в регистре процессора или, возможно, даже оптимизирована из-за отсутствия!
Еще одна проблема с оптимизацией - встроенные функции, в том смысле, что вы всегда будете проходить через них один шаг.
С GCC, с отладкой и оптимизацией, включенными вместе, если вы не знаете, чего ожидать, вы будете думать, что код неправильно себя ведет и повторно выполняет одно и то же утверждение несколько раз - это случилось с парой моих коллег. Кроме того, информация отладки, предоставляемая GCC с оптимизацией, как правило, имеет худшее качество, чем могла бы, на самом деле.
Однако в языках, размещенных на виртуальной машине, такой как Java, оптимизации и отладка могут сосуществовать - даже во время отладки JIT-компиляция в собственный код продолжается, и только код отлаженных методов прозрачно преобразуется в неоптимизированную версию.
Я хотел бы подчеркнуть, что оптимизация не должна изменять поведение кода, если только используемый оптимизатор не глючит, или сам код глючит и полагается на частично неопределенную семантику; последнее чаще встречается в многопоточном программировании или когда также используется встроенная сборка.
Код с символами отладки больше, что может означать больше пропусков кэша, т.е. медленнее, что может быть проблемой для серверного программного обеспечения.
По крайней мере, в Linux (и нет причин, по которым Windows должна отличаться) отладочная информация упакована в отдельный раздел двоичного файла и не загружается во время обычного выполнения. Их можно разбить на другой файл для отладки. Кроме того, на некоторых компиляторах (включая Gcc, я полагаю, также и с компилятором C от Microsoft) информация об отладке и оптимизации могут быть включены одновременно. Если нет, то, очевидно, код будет медленнее.
Если вы выполняете отладку на уровне инструкций, а не на уровне исходного кода, вам будет намного проще сопоставить неоптимизированные инструкции с источником. Кроме того, компиляторы иногда глючат в своих оптимизаторах.
В подразделении Windows в Microsoft все двоичные файлы выпуска построены с использованием символов отладки и полной оптимизации. Символы хранятся в отдельных файлах PDB и не влияют на производительность кода. Они не поставляются с продуктом, но большинство из них доступны на сервере Microsoft Symbol Server.