Поделиться через


Динамическая отладка C++ (предварительная версия)

Это важно

Динамическая отладка C++ в настоящее время доступна в предварительной версии. Эта информация относится к предварительной версии функции, которая может быть существенно изменена до выпуска. Корпорация Майкрософт не предоставляет никаких гарантий, выраженных или подразумеваемых, в отношении информации, предоставленной здесь.

Эта предварительная версия функции, доступная начиная с Visual Studio 2022 версии 17.14(предварительная версия 2), применяется только к проектам x64.

С помощью динамической отладки C++ можно отлаживать оптимизированный код, как если бы он не был неоптимизирован. Эта функция полезна для разработчиков, которым требуются преимущества производительности оптимизированного кода, например разработчики игр, которым требуются высокие частоты кадров. Благодаря динамическим возможностям отладки в C++ вы можете наслаждаться процессом отладки неоптимизированного кода, не жертвуя преимуществами производительности оптимизированных сборок.

Отладка оптимизированного кода вызывает проблемы. Компилятор изменяет положение и переорганизует инструкции по оптимизации кода. Результатом является более эффективный код, но это означает:

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

В прошлом разработчики сталкивались с этими проблемами и другими, когда они находились в процессе отладки оптимизированного кода. Теперь эти проблемы устранены, так как при динамической отладке C++ можно перейти в оптимизированный код, как если бы он не был неоптимизирован.

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

Попробуйте динамическую отладку C++

Во-первых, давайте рассмотрим, что такое отладка оптимизированного кода. Затем вы увидите, как динамическое отладка C++ упрощает процесс.

  1. Создайте проект консольного приложения C++ в Visual Studio. Замените содержимое файла ConsoleApplication.cpp следующим кодом:

    // Code generated by GitHub Copilot
    #include <iostream>
    #include <chrono>
    #include <thread>
    
    using namespace std;
    
    int step = 0;
    const int rows = 20;
    const int cols = 40;
    
    void printGrid(int grid[rows][cols])
    {
        cout << "Step: " << step << endl;
        for (int i = 0; i < rows; ++i)
        {
            for (int j = 0; j < cols; ++j)
            {
                cout << (grid[i][j] ? '*' : ' ');
            }
            cout << endl;
        }
    }
    
    int countNeighbors(int grid[rows][cols], int x, int y)
    {
        int count = 0;
        for (int i = -1; i <= 1; ++i)
        {
            for (int j = -1; j <= 1; ++j)
            {
                if (i == 0 && j == 0)
                {
                    continue;
                }
    
                int ni = x + i;
                int nj = y + j;
                if (ni >= 0 && ni < rows && nj >= 0 && nj < cols)
                {
                    count += grid[ni][nj];
                }
            }
        }
        return count;
    }
    
    void updateGrid(int grid[rows][cols])
    {
        int newGrid[rows][cols] = { 0 };
        for (int i = 0; i < rows; ++i)
        {
            for (int j = 0; j < cols; ++j)
            {
                int neighbors = countNeighbors(grid, i, j);
                if (grid[i][j] == 1)
                {
                    newGrid[i][j] = (neighbors < 2 || neighbors > 3) ? 0 : 1;
                }
                else
                {
                    newGrid[i][j] = (neighbors == 3) ? 1 : 0;
                }
            }
        }
        // Copy newGrid back to grid
        for (int i = 0; i < rows; ++i)
        {
            for (int j = 0; j < cols; ++j)
            {
                grid[i][j] = newGrid[i][j];
            }
        }
    }
    
    int main()
    {
        int grid[rows][cols] = { 0 };
    
        // Initial configuration (a simple glider)
        grid[1][2] = 1;
        grid[2][3] = 1;
        grid[3][1] = 1;
        grid[3][2] = 1;
        grid[3][3] = 1;
    
        while (true)
        {
            printGrid(grid);
            updateGrid(grid);
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            cout << "\033[H\033[J"; // Clear the screen
            step++;
        }
    
        return 0;
    }
    
  2. Измените раскрывающийся список "Конфигурации решений" на "Выпуск". Убедитесь, что раскрывающийся список платформы решений имеет значение x64.

  3. Выберите Построить>Перестроить решение.

  4. Установите точку останова на строке 55, int neighbors = countNeighbors(grid, i, j); в updateGrid(). Запустите программу.

  5. При достижении точки останова откройте окно Локальные. В главном меню выберите "Отладка>локальных параметров>". Обратите внимание, что в окне i вы не можете увидеть значение j или . Компилятор устранил их благодаря оптимизации.

  6. Попробуйте установить точку останова на строке 19, в cout << (grid[i][j] ? '*' : ' ');printGrid(). Это невозможно. Это поведение ожидается, потому что компилятор оптимизировал код.

Остановите программу и включите динамическую отладку C++ и повторите попытку.

  1. В обозревателе решений щелкните проект правой кнопкой мыши и выберите "Свойства ", чтобы открыть страницы свойств проекта.

  2. Выберите "Дополнительное>использование динамической отладки C++" и измените значение "Да".

    Снимок экрана, на котором показаны расширенные свойства проекта.

    Откроется страница свойств > Свойства конфигурации > Дополнительно > Использование динамической отладки C++. Для свойства задано значение Yes.

    Этот шаг добавляет ключ /dynamicdeopt к компилятору и линкеру. За кулисами он также отключает переключатели /GL и /OPT:ICF для оптимизации C++. Этот параметр не перезаписывает переключатели, добавленные вручную в командную строку или другие параметры оптимизации, которые задаются, например /O1.

  3. Выберите Построить>Перестроить решение. Появится диагностический код MSB8088 сборки, указывающий на несовместимость динамической отладки и оптимизации всей программы. Эта ошибка означает, что во время компиляции вся оптимизация/GL программы () была автоматически отключена.

    Вы можете вручную отключить всю оптимизацию программы в свойствах проекта. Выберите "Свойства> конфигурации" Расширенная>оптимизация всей программы" и измените значение "Отключить". Теперь MSB8088 он рассматривается как предупреждение, но он может рассматриваться как ошибка в будущей версии Visual Studio.

  4. Повторно запустите приложение.

    Теперь, когда вы попали в точку останова в строке 55, вы увидите значения i и j в окне Локальные. В окне стека вызовов показано, что updateGrid() неоптимизировано, а имя файла — life.alt.exe. Этот альтернативный двоичный файл используется для отладки оптимизированного кода.

    Снимок экрана, на котором показана отладка функции updateGrid.

    Точка останова отображается в функции updateGrid. Стек вызовов показывает, что функция неоптимизирована, а имя файла — life.alt.exe. В окне "Локальные" отображаются значения i и j и другие локальные переменные в функции.

    Функция updateGrid() неоптимизирована по запросу, так как в ней устанавливается точка останова. При переходе по оптимизированной функции во время отладки она не будет отключена. Если вы входите в функцию, она теряет оптимизацию. Основной способ деоптимизации функции — установить в ней точку останова или войти в нее.

    Вы также можете деоптимизировать функцию в окне стека вызовов. Щелкните правой кнопкой мыши функцию или выбранную группу функций и выберите Deoptimize для следующей записи. Эта функция полезна, если вы хотите просмотреть локальные переменные в оптимизированной функции, для которой точка останова не была установлена в другом месте стека вызовов. Функции, которые неоптимизированы таким образом, группируются в окне точек останова в виде группы точек останова с именем Deoptimized Functions. Если удалить группу точек останова, связанные функции будут возвращены в оптимизированное состояние.

Использование условных и зависимых точек останова

  1. Повторите настройку точки останова в строке 19. cout << (grid[i][j] ? '*' : ' ');printGrid() Теперь он работает. Добавление точки останова в функции снимает её оптимизацию, чтобы можно было отлаживать её в обычном режиме.

  2. Щелкните правой кнопкой мыши точку останова в строке 19, выберите "Условия" и задайте условие i == 10 && j== 10. Затем установите флажок "Включать только при срабатывании следующей точки останова:". Выберите точку останова в строке 55 из раскрывающегося списка. Теперь точка останова в строке 19 не срабатывает до тех пор, пока не будет достигнута точка останова в строке 50, и затем, когда grid[10][10] будет выводиться в консоль.

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

    Снимок экрана: параметры условной точки останова для строки 19.

    Условная точка останова отображается в строке 19, cout < < (grid[i][j] ? '*' : ' ');. Условие имеет значение i == 10 && j== 10. Флажок «Включить только при достижении следующей точки останова» выбран. Раскрывающийся список точек останова установлен на life.cpp строка 55.

  3. Продолжить запуск приложения. При нажатии точки останова в строке 19 щелкните правой кнопкой мыши строку 15 и нажмите кнопку "Задать следующую инструкцию ", чтобы повторно запустить цикл.

    Снимок экрана, на котором показана отладка функции printGrid.

    Условная и зависимая точка останова срабатывает на строке 19, cout < < (grid[i][j] ? '*' : ' '). В окне "Локальные" отображаются значения i и j и другие локальные переменные в функции. В окне стека вызовов показано, что функция неоптимизирована, а имя файла — life.alt.exe.

  4. Удалите все точки останова, чтобы вернуть неоптимизованные функции в оптимизированное состояние. В главном меню Visual Studio выберите пункт Отладка>Удалить все точки останова. Затем все функции возвращаются в оптимизированное состояние.

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

    Снимок экрана, на котором отображается окно точек останова.

    В окне "Точки останова" отображается группа "Деоптимизованные функции". Группа выбрана, и контекстное меню открыто с параметром "Удалить группу точек прерывания".

Отключение динамической отладки C++

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

  • В главном меню Visual Studio выберите пункт Инструменты>Параметры>Отладка>Общие. Снимите флажок "Автоматическая отмена оптимизации отлаживаемых функций при возможности (.NET 8+, динамическая отладка C++)." При следующем запуске отладчика код остается оптимизированным.
  • Многие точки останова динамической отладки — это две точки останова: одна в оптимизированном двоичном файле и одна в неоптимизованном двоичном файле. В окне Остановки выберите Показать столбцы>Функция. Снимите точку останова, связанную с бинарным файлом alt. Другая точка останова в паре прерывается в оптимизированном коде.
  • При отладке в главном меню Visual Studio выберите "Отладка>Windows>Дизассембли". Убедитесь, что на нем сосредоточено внимание. При переходе в функцию через окно Disassembly функция не будет деоптимизирована.
  • Полностью отключите динамическую отладку, не передавая /dynamicdeopt в cl.exe, lib.exe и link.exe. Если вы используете сторонние библиотеки и не можете их перестроить, не добавляйте /dynamicdeopt во время окончательного link.exe для отключения динамической отладки данного двоичного файла.
  • Чтобы быстро отключить динамическую отладку для одного двоичного файла (например, test.dll), переименуйте или удалите двоичный alt файл (например, test.alt.dll).
  • Чтобы отключить динамическую отладку для одного или нескольких .cpp файлов, не передайте /dynamicdeopt их при сборке. Оставшаяся часть проекта создается с помощью динамической отладки.

Включение динамической отладки C++ в Unreal Engine

Unreal Engine 5.6 поддерживает динамическую отладку C++ как для средства сборки Unreal, так и для акселератора Unreal Build. Существует два способа его включения:

  • Измените Target.cs файл проекта, чтобы он содержался WindowsPlatform.bDynamicDebugging = true.

  • Используйте конфигурацию редактора разработки и измените BuildConfiguration.xml, чтобы включить:

    <WindowsPlatform>
        <bDynamicDebugging>true</bDynamicDebugging>
    </WindowsPlatform>
    

Для Unreal Engine 5.5 или более ранних версий, перенесите изменения средства Unreal Build Tool из GitHub в ваш репозиторий. Затем включите bDynamicDebugging , как указано выше. Кроме того, необходимо использовать ускоритель сборки Unreal из Unreal Engine 5.6. Используйте последние биты из ue5-main или отключите UBA, добавив следующее:BuildConfiguration.xml

<BuildConfiguration>
    <bAllowUBAExecutor>false</bAllowUBAExecutor>
    <bAllowUBALocalExecutor>false</bAllowUBALocalExecutor>
</BuildConfiguration>

Дополнительные сведения о настройке сборки Unreal Engine см. в разделе "Конфигурация сборки".

Устранение неполадок

Если точки останова не срабатывают в деоптимизированных функциях:

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

  • Убедитесь, что файлы alt.exe и alt.pdb созданы. Для test.exe и test.pdb, test.alt.exe и test.alt.pdb должны существовать в одном каталоге. Убедитесь, что правильные параметры сборки заданы в соответствии с этой статьей.

  • Запись debug directory существует в test.exe, которую отладчик использует для нахождения двоичного файла alt, предназначенного для деоптимизированной отладки. Откройте командную строку Visual Studio с поддержкой x64 и выполните команду link /dump /headers <your executable.exe>, чтобы узнать, существует ли deopt запись. Запись deopt отображается в столбце Type, как показано в последней строке этого примера.

      Debug Directories
    
            Time Type        Size      RVA  Pointer
        -------- ------- -------- -------- --------
        67CF0DA2 cv            30 00076330    75330    Format: RSDS, {7290497A-E223-4DF6-9D61-2D7F2C9F54A0}, 58, D:\work\shadow\test.pdb
        67CF0DA2 feat          14 00076360    75360    Counts: Pre-VC++ 11.00=0, C/C++=205, /GS=205, /sdl=0, guardN=204
        67CF0DA2 coffgrp      36C 00076374    75374
        67CF0DA2 deopt         22 00076708    75708    Timestamp: 0x67cf0da2, size: 532480, name: test.alt.exe
    

    Если запись каталога отладки отсутствует, убедитесь, что вы передаете deopt в /dynamicdeopt, cl.exe и lib.exe.

  • Динамическая деоптимизация не будет работать согласованно, если /dynamicdeopt не передается в cl.exe, lib.exe и link.exe для всех .cpp, .lib и двоичных файлов. Убедитесь, что при сборке проекта заданы правильные параметры.

Дополнительные сведения о известных проблемах см. в C++ Dynamic Debugging: Full Debuggability for Optimized Builds.

Если что-то не работает должным образом, отправьте запрос в сообществе разработчиков. Добавьте максимально подробную информацию о проблеме.

Общие примечания

IncrediBuild 10.24 поддерживает сборки динамической отладки C++.
FastBuild версии 1.15 поддерживает сборки динамической отладки C++.

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

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

Многие точки останова динамической отладки — это две точки останова: одна в оптимизированном двоичном файле и одна в неоптимизованном двоичном файле. По этой причине вы видите более одной точки останова в окне Breakpoints.

Флаги компилятора, используемые для неоптимизированной версии, совпадают с флагами, используемыми для оптимизированной версии, за исключением флагов оптимизации и /dynamicdeopt. Это означает, что все флаги, заданные для определения макросов, и т. д., также задаются в неоптимизированной версии.

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

Интеграция систем

Для динамической отладки C++ необходимо задать флаги компилятора и компоновщика определенным образом. В следующих разделах описывается настройка выделенной конфигурации для динамической отладки, которая не имеет конфликтующих коммутаторов.

Если ваш проект собран с использованием системы сборки Visual Studio, хорошим способом создать конфигурацию для динамической отладки является использование Configuration Manager для клонирования конфигурации Release или Debug и внесения изменений для адаптации к динамической отладке. В следующих двух разделах описаны процедуры.

Создать конфигурацию релиза

  1. В главном меню Visual Studio выберите «Сборка»>Configuration Manager, чтобы открыть Configuration Manager.

  2. Выберите раскрывающийся список конфигурации и нажмите кнопку <"Создать...>".

    Снимок экрана: Configuration Manager.

    В Configuration Manager в контекстах проекта выпадающий список конфигурации открыт и выделен.

  3. Откроется диалоговое окно "Новая конфигурация решения ". В поле "Имя" введите имя новой конфигурации, например ReleaseDD. Убедитесь, что параметр Копировать настройки из: установлен на Release. Затем нажмите кнопку "ОК ", чтобы создать новую конфигурацию.

    Снимок экрана, показывающий диалоговое окно «Создание конфигурации проекта» для релизной сборки.

    Поле "Имя" имеет значение ReleaseDD. Раскрывающийся список "Копировать параметры из" имеет значение Release.

  4. Новая конфигурация появится в раскрывающемся списке активных конфигураций решения . Выберите Закрыть.

  5. Если в раскрывающемся списке конфигурации задано значение ReleaseDD, щелкните проект правой кнопкой мыши в обозревателе решений и выберите "Свойства".

  6. В "Свойства конфигурации">расширенные установите динамическую отладку C++ в положение Да.

  7. Убедитесь, что для всей оптимизации программы задано значение No.

    Снимок экрана, на котором показаны расширенные свойства проекта.

    Cтраница свойств открывается на Конфигурация > Advanced. Используйте динамическую отладку C++ . Для свойства задано значение Yes. Цельная оптимизация программы установлена в Нет.

  8. В разделе Свойства конфигурации>Компоновщик>Оптимизация убедитесь, что ключ свертывания COMDAT установлен в положение «Нет» (/OPT:NOICF).

    Снимок экрана, показывающий свойства проекта оптимизации компоновщика Linker.

    Страница свойств открывается на Конфигурация свойств > Компоновщик > Оптимизация > Включить CMDAT Folding. Свойство установлено в значение Нет (/OPT:NOICF).

Этот параметр добавляет переключатель /dynamicdeopt к компилятору и компоновщику. С учетом того, что коммутаторы оптимизации C++ /GL и /OPT:ICF тоже выключены, теперь можно создать и запустить проект в новой конфигурации, если требуется оптимизированная сборка релиза, которую можно использовать с динамической отладкой C++.

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

Дополнительные сведения о конфигурациях в Visual Studio см. в статье "Создание и изменение конфигураций".

Создание конфигурации отладки

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

  1. В главном меню Visual Studio выберите «Сборка»>Configuration Manager, чтобы открыть Configuration Manager.

  2. Выберите раскрывающийся список конфигурации и нажмите кнопку <"Создать...>".

    Снимок экрана: Configuration Manager.

    В диспетчере конфигураций в части окна, относящейся к контекстам проекта, раскрывающийся список конфигураций открыт и выделен.

  3. Откроется диалоговое окно "Новая конфигурация проекта ". В поле "Имя" введите имя новой конфигурации, например DebugDD. Убедитесь, что Копировать параметры из: установлено на Debug. Затем нажмите кнопку "ОК ", чтобы создать новую конфигурацию.

    Снимок экрана: диалоговое окно

    Поле имени имеет значение DebugDD. В раскрывающемся списке "Скопировать параметры из:" выбрано "Отладка".

  4. Новая конфигурация появится в раскрывающемся списке активных конфигураций решения . Выберите Закрыть.

  5. В раскрывающемся списке "Конфигурация" установите значение DebugDD, щелкните проект правой кнопкой мыши в обозревателе решений и выберите "Свойства".

  6. В Свойствах конфигурации>C/C++>Оптимизация включите нужные оптимизации. Например, можно установить оптимизацию на максимальную скорость (/O2).

  7. В C/C++>Генерации кода установите Базовые проверки среды выполнения на По умолчанию.

  8. В C/C++>General отключите поддержку отладки только моего кода.

  9. Установите формат отладочной информации на Program Database (/Zi).

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

Дополнительные сведения о конфигурациях в Visual Studio см. в статье "Создание и изменение конфигураций".

Аспекты настраиваемой системы сборки

Если у вас есть пользовательская система сборки, убедитесь, что вы:

  • Передайте /dynamicdeopt в cl.exe, lib.exe и link.exe.
  • Не используйте /ZI, любой из /RTC флагов и /JMC.

Для дистрибьюторов сборок:

  • Для проекта с именем testкомпилятор создает test.alt.obj, test.alt.exptest.objи test.exp. Компоновщик создает test.alt.exe, test.alt.pdb, test.exe и test.pdb.
  • Необходимо развернуть двоичный файл нового набора инструментов c2dd.dll вместе с c2.dll.

Несовместимые параметры

Некоторые параметры компилятора и компоновщика несовместимы с динамической отладкой C++. Если включить динамическую отладку C++ с помощью параметров проекта Visual Studio, несовместимые параметры автоматически отключаются, если они не заданы в дополнительных параметрах командной строки.

Следующие параметры компилятора несовместимы с динамической отладкой C++:

/GH
/GL
/Gh
/RTC1 
/RTCc 
/RTCs 
/RTCu 
/ZI (/Zi is OK)
/ZW 
/clr 
/clr:initialAppDomain
/clr:netcore
/clr:newSyntax
/clr:noAssembly
/clr:pure
/clr:safe
/fastcap
/fsanitize=address
/fsanitize=kernel-address

Следующие параметры компоновщика несовместимы с отладкой C++ в динамическом режиме:

/DEBUG:FASTLINK
/INCREMENTAL
/OPT:ICF  You can specify /OPT:ICF but the debugging experience may be poor

См. также

Флаг компилятора /dynamicdeopt (предварительная версия)
Флаг компоновщика /DYNAMICDEOPT (предварительный просмотр)
Динамическая отладка C++ : полная отладка для оптимизированных сборок
Отладочный оптимизированный код