Потребность решения сложных прикладных задач с большим объемом вычислений и принципиальная ограниченность максимального быстродействия «классических» – по схеме фон Неймана – ЭВМ привели к появлению многопроцессорных вычислительных систем (МВС) или суперкомпьютеров.
Широкое распространение параллельные вычисления приобрели с переходом компьютерной индустрии на массовый выпуск многоядерных процессоров с векторными расширениями. В настоящие время практически все устройства – от карманных гаджетов и до самых мощных суперкомпьютеров – оснащены многоядерными процессорами. И если вы пишите последовательную программу, не применив распределение работы между разными ядрами центрального процессора и не проведя векторизацию, то вы используете только часть вычислительных возможностей центрального процессора.
Пройдя этот курс, вы познакомитесь с основными архитектурами МВС, с двумя стандартами (OpenMP и MPI), позволяющими писать параллельные программы для систем с общей и распределенной памятью. На простых примерах будут разобраны основные конструкции и способы распределения работы. Выполнение практических заданий позволит вам приобрести практические навыки создания параллельных программ. Курс будет интересен всем, кто занимается программированием.
Для участия в курсе слушателю необходимо иметь базовые знания по программированию с использованием С/С++.
Курс состоит из 9 недель. Каждая неделя курса содержит видеолекции, а также проверочные задания. Сертификат получают слушатели, набравшие более 80 % от максимально возможного количества баллов. При этом итоговый результат, представленный как 100 %, складывается из следующих составляющих: тесты 1–5 недели дают 4 %, тесты 6–9 недели дают 5 %, все практические задания дают 10 %, кроме итогового практического задания по OpenMP, которое дает 20 %.
From the lesson
Анализ и оптимизация программ с использованием современных программных пакетов
Вот вы и добрались до пятой недели курса! На этой неделе мы с вами рассмотрим основные опции компилятора Intel и то, как можно попробовать автоматически распараллелить программу. Мы также изучим основные возможности программного пакета Intel Parallel Studio, который упрощает и помогает создавать параллельные программы.
Николай Николаевич Богословский (Nikolay N. Bogoslovskiy)
Кандидат физико-математических наук, доцент (Сandidate of Physics and Mathematics, Associate Professor) Кафедра вычислительной математики и компьютерного моделирования ММФ (Department of Calculus Mathematics and Computer Modelling, Mechanics and Mathematics Faculty)
Евгений Александрович Данилкин (Evgeniy A. Danilkin)
Кандидат физико-математических наук, доцент (Сandidate of Physics and Mathematics, Associate Professor) Кафедра вычислительной математики и компьютерного моделирования ММФ (Department of Calculus Mathematics and Computer Modelling, Mechanics and Mathematics Faculty)
[МУЗЫКА]
[МУЗЫКА]
Приветствую вас.
В этой лекции мы рассмотрим инструмент для анализа работы ошибок с памятью
Intel Parallel Inspector, который входит в программный продукт Intel Parallel Studio.
Данный инструмент тесно интегрируется с Microsoft Visual Studio,
что облегчает работу.
Но его также можно использовать и отдельно.
Этот инструмент предназначен для анализа многопоточных
приложений и выявления ошибок работы с памятью.
Его функционал позволяет также использовать его из командной строки,
что позволяет автоматизировать процесс тестирования и проверки.
Но удобный графический интерфейс позволяет его использовать и в повседневной жизни,
что и делают многие программисты,
а анализ уже готовых программных продуктов позволяет открыть глаза на существующие
ошибки в данном коде и по новой взглянуть на программный продукт.
Давайте теперь рассмотрим особенности использования Inspector.
В нем есть три типа анализа.
Первый тип анализа — Memory Error Analysis.
Данный тип анализа позволяет найти ошибки, связанные с доступом к памяти,
утечки памяти и другие ошибки.
Второй тип анализа — Threading Error Analysis.
Данный тип анализа предназначен для анализа ошибок многопоточных приложений.
Это такие ошибки, как гонка за данными либо взаимная блокировка.
И третий вид анализа — это Custom Analysis, который пользователь настраивает
самостоятельно на основе базовых типов анализа, выбирая те или иные опции.
Но прежде чем перейти к непосредственному рассмотрению данного инструмента,
я кратко затрону теоретические аспекты работы Inspector.
Как и многие другие инструменты анализа корректности работы программы,
Inspector не анализирует синтаксис исходного кода программы,
а работает с бинарным кодом, выполняемым в операционной системе.
Для анализа на лету Inspector использует специальную технику внедрения внутрь
процесса и анализа вызова системных функций и функций многопоточности и
работы с памятью.
Соответственно, Inspector анализирует только те участки кода,
которые непосредственно выполняются.
Отсюда вытекает первый совет: для того чтобы эффективно проанализировать вашу
программу, отбирайте такие сценарии,
которые охватывали бы бо́льшую часть вашего исходного кода.
Ввиду того, что инструмент не имеет априорной информации о количестве
создаваемых потоков, о функциях, вызываемых для синхронизации данных,
о модели данных, о том как осуществляется доступ,
Inspector строит свою собственную динамическую модель исполнения программы.
Основными элементами динамической модели являются события.
Событиями являются вызовы системных функций или факт обращения к общей
переменной.
Системных функций, например, в операционной системе Windows более 170,
и не факт, что все они документированы.
Поэтому инструмент Inspector не может отследить полностью все события и
проследить все связи.
Поэтому иногда возникают False Positive и False Negative ошибки.
То есть это ложное обнаружение ошибки и ложное необнаружение ошибки.
В первом случае система показывает ошибку, но реально ее нет.
Во втором случае ошибка, имеющаяся в программе, может быть не обнаружена.
Отсюда вытекает второй совет: когда вы проверяете свою программу,
будьте внимательны — некоторые ошибки могут быть ложными, и второе — не факт,
что все ошибки в вашей программе обнаружены.
Давайте перейдем к непосредственному рассмотрению данного инструмента и сделаем
это на практических примерах.
Весь исходный код вы можете скачать в дополнительных материалах.
Эти примеры взяты из учебных материалов компании Intel.
Воспользуемся первым типом анализа,
это первый проект: find_and_fix_memory_error_analysis.
Сделаем его стартовым и исходный файл данного проекта.
Как видим, он не очень маленький.
Также много есть вспомогательных файлов.
Попробуем запустить данное приложение.
Итак, мы используем компилятор Intel, скомпилировалось, так,
произошла отрисовка картинки.
Вроде бы все замечательно, приложение работает.
Но давайте теперь попробуем проверить наше приложение на наличие ошибок,
воспользовавшись Inspector.
Выбираем «новый анализ», и здесь мы можем выбрать Memory Error Analysis,
Threading Error Analysis или настроить свой собственный тип анализа.
Также нам позволено выбирать уровень анализа.
Самый минимальный уровень находит утечки памяти, соответственно, работает быстро.
Ну и самый максимальный уровень работает дольше,
но находит большее количество ошибок.
Так как у нас приложение не очень большое,
поэтому выберем сразу максимальный уровень и запустим на выполнение.
Есть еще ниже дополнительные опции, которые вы можете выбирать при анализе.
Итак, запускаем анализ.
Наше приложение запускается,
и производится динамический анализ работы с памятью.
В интерактивном режиме мы можем видеть, сколько наше приложение потребляет памяти,
во вкладочке Summary также видеть дополнительную информацию.
Как видите, уже были найдены некоторые ошибки.
Итак, приложение наше отработало,
производится теперь анализ собранной информации,
и после этого в окне Summary вся эта информация в удобном виде предоставляется.
Первое окно Problems в виде To Do листа сделано,
и мы видим найденные проблемы.
Мы можем по ним переходить.
Итак, представлен здесь тип, исходный файл,
в котором произошла данная ошибка, и модуль.
Ниже представлен исходный код.
Давайте первую ошибку разберем.
Это Mismatched allocation/deallocation,
то есть неправильное выделение либо освобождение памяти.
Здесь мы видим: строчка, в которой произошло неправильное освобождение
памяти, и строчка кода исходного, где эта память была выделена.
Нажмем дважды и мы перейдем в развернутое окно исходного кода.
Здесь мы можем проанализировать исходный код,
а также посмотреть на стек вызова функции.
Итак, здесь память освобождается, а вот здесь она выделяется.
Для выделения используется оператор new, а для освобождения функции free.
Это совершенно неверно.
Поэтому дважды нажав, перейдем в исходный код и исправим.
То есть если память освобождалась с помощью оператора new,
то с помощью оператора delete она должна быть освобождена.
Исправили первую ошибку.
Давайте посмотрим на вторую ошибку — утечка памяти.
То есть мы выделили где-то память, но ее не освободили.
Итак, строчка с выделением памяти у нас подсвечена.
Опять же дважды нажимаем и переходим в исходный код.
Итак, вот здесь у нас выделена была память, это функция operator.
Дальше можно проанализировать, посмотреть как мы работали с переменной local_mbox.
И мы видим, что после того,
как функция завершает свое выполнение, память не освобождается.
Итак, мы должны освободить.
Для этого в строчку 180 мы вставляем код, освобождающий данную память.
Переходим назад, и у нас есть третий тип ошибки —
Invalid memory access, то есть неправильный доступ к памяти.
Итак, опять же здесь представлена информация о том, где произошла ошибка.
То есть в этой строчке подсвечено «произошла ошибка: неправильный
доступ к памяти».
А ниже представлена строчка, где была выделена память под local_mbox.
Итак, переходим в эту строчку, опять же стек вызовов.
Здесь для анализа он нам не нужен, в принципе,
все происходит в одной функции, перейдем в исходный код.
Итак, размер
массива local_mbox — mboxsize,
а цикл у нас идет от 0 до i, меньше или равное количеству элементов,
что, конечно же, неправильно.
Мы должны это исправить, то есть i должно быть меньше,
чем mboxsize, поделенное на размер элемента.
Давайте теперь сохраним и проведем
еще раз анализ.
В таком же уровне запускаем наш проект.
Итак, вышло сообщение, что мы забыли пересобрать наш проект.
Давайте мы полностью пересоберем наш проект после исправлений.
Так, проект был пересобран, отлично.
Никаких ошибок нет.
Все замечательно.
И теперь Еще раз нажимаем Start.
Так, приложение было запущено, опять проводится динамический анализ того,
как наше приложение работает с памятью.
Давайте ускорим видео!
Итак, что увидим в итоге?
Все ошибки, которые были найдены, мы исправили.
Все замечательно.
А это предупреждение.
Эти файлы не относятся к нашему проекту.
В принципе, вы можете посмотреть, что там в них все корректно.
Просто всей дополнительной информации вот,
так как это не относящиеся файлы к нашему проекту, у Inspector'a не было.
Как видите, данный тип анализа позволил нам найти ошибки в программе,
связанные с утечкой памяти и выходом за границы массива при работе с ним.
Следующий тип анализа позволит нам выявить ошибки многопоточных программ.
Давайте выберем Find and
Fix Threading Error Analysis проект, сделаем его стартовым.
Так, закроем исходные код и запустим наше приложение.
Итак, оно выполнилось.
Не знаю, видно у вас на видео или нет будет.
Но, сделав это самостоятельно, вы увидите,
что на картинке есть артефакты – неправильно отрисованные точки.
Это потенциально для данного приложения означает,
что есть какие-то ошибки по работе, по работе с памятью.
Итак, откроем исходный код.
Ну если его пролистать, я думаю,
будет тяжело сразу оценить и найти, где же произошла ошибка.
Давайте воспользуемся Inspector'ом и соответствующим типом анализа.
Итак, выберем Threading Error Analysis и выставим второй уровень для анализа.
Здесь также как и при анализе Memory Error Analysis можно выбрать 3 разных уровня.
Соответственно, на первом будет обнаружение deadlock'ов,
на втором – deadlock'ов и datarace'ов и их,
и на третьем уровне еще будет локализовано их размещение, и стек вызова будет глубже.
Итак, давайте запустим анализ.
Опять же, запускается наше приложение.
В интерактивном режиме мы, опять же, можем видеть,
сколько потребляет наше приложение, что происходит, как происходит анализ.
Итак, видим, уже вот найдены были ошибки.
Так, наше приложение отработало.
Теперь ожидаем завершения анализа.
Вся информация обрабатывается и сейчас будет представлена нам в удобном виде.
Итак, была найдена 1 ошибка.
Здесь, как и в Memory Error Analysis,
информация представлена в аналогичном виде: тип ошибки,
исходный файл, где произошли ошибки.
Итак, гонка данных у нас произошла.
Давайте попробуем проанализировать.
То есть вот здесь у нас произошло одновременное возвращение и запись.
Итак, дважды на нее нажмем.
Итак, это функция color trace.
И здесь, то есть одновременно, значит, 2 потока попытались что-то сделать,
выполнить эту функцию и сделать возврат в shader.
Так, давайте перейдем в исходный код и попробуем понять: а
где же у нас данная функция вызывалась.
Так, найдем, где у нас происходил вызов.
Так, это не относится к нашей.
Так, 104-я.
[КАШЛЯЕТ] Так,
105-я строчка кода.
Итак, вот здесь в нашем исходном файле эта функция вызывалась.
Здесь произошла ошибка.
То есть это означает, что функции выполнялись там независимо.
Тут все были локальные переменные.
А здесь – переменная call.
В нее мы попытались одновременно, видимо, записать.
Здесь комментарии даны.
Конечно, когда это чужое приложение, вам трудно будет сделать быстрый анализ.
Но когда это ваше приложение, вы понимаете, что там происходит.
Итак, что произошло?
У нас call объявлена как глобальная переменная.
Она является, соответственно, общая для всех потоков,
которые вызвали функцию render_one_pixel, то есть мы распараллеливали за счет того,
что потоки рендерят разные пиксели.
И, соответственно, они пытались сюда записать.
Поэтому эту переменную нужно сделать приватной.
Для этого комментируем, [НЕРАЗБОРЧИВО] здесь общий.
И раскомментируем вот эту строчку 89-ю, где она объявлена внутри функции.
И, соответственно, при вызове этой функции она будет локальной для каждого потока.
Итак, сохраняем.
Попробуем теперь запустить приложение.
Итак, собирается.
Как видите, картинка стала чистой и правильной.
Ну и теперь давайте еще раз проверим,
выполним анализ нашего
приложения, ускорим видео.
Итак, как вы видите,
приложение было проанализировано, и проблем не было найдено.
Данный инструмент очень полезен, особенно при проверке очень больших программ,
которые пишутся, как правило, не одним человеком, и вручную проконтролировать всю
правильность работы программы просто невозможно.
В следующей лекции мы рассмотрим еще один инструмент от компании Intel,
который позволяет проанализировать код и выявить те участки кода,
которые наиболее часто исполняются,
а также оценить масштабируемость программы и провести другие анализы.