Почему в программе C / C++ часто отключается оптимизация в режиме отладки?

В большинстве сред C или C++ есть режим отладки и режим компиляции. Глядя на разницу между ними, вы обнаруживаете, что режим отладки добавляет символы отладки (часто параметр -g во многих компиляторах), но также отключает большинство оптимизаций. В режиме «выпуска» у вас обычно включены всевозможные оптимизации. В чем разница?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
13
0
3 822
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

Ответ принят как подходящий

Без какой-либо оптимизации поток вашего кода будет линейным. Если вы находитесь в строке 5 и один шаг, вы переходите к строке 6. При включенной оптимизации вы можете получить переупорядочение инструкций, развертывание цикла и всевозможные оптимизации. Например:


void foo() {
1:  int i;
2:  for(i = 0; i &lt 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)
  • Вместо доступа к памяти, доступ указателя может быть перемещен в регистр-> Нет обновления ввода-вывода
  • доступ к памяти может быть кэширован (не обязательно, связанный с оптимизацией компилятора)

Во всех этих случаях поведение было бы совершенно другим и, скорее всего, неправильным.

как насчет volatile int* ptr? IIUC решит пункты 2 и 3, верно? Однако насчет while я не уверен.

Alexander Malakhov 13.03.2013 11:50

Еще одно важное различие между отладкой и выпуском - способ хранения локальных переменных. Концептуально локальным переменным выделяется память в фрейме стека функций. Файл символов, сгенерированный компилятором, сообщает отладчику смещение переменной в кадре стека, поэтому отладчик может показать его вам. Для этого отладчик просматривает область памяти.

Однако это означает, что каждый раз, когда локальная переменная изменяется, сгенерированный код для этой исходной строки должен записывать значение обратно в правильное место в стеке. Это очень неэффективно из-за накладных расходов на память.

В сборке выпуска компилятор может назначить локальную переменную регистру для части функции. В некоторых случаях он может вообще не назначать для него хранилище стека (чем больше регистров на машине, тем проще это сделать).

Однако отладчик не знает, как регистры сопоставляются с локальными переменными для конкретной точки кода (мне неизвестен какой-либо формат символа, который включает эту информацию), поэтому он не может показать ее вам точно, поскольку не Не знаю, куда его искать.

Еще одна оптимизация - встраивание функций. В оптимизированных сборках компилятор может заменить вызов foo () фактическим кодом для foo везде, где он используется, потому что функция достаточно мала. Однако, когда вы пытаетесь установить точку останова на foo (), отладчик хочет знать адрес инструкций для foo (), и на это больше нет простого ответа - могут быть тысячи копий foo ( ) байты кода распределены по вашей программе. Отладочная сборка гарантирует, что вы можете где-нибудь поставить точку останова.

Ожидается, что отладочная версия будет - отлажена! Установка точек останова, пошаговое выполнение при просмотре переменных, трассировки стека и все остальное, что вы делаете в отладчике (IDE или другом), имеет смысл, если каждая строка непустого исходного кода без комментариев соответствует некоторой инструкции машинного кода.

Большинство оптимизаций нарушают порядок машинных кодов. Развертывание цикла - хороший пример. Общие подвыражения могут быть извлечены из циклов. При включенной оптимизации даже на самом простом уровне вы можете попытаться установить точку останова на строке, которой на уровне машинного кода не существует. Иногда вы не можете контролировать локальную переменную из-за того, что она хранится в регистре ЦП или, возможно, даже не оптимизирована!

Если вы отлаживаете на уровне инструкций, а не на уровне исходного кода, вам будет очень сложно сопоставить неоптимизированные инструкции с источником. Кроме того, компиляторы иногда ошибаются в своих оптимизаторах.

В подразделении Windows в Microsoft все бинарные файлы выпусков построены с отладочными символами и полной оптимизацией. Символы хранятся в отдельных файлах PDB и не влияют на производительность кода. Они не поставляются с продуктом, но большинство из них доступны по цене Сервер символов Microsoft.

Оптимизация кода - это автоматизированный процесс, который улучшает производительность кода во время выполнения при сохранении семантики. Этот процесс может удалить промежуточные результаты, которые не нужны для завершения вычисления выражения или функции, но могут быть интересны вам при отладке. Точно так же оптимизации могут изменить кажущийся поток управления, так что все может происходить в несколько ином порядке, чем в исходном коде. Это сделано для того, чтобы пропустить ненужные или избыточные вычисления. Такое изменение кода может нарушить соответствие между номерами строк исходного кода и адресами объектного кода, что затрудняет отслеживание отладчиком потока управления в том виде, в каком вы его написали.

Отладка в неоптимизированном режиме позволяет вам видеть все, что вы написали, так, как вы это написали, без удаления оптимизатором или изменения порядка вещей.

Убедившись, что ваша программа работает правильно, вы можете включить оптимизацию, чтобы повысить производительность. Несмотря на то, что в наши дни оптимизаторы заслуживают доверия, все же неплохо создать набор тестов хорошего качества, чтобы гарантировать, что ваша программа работает одинаково (с функциональной точки зрения, без учета производительности) как в оптимизированном, так и в неоптимизированном режиме.

Еще одна проблема с оптимизацией - это встроенные функции, также в том смысле, что вы всегда будете выполнять их пошагово.

С GCC, при одновременном включении отладки и оптимизации, если вы не знаете, чего ожидать, вы подумаете, что код ведет себя неправильно и повторно выполняет один и тот же оператор несколько раз - это случилось с парой моих коллег. Кроме того, отладочная информация, предоставляемая GCC с включенными оптимизациями, обычно имеет более низкое качество, чем они могли бы на самом деле.

Однако в языках, размещенных на виртуальной машине, таких как Java, оптимизация и отладка могут сосуществовать - даже во время отладки продолжается JIT-компиляция в собственный код, и только код отлаженных методов прозрачно преобразуется в неоптимизированную версию.

Я хотел бы подчеркнуть, что оптимизация не должна изменять поведение кода, если только используемый оптимизатор не содержит ошибок или сам код содержит ошибки и полагается на частично неопределенную семантику; последнее чаще встречается в многопоточном программировании или когда также используется встроенный ассемблер.

Code with debugging symbols are larger which may mean more cache misses, i.e. slower, which may be an issue for server software.

По крайней мере, в Linux (и нет причин, по которым Windows должна отличаться) отладочная информация упакована в отдельный раздел двоичного файла и не загружается во время обычного выполнения. Их можно разделить на другой файл, который будет использоваться для отладки. Кроме того, на некоторых компиляторах (включая Gcc, я думаю, также с компилятором Microsoft C) отладочная информация и оптимизация могут быть включены вместе. Если нет, очевидно, что код будет медленнее.

Другие вопросы по теме