В большинстве сред C или С++ существует режим "отладки" и компиляция режима "release".
Рассматривая разницу между ними, вы обнаружите, что режим отладки добавляет символы отладки (часто параметр -g для множества компиляторов), но также отключает большинство оптимизаций.
В режиме "выпуска" вы обычно включаете все виды оптимизаций.
Почему разница?
Почему программа C/С++ часто отключает оптимизацию в режиме отладки?
Ответ 1
Без какой-либо оптимизации, поток через ваш код является линейным. Если вы находитесь в строке 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)
- Вместо доступа к памяти доступ указателя может быть перемещен в регистр → Нет обновления ввода/вывода
- доступ к памяти может быть кэширован (не обязательно связан с оптимизацией компилятора)
Во всех этих случаях поведение будет сильно отличаться и, скорее всего, будет неправильным.
Ответ 2
Еще одно важное отличие между отладкой и выпуском - сохранение локальных переменных. Концептуально локальные переменные выделяют хранилище в фрейме стека функций. Файл символов, сгенерированный компилятором, сообщает отладчику смещение переменной в стеке стека, поэтому отладчик может показать его вам. Отладчик заглядывает в ячейку памяти, чтобы сделать это.
Однако это означает, что каждый раз, когда локальная переменная изменяется, сгенерированный код для этой исходной строки должен записать значение обратно в нужное место в стеке. Это очень неэффективно из-за нехватки памяти.
В сборке релизов компилятор может назначить локальную переменную регистру для части функции. В некоторых случаях он может вообще не назначать для него хранилище стеков (чем больше регистров, тем проще это сделать машине).
Однако отладчик не знает, как регистры сопоставляют локальные переменные для определенной точки в коде (я не знаю ни одного символьного формата, который включает эту информацию), поэтому он не может точно показать это вам так как он не знает, куда его искать.
Другая оптимизация будет включать функцию. В оптимизированных сборках компилятор может заменить вызов foo() фактическим кодом для foo всюду, когда он используется, потому что функция достаточно мала. Однако, когда вы пытаетесь установить точку останова на foo(), отладчик хочет знать адрес инструкций для foo(), и теперь нет более простого ответа на этот вопрос - могут быть тысячи копий foo ( ) байты кода, распространяемые по вашей программе. Отладочная сборка гарантирует, что вам где-то нужно поставить точку останова.
Ответ 3
Оптимизация кода - это автоматизированный процесс, который улучшает производительность во время выполнения кода при сохранении семантики. Этот процесс может удалять промежуточные результаты, которые не нужны для завершения оценки выражения или функции, но могут вас заинтересовать при отладке. Точно так же оптимизация может изменить видимый поток управления, чтобы вещи могли произойти в несколько ином порядке, чем то, что появляется в исходном коде. Это делается для того, чтобы пропустить ненужные или избыточные вычисления. Это перекодирование кода может испортиться с отображением между номерами строк исходного кода и адресными кодами объектов, что затрудняет отладчику следить за потоком управления, как вы его написали.
Отладка в неоптимизированном режиме позволяет вам видеть все, что вы написали, так как вы написали его без удаления или переупорядочения оптимизатора.
Как только вы счастливы, что ваша программа работает правильно, вы можете включить оптимизацию для повышения производительности. Несмотря на то, что оптимизаторы в наши дни очень надежны, все же неплохо создать набор тестов хорошего качества, чтобы гарантировать, что ваша программа работает идентично (с функциональной точки зрения, не учитывая производительность) как в оптимизированном, так и в неоптимизированном режиме.
Ответ 4
Ожидается, что отладочная версия будет отлажена! Установка точек останова, одношаговой при просмотре переменных, трассировки стека и всего остального, что вы делаете в отладчике (IDE или в другом случае), имеет смысл, если каждая строка непустого исходного кода без комментария соответствует некоторой инструкции машинного кода.
Большинство оптимизаций беспорядок с порядком машинных кодов. Хорошим примером является разворот петли. Общие подвыражения могут быть сняты из циклов. Если оптимизация включена, даже самый простой уровень, вы можете попытаться установить точку останова на строке, которая на уровне машинного кода не существует. Иногда вы не можете контролировать локальную переменную из-за того, что она хранится в регистре CPU или даже оптимизирована из существования!
Ответ 5
Если вы отлаживаете уровень команд, а не исходный уровень, вам гораздо проще сопоставить неоптимизированные инструкции с исходным кодом. Кроме того, компиляторы иногда ошибаются в своих оптимизаторах.
В подразделении Windows в Microsoft все исполняемые файлы выпуска создаются с помощью отладочных символов и полной оптимизации. Символы хранятся в отдельных файлах PDB и не влияют на производительность кода. Они не поставляются с продуктом, но большинство из них доступны на Microsoft Symbol Server.
Ответ 6
Еще одна проблема с оптимизацией - это встроенные функции, а также в том смысле, что вы всегда будете проходить через них.
С GCC, когда отладка и оптимизация включены вместе, если вы не знаете, чего ожидать, вы подумаете, что код плохо себя ведет и повторяет один и тот же оператор несколько раз - это случилось с несколькими моими коллегами. Кроме того, отладка информации, предоставленной GCC с оптимизацией, как правило, имеет более низкое качество, чем они могли, на самом деле.
Однако в языках, размещенных на виртуальной машине, таких как Java, оптимизация и отладка могут сосуществовать - даже во время отладки компиляция JIT для собственного кода продолжается, и только код отлаженных методов прозрачно преобразуется в неоптимизированную версию.
Я хотел бы подчеркнуть, что оптимизация не должна изменять поведение кода, если используемый оптимизатор не работает, или сам код не работает, и полагается на семантику undefined; последний чаще встречается в многопоточном программировании или когда используется встроенная сборка.
Код с отладочными символами больше, что может означать больше промахов в кеше, то есть медленнее, что может быть проблемой для серверного программного обеспечения.
По крайней мере, в Linux (и нет причин, по которым Windows должна отличаться) информация об отладке упаковывается в отдельный раздел двоичного файла и не загружается во время обычного выполнения. Их можно разделить на другой файл, который будет использоваться для отладки. Кроме того, на некоторых компиляторах (включая Gcc, я думаю, также с компилятором Microsoft C) отладка информации и оптимизации могут быть одновременно включены. Если нет, очевидно, что код будет медленнее.