Отладка оптимизированного кода

Источник: IBM

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

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

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

Отладка оптимизированных программ

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

Компиляторы серии IBM XL C/C++ всегда позволяли ограничить степень оптимизации прикладной программы с целью упрощения отладки или предоставлять необходимую отладочную информацию. В релизе z/OS V2R1 компилятора z/OS XL C/C++ был представлен обширный набор усовершенствований в сфере отладки, в том числе типовые уровни оптимизации и механизмы встраивания. Эти усовершенствования дополнительно облегчают отладку оптимизированных программ.

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

Эти усовершенствования в сфере отладки упрощают и ускоряют программирование, уменьшают затраты на сопровождение, а также позволяют генерировать оптимизированные приложения, сохраняя возможность их отладки.

Пошаговое прохождение по программному коду

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

Возможные варианты отладки и оптимизации

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

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

Поддержка уровней отладки и информация, предоставляемая на разных уровнях отладки

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

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

В таблице 1 приведены обобщенные сведения о некоторых важных уровнях отладки, а также об информации, предоставляемой на этих уровнях.

Таблица 1. Характеристики уровней отладки

Уровень отладки Строки исходного кода с поддержкой контрольных точек Сведения о переменных
1 - Генерируются таблицы с номерами строк без гарантии, что эти номера строк в оптимизированных программах соответствуют строкам первоначального исходного кода - Информация о переменных отсутствует
2 - Генерируются таблицы с номерами строк без гарантии, что эти номера строк в оптимизированных программах соответствуют строкам первоначального исходного кода (аналогично уровню 1 отладки) - Информация о переменных генерируется, но без какой-либо гарантии ее корректности
3 - Генерируются таблицы с номерами строк без гарантии, что эти номера строк в оптимизированных программах соответствуют строкам первоначального исходного кода (аналогично уровню 1 отладки) - Параметры функции видимы в памяти для XPLINK
5 - Есть возможность остановки на строках операторов if, вызовов функций, циклов и первых исполняемых операторов функций
- В таблице с номерами строк перечислены лишь строки с операторами if, вызовами функций, циклами и первыми исполняемыми операторами функций
- Переменные видимы и корректны в точках, указанных в столбце со строками исходного кода
8 - Каждый исполняемый оператор
- В таблице с номерами строк перечислены лишь строки для всех исполняемых операторов
- Переменные корректны в точках, указанных в столбце со строками исходного кода
9 - Каждый исполняемый оператор
- В таблице с номерами строк перечислены лишь строки для всех исполняемых операторов
- Переменные корректны и допускают изменение в точках, указанных в столбце со строками исходного кода

Примечание:
Уровни отладки, не описанные в таблице 1, ведут себя как предшествующие им перечисленные уровни - например, уровень отладки 4 предоставляет такую же информацию, как уровень отладки 3. В будущих релизах функциональность этих уровней может измениться.

Отладка встроенных функций

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

Методы встраивания функций/процедур

Существует множество способов встраивания функций и процедур, например: опция встраивания (допускающая самонастройку в широком диапазоне), опция оптимизации, ключевое слово inline в C/C++, использование встроенных директив #pragma. При встраивании функции с помощью какого-либо из этих способов возникают проблемы при отладке. Это связано с тем, что сама функция в результате встраивания может перестать существовать, а если она и существует, она может не являться "экземпляром" исполняемой функции (поскольку была встроена).

Эффекты связывания

Кроме того, спецификация связывания XPLINK позволяет передавать параметры в регистрах, а не обязательно в памяти, как требуется при традиционном MVS-связывании (Multiple Virtual Storage). Это означает, что отладчик, предполагающий увидеть и изменить параметры какой-либо функции, сможет изменить требуемый параметр в ячейке памяти (если специфицирована подопция XPLINK STOREARGS). Однако это действие не окажет никакого влияния на программу, поскольку окружающий код использует версию из регистра.

Использование поддержки уровней отладки

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

Распространенным примером встроенных функций являются методы getter and setter, которые широко применяются в объектно-ориентированном программировании. В случае встраивания этих функций их параметры можно менять "на лету" с помощью отладчика на достаточно высоком уровне отладки. Например, функция setter может изменять значение параметра, что позволяет присваивать параметру значение, отличающееся от значения, заданного в первоначальном исходном коде.

Задание уровней отладки и их использование

Уровни отладки, описанные в этой статье, впервые появились в релизе z/OS V2.1 компиляторов XL C/C++, поэтому для их поддержки требуется как минимум этот релиз. Тем не менее новая функциональность уровней отладки не порождет каких-либо проблем совместимости с существующими сборками.

Существующая опция -g в Unix® System Services (USS) не изменила своего поведения. "Устаревшая" (deprecated) опция TEST по-прежнему предоставляет отладочную информацию ISD; подобно другим устаревшим опциям, она не была усовершенствована при реализации этой новой поддержки уровней отладки. Опция DEBUG была улучшена в связи с появлением новых уровней отладки - появились новые значения в подопции LEVEL. Существующее значение по умолчанию не было изменено, смысл значения LEVEL по умолчанию также не изменился. Уровень отладки 0 продолжает означать то же самое, что и прежде; он эквивалентен новому уровню отладки 9 для неоптимизированных компиляций и уровню отладки 2 для оптимизированных компиляций.

Упрощение специфицирования отладки

При использовании уровней отладки в USS были добавлены новые флаги, призванные облегчить специфицирование отладки. Теперь после флага -g может следовать дополнительное число, обозначающее уровень отладки.

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

Таблица 2. Эквивалентные спецификации уровня отладки

xlc -qdebug=level=8 -O2 hello.c
xlc -Wc,"DEBUG(LEVEL(8))" -O2 hello.c 
xlc -g8 -O2 hello.c 

Не требуется никаких новых библиотек или наборов данных

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

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

Заключение

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

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

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

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=36981