Введение
Parallel Inspector является одним из четырех инструментов, входящих в состав набора Intel Parallel Studio. Inspector может быть установлен и проинтегрирован в Microsoft Visual Studio как часть набора, так и отдельно. На сегодняшний день — это самый интересный и ожидаемый инструмент в составе пакета, так как он помогает обнаружить ошибки в многопоточной программе, на этапе верификации, повышая корректность и стабильность ее исполнения. Специфичный функционал Inspector’а подразумевает его применение командами тестировщиков (QA team). Однако и сами разработчики включают проверку программы на наличие ошибок, хотя бы на уровне юнит-тестов (unit tests), в свою инженерную практику.
В данной статье мы рассмотрим особенности использования Inspector’а для поиска ошибок доступа к памяти. Инструмент также позволяет находить и ошибки, характерные исключительно для многопоточных программ (dead locks, data races), но эта тема заслуживает отдельной статьи, и мы ее обсудим позже.
Механизмы поиска ошибок
В общем случае механизм обнаружения ошибок памяти основан на структурном анализе всей совокупности актов чтения и записи в память процесса в течение исполнения программы. Такой подход не нов и используется различными «мемори-чекерами», которые применяют так называемую статическую инструментацию для отслеживания инструкций чтения/записи и вызовов функций API работы с памятью. Для этого «измерительный инструмент» (instrumentation engine) модифицирует бинарный код еще до запуска его на исполнение (static binary instrumentation), вставляя определенные инструкции до и сразу после нужной инструкции или функции. Далее, инструментированная программа запускается на исполнение, а при достижении вставок кода сохраняются параметры программы, такие как временная отметка, текущий стек и контекст исполнения. Затем, вся эта масса информации обрабатывается с целью поиска нарушений доступа, некорректного использования, и других ошибок работы с памятью.
В Inspector’е применяется несколько другой подход анализа всех инструкций чтения/записи памяти и их адресов на уровне бинарного кода с помощью динамической бинарной инструментации. В основе инструментатора лежит утилита Pin — Dynamic Binary Instrumentation Tool, которая внедряется в анализируемый процесс непосредственно перед стартом и позволяет отслеживать выполнение практически любых инструкций, предоставляет API доступа к содержимому регистров, контексту выполнения программы, символьной и отладочной информации.
Можно провести некоторую аналогию между Pin и JIT (just-in-time) компилятором. Только на вход «Pin-компилятора» подается не байт-код, а исполняемый бинарный код. Pin перехватывает самую первую инструкцию программы и генерирует свою последовательность инструкций (трассу), совпадающую с первыми оригинальными инструкциями вплоть до первого ветвления программы (или до значения лимита инструкций в трассе), а затем, передает управление этой последовательности. Управление возвращается к Pin при наступлении ветвления, он генерирует новую последовательность инструкций, соответствующую ветке, и снова передает управление. Pin держит весь сгенерированный код в памяти, поэтому и исполнение и ветвление довольно эффективны. Оригинальный код не исполняется, а исполняются только сгенерированные последовательности. При этом пользователю утилиты дается возможность внедрять свой (анализирующий) код куда угодно (инструментация), за исключением разве что модулей ядра операционной системы.
Обе составляющие результирующего кода (инструментированный и анализирующий) «живут» в одном адресном пространстве модуля Pintool, который можно рассматривать как plug-in, позволяющий модифицировать процесс генерирования кода внутри утилиты Pin. Pintool регистрирует функции обратного вызова (callback), которые вызываются Pin каждый раз, когда в профилируемом приложении выполняется определенное условие (выполняется инструкция, вызывается функция, и т.д.). Эти функции ответственны за состав сгенерированной последовательности инструкций, они инспектируют код программы и определяют, есть ли необходимость, и где именно нужно вставить вызовы анализирующего кода в эту последовательность. Этими вызовами можно «накрыть» практически любые функции, исполняемые в приложении. А Pin позаботится о том, чтобы контексты вызовов были сохранены и восстановлены без изменений, чтобы функциям передавались правильные аргументы.
Pintool может иметь еще более низкую гранулярность и инструментировать каждую инструкцию приложения. Для этого Pin предоставляет специальный API, позволяющий селективно инструментировать инструкции определенного типа, например, доступа к памяти или ветвления. Инструкции, специфичные для определенных микроархитектур, тоже могут быть проинструментированы с помощью специального API.
Так как Pintool является плагином, то он исполняется в том же процессе, что Pin и анализируемое приложение. Поэтому ему доступны все данные исполняемого модуля, включая файлы и дескрипторы. Если исполняемый модуль слинкован во время компиляции с динамическими библиотеками, то Pintool имеет доступ и к ним тоже, так как он контролирует выполнение загрузчика библиотек.
Интерфейс
Таким образом, в зависимости от целей и задач анализа, можно сформировать несколько типов Pintool-инструментов, которые бы имели свой уровень гранулярности инструментации и были бы «нацелены» на сбор определенного типа данных в исполняемой программе. Понятно, что всесторонний анализ исполняемого кода не может быть проведен без существенных накладных расходов. Поэтому он разделен на уровни, использующие разные Pintool-инструменты, имеющие разные возможности обнаружения ошибок и отражающие глубину и сложность анализа. Чем выше уровень, тем, как правило, больше потребуется времени для проверки приложения.
Окно выбора глубины анализа
- Уровень mi1 – позволяет обнаруживать только утечки памяти, выделенной в куче (heap). Глубина стека функций равна 7. Это даст достаточно информации для определения местонахождения ошибки и структуры вызовов функций, выделявших память.
- Уровень mi2 – позволяет обнаруживать все остальные ошибки работы с памятью в куче, которые мы рассмотрим ниже. Однако для снижения накладных расходов и ускорения анализа, глубина стека выбрана равной единице. То есть, на этом уровне мы сможем найти ответ на вопрос, есть ли в принципе ошибки в программе. А где эти ошибки, нам поможет определить следующий уровень.
- Уровень mi3 – отличается от предыдущего тем, что глубина стека увеличена до 12-ти. Плюс добавлен функционал поиска утерянных указателей. На этом уровне мы получаем наиболее полный анализ корректности работы с памятью в куче, но заплатим за это накладными расходами, которые увеличат время выполнения программы от 20 до 80 раз по сравнению с оригиналом.
- Уровень mi4 – высший уровень, дополнен анализом ошибок доступа к памяти, выделенной на стеке, которые не обнаружены на стадии компиляции или с помощью run-time check опций компилятора. Уровень вложенности функций – 32. Как и все остальные уровни, 4-й является инклюзивным, то есть включающим в себя все виды анализа на предыдущих уровнях. Соответственно, накладные расходы будут максимальными.
Глубина стека на каждом уровне выбрана эмпирически и является компромиссом между полнотой предоставляемой информации и величиной накладных расходов на анализ приложения. В данной версии продукта пользователь не может менять глубину стека.
Чтобы начать анализ приложения, необходимо просто выбрать соответствующий тип анализа в инструментальной панели Parallel Inspector, нажать кнопку «Inspect» и выбрать уровень анализа.
Спецификация типа анализа Inspector’а
Прежде чем приложение будет запущено, соответствующий Pintool внедрится в его процесс и, заменив оригинальный код, запустится в новом процессе, инициализируя запуск кода приложения. Вместе с загрузкой дополнительных библиотек это занимает какое-то время, поэтому пользователь заметит задержку старта приложения. Чем больше динамических библиотек в программе, тем больше эта задержка. Далее, приложение исполняется как обычно, только медленнее — в зависимости от глубины анализа, и, конечно, от относительного количества специфичных вызовов функций Memory API во время исполнения.
Еще до окончания анализа приложения нам предоставляется лог событий или ошибок, которые были выявлены в процессе выполнения. Причем на всех уровнях, кроме mi1, мы можем начать анализировать логи, и исходный код еще до окончания анализа, так как результаты уже будут доступны.
Результаты анализа структурированы таким образом, чтобы вначале у нас был обзор списка проблем (Problem Sets), представляющих собой конечный результат действия нескольких событий, приведших к проблеме. Например, доступ к недействительной памяти является следствием выделения самой памяти, преждевременного ее освобождения и собственно попытки доступа. Такие детали в виде зарегистрированных событий (Observations) можно посмотреть в окне, нажав кнопку «Details» в главном окне. Там же можно быстро взглянуть на код, в котором это событие произошло (Рис.3).
Список обнаруженных утечек памяти и их размер
Это удобно, когда нет необходимости переключаться в редактор исходного кода, а нужно просто пройтись по ошибкам в списке и просмотреть, на какие строки кода ссылается диагностика.
В списке с найденными ошибками нам доступна вся информация относительно процесса, модуля, функции, и номера строки кода, в которой эта ошибка произошла. В реальных проектах список ошибок бывает значительный. В этом случае удобно воспользоваться фильтром по типу ошибки, ее описанию, по имени исходника, по имени функции или модуля. В процессе исправления ошибок можно помечать их как исправленные, или отфильтровывать, чтобы они не загромождали список.
Из списка проблем или детального списка можно перейти в режим Sources (Рис.4).
Режим Sources
Инструмент предоставит два окна с исходным кодом, в которых будет отображена взаимосвязь между начальным действием, как правило, выделением или инициализацией памяти, и конечным действием, которое привело к ошибке, например попытка чтения или записи. Какое именно их этих действий является ошибочным, инструмент знать не может, так как они определяются логикой приложения. Но нарушение корректности реализации логики будет продемонстрировано, как с помощью исходного кода, так и в окне Observations Relationships, где графически выстраивается связь между исходным и конечным событиями. При этом события помечаются различными цветами, чтобы легко было определить соответствующие окна исходного кода, описания событий и стеки вызовов функций. Стеки вызовов нужны для того, чтобы было легче ориентироваться, каким образом мы попали в ту или иную функцию, так как событие, приведшее к ошибке, далеко не всегда может оказаться в самом исследуемом приложении, а, например, в сторонней библиотеке. Найдя ошибку, совсем не обязательно переключаться Alt-Tab’ом в окно редактора Visual Studio и искать нужные файл исходника и строку. Достаточно двойного щелчка мышки на строке кода в окне Sources, и мы попадаем в редактор Visual Studio в нужное нам место.
Одной из полезных особенностей интерфейса Inspector’а является подавление диагностики уже известных ошибок. По каким-либо причинам, у нас может не быть необходимости исправлять ошибки определенного типа, или ошибки в определенных модулях, или в исходных файлах. Например, существует проблема так называемых false positives, то есть ложных диагностик, когда обнаруживается якобы ошибка работы с памятью, которая, на самом деле, таковой не является. Такие случаи возможны при использовании некоторых сторонних библиотек, или при реализации собственных пользовательских аллокаторов в программе. В таком случае можно, используя фильтры, добавить ошибку в список Private Suppressions, и при старте анализа использовать одну из опций: «Mark problems» или «Delete Problems». При этом ошибки будут либо помечены в списке, либо вообще не отображены. Для отмены подавления достаточно выбрать опцию «Do not use suppressions».
В заключение описания интерфейса полезно упомянуть о возможности использования инструмента в режиме command line. Основное его предназначение – автоматизация процесса тестирования, например, для регрессионных тестов в QA. Диагностики в этом случае формируются в текстовом режиме и легко могут быть обработаны с помощью скриптов. Встроенная система помощи содержит всю информацию относительно формата командной строки.
Типы обнаруживаемых ошибок
Memory Leaks. На первом уровне mi1 инструмент обнаруживает только «утечки памяти». Они возникают при выделении памяти в куче и неосвобождении ее по окончании программы. В списке Problems мы увидим ошибки Memory Leak, для которых, помимо обычной информации будет указан размер утечки. Если выделение потерянной памяти происходит несколько раз в одной и той же строке, например, в цикле, то диагностика выдаст суммарный результат. Если к этой строке осуществлялся доступ разными путями, то есть будут разные стеки, то диагностики будут отдельными.
Необходимо отметить, что анализ на уровне mi1 происходит гораздо быстрее, чем на других уровнях. Это связано с тем, что, несмотря на инструментацию, утилита Pin используется в режиме Probe mode. Probe mode – это метод, в котором используется внедрение «датчиков», jump-инструкций, только в начале определенных функций перед загрузкой образа исполняемого модуля. Перед внедрением кода «датчика», Pin замещает несколько первых инструкций кода функции на свои, и перенаправляет управление в обработчик. Длинна внедренного кода «датчика» на архитектуре IA-32 составляет 5 байт, на архитектуре Intel 64 – 7 байт. Поскольку и приложение, и обработчик выполняются практически без изменений (нет замещения кода всех инструкций), то производительность оказывается намного выше, чем в обычном режиме.
Pintool (mi1) дополняет функции выделения и освобождения памяти собственными, анализирующими функциями, с помощью Pin API. Отследив все вызовы выделения и освобождения памяти в куче, можно, сопоставив их, сделать вывод, какие из выделенных объектов памяти не были освобождены до завершения программы. Стоит только отметить, что если ссылка на выделенную в куче память будет сохранена в глобальном указателе, то ошибка «утечка памяти» сигнализирована не будет. Это отчасти является ограничением технологии, и на сегодняшний день анализ глобальных указателей не поддерживается. С помощью Windows API dbghelp.dll будут определены модули, имена функций, в которых память была выделена, и номера соответствующих строк кода.
Нахождение символов и строк кода, как и получение корректного стека, требует наличия отладочной информации для исполняемого кода. Поэтому крайне желательно включить ее генерирование во время компиляции и линковки с помощью ключей /Zi и /DEBUG. Оптимизирующий компилятор создаст дополнительные трудности на пути определения принадлежности собранных данных функциям и строкам, поэтому оптимизацию кода лучше отключить, используя ключ /Od. Если анализ приложения происходит в Debug-режиме, то эти опции включены в проекте Visual Studio по умолчанию. Какой именно С++ компилятор используется в Visual Studio, — Intel или Microsoft, особого значения не имеет.
Следующий уровень анализа mi2 способен обнаружить почти все оставшиеся типы ошибок доступа к памяти, выделенной в куче. Однако для ускорения анализа глубина стека выбрана равной единице. То есть на этом уровне мы сможем найти ответ на вопрос, есть ли в принципе ошибки в программе. А где эти ошибки, как мы попали в функции с ошибками, нам поможет определить следующий уровень mi3. Он отличается от предыдущего тем, что глубина стека функций увеличена до 12. Плюс добавлена возможность поиска утерянных указателей, которую мы рассмотрим позже.
Missing Allocation. Ошибки возникают при попытке освободить память по несуществующему адресу. Проще говоря, если будет случайно продублирован вызов функций free/delete или их аргумент указывает на несуществующую память, будет диагностирована ошибка. И если в Debug-режиме компилятор с включенной опцией run-time check найдет эту оплошность еще в процессе компиляции, в Release-режиме сообщение об ошибке выдано не будет.
Mismatched Allocation/Deallocation. Такие ошибки возникают при попытке освободить память с помощью функций, не соответствующих функции выделения памяти. Например, где-то «глубоко в недрах программы» выделяется какой-нибудь объект, допустим, буфер обмена или дескриптор, и конечный пользователь должен его освободить. При этом сам объект может быть выделен с помощью run-time функции malloc, но пользователь использует в своем C++ модуле функцию delete (Листинг 1).
char *pStr = (char*) malloc(16);
…
delete pStr;//Err:Mismatched Allocation/Deallocation
free(pStr);
free(pStr);// Err:Missing Allocation
В окне Details Inspector выдаст две цитаты кода с выделенными строками: в качестве Allocation Site — строку когда, где память была выделена в malloc, и Mismatched Allocation/Deallocation диагностику в месте, где вызвана delete-функция. Этим особенно хорошо иллюстрируется разница между событиями и проблемой. Несколько вполне корректных сами по себе событий, таких как аллокация и деаллокация памяти, составляют одну проблему из списка, который и представлен в окне Overview. А окно Observations Relationships указывает временную причинно-следственную связь между событиями. То есть в данном случае первична аллокация памяти, за которой последовала ее некорректная деаллокация.
Invalid Memory Access и Invalid Partial Memory Access. Ошибки возникают при чтении/записи по недействительным адресам памяти в куче или в стеке и по частично недействительным адресам памяти. В программах довольно часто встречается такая небезопасная функция копирования строк, как strcpy. В Листинге 2 представлен пример, где сделана попытка скопировать строку «my string» по уже несуществующему адресу.
char *pStr = (char*) malloc(16);
free(pStr);
strcpy(pStr, «my string»);//Err:Invalid Memory Access
char *pStr = (char*) malloc(16);
free(pStr);
char *pStr1 = (char*) malloc(16);
strcpy(pStr, «my string»);//Err:Invalid Memory Access
В результате работы Inspector’а мы можем получить несколько одинаковых ошибок Invalid Memory Access, ссылающихся на одну и ту же строчку кода. Возможно, это смутит пользователя, однако нужно понимать, что Inspector анализирует не исходный код, а исполняемый. И в данном случае, возможно, компилятор оптимизировал копирование строки, выполнив его несколькими инструкциями. Естественно, что все эти инструкции принадлежат одной и той же строке исходного кода. Это, кстати, одна из причин, почему лучше использовать Debug-режим для анализа приложения Inspector’ом.
Если в прошлом примере мы копировали строку в память, которая была недействительна, то сейчас мы рассмотрим более сложный случай – одна из самых коварных ошибок при работе с указателями и памятью (Листинг 2). Сначала мы выделяем буфер памяти с помощью malloc и сохраняем его адрес в указателе pStr. Затем сразу освобождаем ее и выделяем буфер такого же размера, но по указателю pStr1. Далее, копируем строку по старому указателю. В некоторых случаях, когда между выделениями памяти нет других операций с кучей, и при этом, когда программа выполняется в незагруженных другими приложениями системах, велика вероятность, что значения адреса в обоих указателях pStr и pStr1 совпадут, и ничего страшного в этот раз не произойдет. Но тем и коварна данная ошибка, что при переносе программы на реальную систему, например, у заказчика, приложение начнет падать, что совершенно недопустимо. Осталось отметить, что такой тип ошибки обнаруживается Inspector’ом только на уровне mi3 и выше, где включен дополнительный механизм поиска «утерянных» указателей. Естественно, расплатой за это станут дополнительные накладные расходы во время анализа.
Ошибка Invalid Partial Memory Access возникает, когда происходит доступ к составному объекту памяти, например, структуре, часть которого недействительна. В Листинге 3 представлен пример такого кода.
struct tally {
int num;
char count;
};
struct tally *pCurrent = (struct tally*)malloc(5); //incorrect size!
struct tally *pRoot = (struct tally*)malloc(sizeof(struct tally));
pCurrent->num = 1;
pCurrent->count = 1;
*pRoot= *pCurrent;//Err:Invalid Partial Memory Access
char array[10];
strcpy(array, «my string»);
int len = strlen(array);
while (array[len] != ‘Z’)//Will read from below the stack pointer
len—;
Здесь используется функция выделения памяти под структуру tally с явным указанием размера и с помощью sizeof, что более правильно. Далее, мы инициализируем поля одной из структур по указателю pCurrent единицами и копируем ее в структуру по указателю pRoot. Inspector диагностирует ошибку Invalid Partial Memory Access в строке копирования, при этом в диагностике ошибочной (Partial Invalid Read) будет названа структура по указателю pCurrent.
Давайте разберемся, в чем состоит ошибка. К сожалению, Inspector пока еще не может подсказать нам, что по умолчанию включена опция компилятора /Zp4, которая заставляет его выравнивать размеры структур до величины 4 байт. А значит, sizeof нашей структуры из переменных типа int и char составляет не 5, а 8 байт. То есть при копировании мы попытались прочитать 8 байт структуры по указателю pCurrent, в которой инициализированы, а значит, действительны, только первые 5 байт памяти.
В том же Листинге 3 представлен пример ошибки, которая может быть обнаружена только на уровне mi4. Этот уровень позволяет находить ошибки доступа к памяти, выделенной на стеке, которые, по какой-то причине, не обнаруживаются еще на стадии компиляции с помощью run-time chek опций компилятора. Для данного примера копирования символов строки «my string» в цикле Inspector выдаст диагностику чтения недействительной памяти в теле массива. Действительно, так как в данной строке нет символа ‘Z’, то на одиннадцатой итерации произойдет попытка чтения символа за границей выделенного в стеке участка памяти.
Uninitialized Memory Access и Uninitialized Partial Memory Access. Ошибки возникают при попытке чтения выделенной, то есть действительной, но неинициализированной памяти, в куче или в стеке. Простейший пример такой ошибки представлен в Листинге 4.
char *pStr = (char*) malloc(16);
char c = pStr[0]//Uninitialized Memory Access
struct person {
unsigned char age;
char firstInitial;
char middleInitial;
char lastInitial;
};
struct person *p1, *p2;
p1 = (struct person*) malloc(sizeof(struct person));
p2 = (struct person*) malloc(sizeof(struct person));
p1->firstInitial = ‘c’;
p1->lastInitial = ‘o’;
*p2 = *p1;//Uninitialized Partial Memory Read
Мы сделали попытку чтения первого символа из неинициализированной строки в переменную с. Как и в случае доступа к частично недействительной памяти, может быть ошибка доступа к частично неинициализированной памяти. Пример тоже со структурой. Если попытаться только скопировать частично проинициализированную структуру person, расположенную по указателю p1, в новую структуру по указателю p2, Inspector выдаст ошибку Uninitialized Partial Memory Read для строки копирования, при этом в качестве Allocation Site будет определена строка выделения памяти для структуры по указателю p1.
Заключение
Наверное, не имеет особого смысла объяснять последствия ошибок, вызванных некорректным использованием памяти, разработчикам программного обеспечения. Необходимость использования инструмента, подобного Inspector, в процессе разработки не вызывает сомнений. Вопрос в том, насколько этот инструмент удобен для использования, и насколько полно он покрывает возможные проблемы корректности исполнения программ. Разработчики Intel Parallel Inspector будут рады услышать мнения пользователей о продукте и обсудить те недостатки, которые еще есть в нем, на ISN форумах, как англоязычном, так и русскоязычном.
Аннотация: В лекции вводится понятие ошибки, рассматриваются основные типы ошибок в параллельных программах, проблемы поиска и устранения ошибок в параллельных программах. Характеризуются возможности инструментов отладки параллельных программ: Intel Thread Checker, Intel Parallel Inspector. Вводится понятие профилирования, критического пути, анализируются проблемы производительности, определяемые при помощи профилирования. Рассматриваются инструменты оценки эффективности параллельных программ: Intel Thread Profiler, Intel Parallel Amplifier.
Цель лекции: Лекция направлена на знакомство слушателей с ошибками, возникающими в параллельных программах с приведением способов их избегания и решения с помощью инструментов Intel.
6. Введение в отладку параллельных программ в системах с общей памятью
Ни для кого не секрет – наличие инструментов делает жизнь проще. Тезис
этот находит подтверждение как в повседневной жизни (чистить одежду удобнее и
эффективнее щеткой, чем руками, заворачивать гайки гораздо проще ключом, чем
пальцами), так и в профессиональной деятельности (кто сейчас возьмется строить,
ладно, даже не дом, пусть всего лишь баню, при помощи одного лишь топора?!).
Естественно, программисты – не исключение. Значение инструментов, облегчающих путь от анализа постановки задачи до получения решения, готового к внедрению, трудно переоценить. В данном кратком описании представлен инструмент
отладки параллельных программ, разработанный корпорацией Intel и носящий
название Intel® Thread Checker.
В п. 6.1 приводится назначение рассматриваемого инструмента, характеризуются области его возможного применения. В п. 6.2 дается краткая
характеристика принципов работы Intel® Thread Checker. В пп. 6.3 – 1.5 приводится информация, необходимая для подготовки пользовательского проекта и инструмента для
анализа. П. 6.6 посвящен вопросам сбора и анализа данных, полученных в результате работы Intel® Thread Checker. В п. 6.7 возможности инструмента рассмотрены
на простом примере, входящем в поставку Intel® Thread Checker.
6.1. Назначение Intel Thread Checker
Процесс отладки в общем случае можно разбить на следующие шаги:
- определение факта наличия ошибки;
- поиск (локализация) ошибки;
- выяснение причин ошибки;
- определение способа устранения ошибки;
- устранение ошибки.
Кажется, что на первом шаге никакой инструмент не требуется. Запускаем
программу и либо на некоторых исходных данных получаем неверные результаты,
либо обнаруживаем, что некоторая последовательность действий по использованию программы ведет к ее «падению» или «зависанию». Однако для параллельной
программы даже этот очевидный шаг может иметь существенную сложность. На
практике нередко встречаются ситуации, когда неработоспособность параллельной программы проявляется один раз на сотню и более запусков. Очевидно, в этом
случае инструментальная поддержка лишней не будет.
Назначение Intel® Thread Checker (ITC) – поиск мест с возможным недетер минированным поведением многопоточной программы, написанной как на основе
библиотеки потоков (Windows или POSIX threads), так и с использованием технологии OpenMP. Соответственно ITC может быть использован под операционными
системами как семейства Windows, так и семейства Linux. Принципы поиска ошибок рассмотрены ниже, здесь же укажем, что ITC неплохо справляется с задачей
обнаружения факта ошибки в программе, даже если эта ошибка в текущем варианте исполнения программы и не проявила себя.
Второй шаг – поиск ошибки – заключается в как можно более точной ее ло кализации; в идеале должна быть найдена переменная с неверным значением
и/или строка кода, ведущая к краху программы. Типичный метод работы в этом
пункте – использование режима трассировки в отладчике с наблюдением за состоянием переменных, регистров, стека вызова и т.д. «Плохая новость» – для много-
поточных программ режим трассировки практически неприменим, поскольку автоматически меняет характер их выполнения, а значит, скрывает места, которые
могут приводить к проблемам во время реальной работы. Кстати говоря, даже типичный способ локализации ошибки расстановкой операторов печати по тексту
программы в этом случае нужно использовать с большой осторожностью – печать
также вносит синхронизацию в выполнение программы.
Что же делать? Быть может, наилучшее из возможных решение реализовано в
ITC. ITC не есть привычный всем отладчик с режимами трассировки, наблюдения
и т.д. ITC выполняет анализ программы сам, без участия программиста, причем
анализируется не только выполненный «прогон» программы, а все возможные варианты ее выполнения. В результате выясняются и показываются программисту
места в программе, в которых содержатся ошибки (с той или иной долей вероятности, в большинстве случаев близкой к 100%).
Шаг третий – выяснение причин ошибки. Задача здесь – понять, почему
ошибка возникла. Отсюда во многих случаях автоматически вытекает способ ее
устранения (задача шага четвертого). Можно, конечно, выяснить условия, ведущие
к проявлению ошибки (некое сочетание значений переменных, например), и просто вставить в код заплатку именно для этого случая. Данный вариант мы здесь не
рассматриваем. На этом шаге ITC помогает тем, что каждое найденное им проблемное место сопровождает комментарием, содержащим тип ошибки: гонка дан-
ных, несинхронизированный доступ к переменной, тупик и т.д.
В результате мы получаем место потенциальной ошибки, переменную, с которой связана проблема, и описание ошибки. Остается лишь освоить типовые способы борьбы с типовыми ошибками – и значительная их часть будет находиться и
исправляться без грандиозных усилий.
Единственное, в чем ITC совсем не может помочь – это шаг пятый. Устранять
найденную ошибку все-таки придется программисту самостоятельно.
6.2. Возможности Intel Thread Checker
Согласно [2] ITC обнаруживает ошибки следующих видов: гонки данных
(data races), тупики (deadlocks), потоки в состоянии ожидания
(stalled threads), потерянные сигналы (lost signals), заброшенные
замки (abandoned locks).
Приведем краткое описание каждого вида.
-
Гонки данных. Возникают, когда несколько потоков работают с разделяемыми данными и конечный результат зависит от соотношения скоростей
потоков. Пусть, например, один поток выполняет над общей переменной x
операцию x = x + 3, а второй поток – операцию x = x + 5. Данные
операции для каждого потока фактически разбиваются на три отдельные
подоперации: считать x из памяти, увеличить x, записать x в память. В зависимости от взаимного порядка выполнения потоками
подопераций финальное значение переменной x может быть больше исходного на 3, 5 или
8. Гонка данных возможна и в случае, когда один поток пишет в перемен ную, а остальные только читают из нее. -
Тупики. Взаимная блокировка потоков, ожидающих наступления некоторого события для продолжения работы. Типичный пример тупика: нулевой поток занял для использования ресурс 1 и ожидает предоставления
ему ресурса 2, а первый поток занял ресурс 2 и ожидает предоставления ему ресурса 1. -
Потоки в состоянии ожидания. Одно из состояний потока в многозадачной операционной системе – ожидание. Поток переходит в него, когда для
продолжения выполнения ему требуется наступление некоторого внешнего события. Если пребывание потока в этом состоянии продолжается
слишком долго, ITC рапортует об ошибке типа stalled thread. Интервал
времени, по истечении которого выдается данная диагностика, может
быть задан в настройках ITC. -
Потерянные сигналы. Возникают, когда поток ожидает наступления некоторого события, произошедшего прежде, чем поток пришел в состояние
готовности к его приему и обработке. В результате поток никогда не сможет выйти из состояния ожидания. -
Заброшенные замки. Возникают в ситуации, когда поток захватил некоторый ресурс (критическую секцию, мьютекс) и был снят с выполнения по
той или иной причине. В результате ресурс не может быть освобожден. Если он требуется другому потоку, это приведет к бесконечному ожиданию.
6.3. Принцип сбора информации
Анализ программы, выполняемый ITC, основан на процедуре инструментации. Инструментация – вставка обращений к библиотеке ITC для записи дей-
ствий, потенциально способных привести к ошибкам: работа с памятью, вызовы
операций синхронизации и работа с потоками [2]. Может выполняться автоматически на уровне исполняемого модуля (а также dll-библиотеки) и/или по указанию
программиста на уровне исходного кода. Для достоверности получаемых результатов крайне желательно, чтобы во время сборки анализируемой программы была
выключена оптимизация (сборка в конфигурации debug необязательна).
В процессе анализа контролируются:
- доступ к памяти;
- операции синхронизации;
- операции создания потоков.
Необходимо отметить, что неисполняемые участки (невызываемые функции, ветки условных переходов и т.д.) никак не проверяются, то есть под анализ не
подпадают.
6.4. Подготовка программы для анализа
Использование отладчика Intel® Thread Checker возможно в двух режимах:
- Бинарная инструментация программы – осуществляется автоматически в момент запуска
Активности (Activity)1Активность – термин из среды VTune, в рамках которой работает ITC. Фактически
представляет собой «контейнер», содержащий, с одной стороны, настройки системы и параметры анализируемой программы, с другой – результаты проводимого анализа.
в проекте ITC. Рекомендуется в случае, если отсутствует доступ к исходным кодам или невозможна повторная сборка программы с нужными ITC настройками. - Компиляторная инструментация – при сборке анализируемой программы необходимо указать ключ компилятора /Qtcheck. Позволяет ITC
предоставить информацию о найденных ошибках с указанием имен переменных, с которыми эти ошибки связаны.
Сборка приложения для работы с ITC предполагает установку следующих
опций проекта (или настроек в make-файле):
- Компиляция потокобезопасного кода: -MT[d], -MD[d]. Данные опции автоматически устанавливаются при сборке в конфигурации debug. При
сборке в конфигурации release указанные опции необходимо устанавливать вручную. - Использование debug-опций: -Z[i,I,7], -Od. Замечание аналогично предыдущему пункту.
- Связывание с ключом /fixed:no. Необходимо указывать явно.
Дополнительно необходимо отметить, что при использовании ключа /Qtcheck в среде разработки (IDE) требуется указать путь к библиотекам ITC. Обычно этот путь имеет вид C:Program FilesIntelVTuneAnalyzerLib.
6.5. Создание проекта в Intel Thread Checker
Работа в ITC выполняется в рамках проекта. Для его создания используется
команда меню File →New Project. В главном окне мастера настройки проекта (см.
рис. 6.1) необходимо выполнить всего лишь два действия. Первое – указать исполняемый файл (Launch an application). Второе (необязательное) – указать аргументы командной строки.
Рекомендуется при анализе программы, с одной стороны, использовать типичные размеры обрабатываемых данных, с другой – задавать их так, чтобы программа «убиралась» в оперативную память с учетом накладных расходов ITC, которые могут быть довольно значительными.
Рис.
6.1.
Мастер настройки проекта в Intel Thread Checker (версия 3.0)
6.6. Сбор и анализ данных
После запуска в проекте ITC активности (при создании проекта это происходит автоматически) начинается инструментация исполняемого модуля, указанного для
анализа, и используемых им динамических библиотек. Затем модуль запускается и начинается процесс анализа. По завершении ITC формирует окно с информацией о
найденных ошибках и подозрительных местах. Возможный его вид указан на рис. 6.2.
Рис.
6.2.
Результат анализа – список диагностики
В случае повторного запуска активности необходимо использовать один из следующих вариантов: 1) выбрать пункт меню Activity → Run; 2) нажать F5;
3) нажать кнопку 
По каждой диагностике, выданной ITC, в случае если сборка выполнялась с приведенными в пункте 1.4 настройками, может быть получена дополнительная
информация (см. пункт 1.7), вид которой показан на рис. 6.3.
Рис.
6.3.
Результат анализа – диагностика в исходном коде
| Intel Parallel Inspector | |
|---|---|
| Тип | Профилировщик / Отладчик использования памяти |
| Разработчик | Intel Developer Products |
| Операционная система | Windows, Linux |
| Лицензия | Проприетарная, однопользовательская |
| Сайт | software.intel.com/en-us… |
Intel Parallel Inspector — анализатор корректности с возможностью проверки работы с памятью и потоками. Имеет отдельный пользовательский интерфейс, а также встраивается в Microsoft Visual Studio. Parallel Inspector помогает повысить надежность, защищенность и точность приложений, написанных на языках Си/C++ и Fortran.
Обзор
- Надежность: Поиск взаимных блокировок (deadlocks) и ошибок работы с памятью, приводящих к сбоям в работе программы
- Защищенность: Поиск уязвимостей в использовании памяти и потоков, которыми могут воспользоваться хакеры
- Точность: Обнаружение повреждения памяти и состояния гонки для устранения ошибочных результатов
Проверка памяти включает в себя проверку утечки памяти, повисшие указатели, переменные без инициализации, использование некорректных ссылок на участки памяти, несовпадающие размеры памяти с вычисленными ранее, выделение и освобождение памяти, проверки стеков в памяти, а также исследование стеков с управляемой глубиной.
Проверки потоков включают в себя проверки состояний гонки, взаимных блокировок, анализ стека вызовов с настраиваемой глубиной, руководство по диагностике, встроенная поддержка Intel Threading Building Blocks, OpenMP и потоков Windows.
Программа доступна как часть Intel Parallel Studio или в виде отдельного продукта.
Недостатки
Работа Intel Parallel Inspector основана на отслеживании вызовов Windows API, выполняемых исследуемой программой. Однако, Intel Parallel Inspector отслеживает лишь официально задокументированные WinAPI-вызовы, тогда как в случае использования недокументированных вызовов Intel Parallel Inspector их не сможет корректно обработать. В этом случае возможно как ложное обнаружение ошибки, так и игнорирование реально существующей ошибки.
Ссылки
- Сайты и ресурсы
- Домашняя страница Intel Parallel Studio (англ.)
- Домашняя страница Intel Parallel Inspector (англ.)
- Статьи
- Секреты использования Intel Parallel Inspector для поиска ошибок многопоточности (рус.)
См. также
- Отладчик использования памяти
- Утечка памяти
Intel® Inspector XE – инструмент динамического анализа корректности кода, т.е. анализа исполняемого процесса. Inspector XE предназначен для поиска ошибок памяти и проблем, возникающих при взаимодействии потоков, в последовательных и многопоточных приложениях. Инспектировать можно код, написанный на C, C++, C# и Fortran.
Intel® Inspector XE позволяет запустить удаленный анализ на вычислительном узле кластера и просмотреть результаты сбора данных на хосте. Для сбора данных на кластере используется интерфейс командной строки inspxe-cl (Command Line Interface Support). Для просмотра результатов удобно использовать графический интерфейс (GUI), который вызывается командой inspxe-gui.
- Подготовка к запуску
- Запуск Intel Inspector на вычислительном узле
- Выдача результатов анализа
Подготовка к запуску
1. Intel Inspector XE проверяет весь исполненный код и не требует специальной перекомпиляции программ. Тем не менее рекомендуется включать в сборку приложения (его отладочной версии с -O0 или релиза) опцию -g для обеспечения связи исполняемого кода с исходным текстом.
Рекомендуется работать в окружении, устанавливаемом командой
mpiset 8
2. Для работы инспектора следует установить Intel Inspector XE окружение, выполнив команду:
source /opt/intel/inspector/inspxe-vars.sh
Примечание.
1. Поиск ошибок памяти (утечки, доступ к неинициализированной памяти и др.) связан с реально возникающими проблемами, поэтому рекомендуется использовать полный набор данных.
2. При поиске ошибок многопоточности (гонки данных, взаимные блокировки и др.) инспектор ищет потенциальные проблемы взаимодействия потоков, поэтому нагрузку на приложение для экономии времени лучше снизить, например уменьшить размер входных данных. При этом в процессе тестирования надо стремиться активизировать разные ветви кода, поскольку будет проанализирован только исполненный код. Так, взаимная блокировка будет найдена, даже если существует только потенциальная возможность её появления в исполнившихся ветках кода. Вот почему проверка потоков иногда сообщает об ошибках в программе, даже если программа выдает (имеет) правильный вывод.
Запуск Intel Inspector на вычислительном узле
Выполнение анализа инициируется командой inspxe-cl с указанием действия collect (сбор данных). Запустить её на кластере можно с помощью команд srun, sbatch или mqrun (см. Запуск задач на кластере). Например для последовательной программы (-n 1):
mqrun -n 1 inspxe-cl -collect mi1 -search-dir all:r=../ -- ./app
где
действие -collect mi1 означает собрать данные об ошибках памяти с учетом типа анализа mi1;
опция -search-dir all:r=../ задает родительский каталог в качестве начального для рекурсивного (r) поиска всех (all) типов файлов (bin, src, sym);app – исполняемый файл приложения в текущем рабочем каталоге (./).
Основные типы анализа:mi1 Detect Leaksmi2 Detect Memory Problemsmi3 Locate Memory Problemsti1 Detect Deadlocksti2 Detect Deadlocks and Data Racesti3 Locate Deadlocks and Data Races
Рекомендуется начать с первого уровня анализа (mi1, затем ti1), который характеризуется очень низкими накладными расходами (см. Collecting Result Data from the Command Line).
Замечания.
1. Для доступа к текущей документации о команде inspxe-cl следует набрать
inspxe-cl -help
Уточнить информацию о действии collect позволяет команда
inspxe-cl -help collect
2. Можно получить копию команды inspxe-cl -collect, которая будет выполнена в графическом интерфейсе Intel Inspector. Для этого, после вызова GUI:
inspxe-gui &
создать, например, проект (выполнив New Project…), затем в главном окне выбрать New Analysis…, в открывшемся окне Configure Analysis Type выбрать тип анализа (например, Detect Leaks) и, нажав на кнопку Get Command Line, получить соответствующую команду.
Выдача результатов анализа
Собранные данные по умолчанию записываются в результирующий каталог с именем вида r@@@{at}, где
@@@ — следующий доступный номер для результата в текущем рабочем каталоге, 000 – начальный номер;
{at} – код типа анализа (analysis type).
Результирующий каталог, например r000mi1, содержит результирующий файл с тем же именем и расширением inspxe, например r000mi1.inspxe, а также краткий (суммарный, Summary) отчет в файле inspxe-cl.txt.
Краткий отчет включает общее число проблем, их типы, время старта и окончания анализа.
Замечание. Используя опцию -result-dir в команде inspxe-cl -collect, можно задать свой результирующий каталог. При этом к его имени можно приписать до 5-и знаков @. Это удобно в скриптах: не надо будет обновлять имя каталога каждый раз.
После каждого анализа следует посмотреть краткий отчет в файле inspxe-cl.txt. Если обнаружены проблемы, то можно открыть результат для визуализации в GUI или использовать команду inspxe-cl -report для генерации отчетов одного или более типов из полученного результата. Подробнее о команде см.:
inspxe-cl -help report
Для просмотра результатов проведенного анализа в GUI можно выполнить команду inspxe-gui с указанием результирующего каталога, например:
inspxe-gui r000mi1 &
или выполнить вызов GUI:
inspxe-gui &
и в открывшемся окне, выбрав Open Result, перейти в каталог с результатами анализа и открыть соответствующий файл, например файл r000mi1.inspxe в каталоге r000mi1.







