[МУЗЫКА] [МУЗЫКА] [МУЗЫКА] Здравствуйте! В этой лекции я кратко расскажу об инструменте Intel Parallel Amplifier. Это профилировщик многопоточных программ, входящих в набор программного продукта Intel Parallel Studio. Этот инструмент позволяет найти те участки кода, которые наиболее часто исполняются на процессоре. Также он позволяет оценить масштабируемость вашего параллельного приложения. И если есть какие-то проблемы с масштабируемостью, то найти те участки кода, которые этой масштабируемости мешают. Сегодня с выходом новых многоядерных архитектур хорошая масштабируемость является тем фактором, который обеспечивает все время программе нужную эффективность. Поэтому инструмент Amplifier нужно применять для создания эффективных параллельных приложений. Методология оптимизации параллельных приложений заключается в выполнении трех этапов, которые позволяют оценить производительность и эффективность параллельной программы. Для более легкого понимания этих трех этапов в инструменте Amplifier имеется три базовых типа анализа. Первый тип анализа — Hotspot-анализ. Данный анализ позволяет ответить на вопрос «На что моя программа больше всего тратит времени?» То есть найти те участки кода Hotspot-функции, которые наиболее часто исполняются. Благодаря этому мы знаем, где нам нужно провести распараллеливание или провести оценку и анализ. Следующий тип анализа — это Concurrency-анализ. Данный тип анализа позволяет ответить на вопрос «Насколько хорошо моя программа параллелится?» Благодаря этому анализу мы можем оценить, насколько хорошо масштабируется и возможно ли, что когда у нас станет архитектура восьмиядерная, мы получим прирост эффективности. И третий тип анализа — это Lock & Wait Analysis. Если у нас имеются проблемы с масштабируемостью, то данный типа анализа позволяет оценить и выявить те участки кода, где у нас происходит задержка, где, возможно, у нас имеется излишняя синхронизация. И если мы видим такие участки кода, нам нужно изменить или, возможно, пересмотреть всю архитектуру или часть архитектуры нашего приложения. Ну а теперь давайте на двух практических примерах рассмотрим, как данный инструмент применять. Все исходные коды вы можете взять в дополнительных материалах нашего курса. И первый пример основан на рендеринге картинки. Итак, мы рассмотрим, как применять Amplifier, для того чтобы проанализировать приложение. Представим ситуацию, что вам дали некое приложение и просят повысить его производительность. Для того чтобы проанализировать весь код и понять, где дольше всего работает программа, может уйти много времени, это сделать тяжело. Для этого воспользуемся инструментом, анализом в «амплифайере» — — Basic Hotspot Analysis. Итак, делаем наш проект find_hotspots основным исполняемым. Так... Теперь на всякий случай я его еще раз пересоберу. И дальше воспользуюсь Basic Hotspot Analysis. Если его у вас здесь не появится, просто нажимаем New Analysis и здесь во вкладке «Алгоритм анализа» собираем его. Как видите, анализов достаточно много, мы рассмотрим только три бата. Итак, запускаем анализ. Наше приложение запускается, оно пока однопоточное, сейчас приложение отработает. А соответственно, Amplifier собирает всю информацию. Итак, приложение отработало. Давайте посмотрим, где же наиболее часто выполнялась наша программа. Итак, общая суммарная информация. Итак, время у нас 15, почти 16 секунд. И здесь мы видим Top Hotspots, то есть функция, на которую ушло больше всего времени выполнения. Это функция инициализации инициализации двумерного буфера. Во вкладочках Bottom_up вся эта информация представляется в более удобном виде. Мы видим функции и их названия, а также время, затраченное на их выполнение. Они здесь отсортированы в порядке убывания. Также видим, соответственно, в каком модуле: то есть эта функция из нашего приложения, эта — из библиотеки. Далее видим саму функцию и, соответственно, исходный файл, в котором эта функция находится. Всю эту информацию мы можем сгруппировать по-другому. Таких видов группировки здесь достаточно много, и вы можете выбирать ту, которая вам удобна для представления информации и анализа ее. Также здесь при выборе соответствующей функции справа представляется весь Stack вызова, то есть то, как мы в эту функцию попали. Есть другие виды представления той же самой информации, то есть кто вызывал, где вызывал, и Top-down-tree, то есть здесь мы прям можем видеть всё дерево вызова, Stack-вызова. И, соответственно, что и внутри тоже происходило. Итак, вернемся в Bottom-up и дважды нажмем на самую первую функцию, на которую ушло больше всего времени. И мы попадем в исходный код. Здесь подсвечено, какие строчки, сколько времени выполнялись. Это неточное время, так как трудно соотнести строчки исходного кода и те места бинарные, которые выполнялись. Поэтому иногда информация отображается так, что, допустим, у вас здесь на цикле было основное время. Но благодаря этому, в принципе, вы можете оценить в общем блок кода, который дольше всего выполнялся. Давайте теперь посмотрим, перейдем непосредственно в исходный файл, уже попытаемся проанализировать, что здесь можно сделать. Инструмент сам ничего не исправит, он только вам является помощником и подсказчиком, где больше всего выполнялась ваша программа, где дольше всего находились. Итак, видим, что здесь происходит заполнение массива mem_array. Какой-то программист не очень удачно написал здесь заполнение этого массива. Происходят постоянные скачки между элементами массива — если в режиме дебатов вы попробуете это сделать, проанализировать, то увидите. Соответственно, плохо происходит предсказание того, какие элементы будут использоваться, и что дальше нужно будет выполнить. Нужно это исправить. Давайте закомментируем данный участок кода. Ниже уже представлено готовое решение. В принципе, у нас обычный массив — его просто нужно заполнить значением. Поэтому проходим по нему, как по обычному массиву. Итак, давайте сохраним, соберем наше решение и теперь еще раз запустим анализ. Даже уже на глаз видно, что приложение стало работать быстрее. Итак, сравним теперь результаты, дождемся анализа, завершения анализа. Итак, видим, что время составило 6,6 секунды. До этого у нас было 15 секунд. Итак, для того чтобы сравнить между собой несколько результатов расчета, есть удобный инструмент. Нажимаем правой кнопкой на соответствующем анализе и пытаемся сравнить. Итак, мы сравним предыдущий анализ и текущий. Итак, как у нас записано здесь имя, давайте поясню: r — это константа, 002 — это номер анализа, и hs обозначает, что это Hotspot-анализ. Они здесь собираются все в одну папку. Так, попытаемся сравнить. Если вдруг вы забыли закрыть, то будет вот такая ошибка. Нужно все анализы здесь закрыть. Давайте еще раз их сравним. Выберем, так, второй, третий и сравниваем их. И информация представлена в более удобном виде уже. Мы видим разницу во времени, ну и, соответственно, другие характеристики. Как видите, вот до этого он занимал 6 секунд, после изменений — неизвестно, то есть она занимает очень мало времени, данная функция. То есть практически мы свели время ее выполнения к нулю. Давайте теперь разберемся со вторым видом анализа. Итак, отлично, мы исправили какое-то место, можно дальше конечно же код анализировать и пытаться его ускорить. Но, предположим, мы провели распараллеливание. Итак, выбираем проект analysis_lock. Делаем его стартовым, при этом неважно, с помощью какой технологии вы провели распараллеливание, — проанализировать можно абсолютно любые приложения. Итак, давайте запустим... то есть у нас параллельные приложения. Попробуем оценить, какие есть задержки и есть ли они у нас в программе. Итак, запускаем. Программа, видите, она работает параллельно, каждая нить отрисовывает свою строчку в изображении. Так, дождемся окончания анализа. Итак, посмотрим. Общее время: 6,3 секунд, и у нас есть white count, то есть счетчик количества ожиданий, то есть где наше приложение простаивало, где потоки простаивали, и spin time, то есть время, показывающее, сколько было потрачено времени на синхронизацию. Видим, что она подсвечивается красным, метрика подсчитала, что это достаточно много для данного приложения. Теперь посмотрим следующее: top white objects. Здесь представлены объекты, в которых программа наиболее часто простаивала. Итак, нас интересуют эти объекты, это критическая секция, по сути, в ней больше всего было затрачено времени. Нажав на него, мы попадаем в окно Bottom Up, здесь видим, что есть эта критическая секция, представлено время, оно раскрашивается, то есть на что было потрачено, здесь на ожидание. Представлена дополнительная служебная информация, которая позволит проанализировать, где это произошло. А также внизу представлена информация по нитям, то есть как они исполнялись. Как видите, много времени — вот красным здесь представлено spin time — на какую-то синхронизацию было потрачено. Итак, смотрим, что в этой критической секции. Это функция petreet metrics locks. Давайте дважды на нее нажмем, посмотрим, да, действительно, есть такая функция, в ней вот здесь много было потрачено время на ожидание. Итак, развернем, следующий уровень посмотрим, смотрим, где она вызывалась. Итак, это было draw task operator. Нажимаем дважды и видим, что в этом исходном коде, вот здесь много провели потоки в ожидании. Перейдем в исходный код. Итак, видим, что здесь перед выполнением этого цикла ставится замок на переменную m_rgb_mutex, и после завершения она освобождается. Встает вопрос, нужно ли здесь нитям ожидать, то есть они в этой критической секции находятся, один поток, как только он завершил выполнение, сюда попадает второй поток и так далее. В данном случае каждый пиксель отрисовывается независимо, поэтому нет смысла здесь устраивать критическую секцию. Давайте мы — то есть какой-то программист неудачно реализовал — закомментируем, сохраним, скомпилируем приложение и проведем опять анализ. Как видите, быстро отработало наше приложение. Сравним результат. Итак, как итог: у нас стало чуть больше, чем в 2 раза быстрее выполняться наше приложение, ну и здесь представлены, соответственно, объекты, которые также ожидают. Здесь можно их проанализировать, но это уже служебные объекты, которые реализуют синхронизацию технологии TBB, которая использовалась для распараллеливания. То есть здесь мы уже сделать ничего больше не можем. Как видите, 2 этих типа анализа позволили существенно ускорить наше приложение. Если у нас оно изначально выполнялось тут 16 секунд, то после проведенного анализа [ПАУЗА] это время сократилось до 2,6 секунды. >> В этом примере мы рассмотрели два типа анализа: hotspot анализ, который позволил найти тот участок кода, который наиболее часто исполнялся, и lock and white анализ, который позволил удалить лишнюю синхронизацию и повысить эффективность нашей программы. Ну а теперь давайте рассмотрим еще один пример. В нем реализован алгоритм разложения числа на простые множители. Давайте на примере разберем, как работает алгоритм. Допустим, что необходимо определить простые множители числа 12. Для этого перебираются числа меньше 12, начиная с 2, и выполняется последовательно операция деления. 12 делится на 2, получается 6. Остаток равен 0. Пробуем делить еще раз на 2. 6 делим на 2, получаем 3. Остаток равен 0. Пробуем делить еще раз на 2. 3 делим на 2, получается 1,5. Остаток отличен от 0. Соответственно, 2 запоминаем как простой делитель и рассматриваем следующее число меньше 12. Это 3. 3 делим на 3. Получается 1. Остаток равен 0. А частное равно 1. Значит, запоминаем число 3, и алгоритм останавливает свою работу, так как получилось частное деление 1. В программе ищутся простые множители первых ста тысяч натуральных чисел. Давайте теперь попробуем применить Amplifier для уменьшения времени работы программы и повышения ее эффективности. Итак, в приложении реализован алгоритм вычисления простых множителей. Это основная функция факторизации, которая вычисляет множители, ну и выводит 10 произвольных чисел, простые множители этих чисел. Итак, давайте скомпилируем наше приложение и воспользуемся hotspot анализом, для того чтобы оценить начальное время, ну и посмотреть, где программа проводит больше всего времени. Программа маленькая, очевидно, что это будет функция факторизации. Итак, переходим в нее и видим, что, действительно, здесь она проводит больше всего времени. Давайте попробуем сначала сделать алгоритмически лучше. Итак, алгоритм устроен так, что проходит числа от 2 до числа, которого мы ищем, простые множители, и каждый из них пытается проверить. Очевидно, что для числа большего, чем number поделить пополам, деление на него не даст в остатке 0. Поэтому мы можем сократить количество проверяемых чисел, которые являются множителями, ровно в 2 раза. Итак, давайте это сделаем, это первое улучшение. Скомпилируем это наше приложение, и теперь опять проведем оценку времени. Итак, данное изменение позволило нам... давайте сейчас воспользуемся инструментом сравнения, так, сравниваем и видим, что позволило ускорить на 0,7 наше приложение. Достаточно неплохо, мы еще даже не параллелили. Теперь давайте распараллелим наше приложение. Итак, очевидно, что числа можно, к числам множители можно искать независимо, простые множители. Итак, давайте для этого воспользуемся openMP, создадим параллельный регион и у меня компьютер, на котором есть 4 физических ядра, поэтому я явно здесь сейчас укажу, что мне нужно, чтобы 4 потока было в параллельном регионе. Для распределения работы в этом цикле for мы воспользуемся директивой for. Итак, давайте теперь скомпилируем. Для того чтобы OpenMP поддерживался, в свойствах, напоминаю, CC++ language Intel C++, нужно включить соответствующую опцию. Нажимаем ОК, компилируем приложение и запускаем опять же Basic Hotspot Analysis. Итак, давайте теперь проведем опять же сравнение: то, что у нас было — исходная версия, и плюс распараллеливание. Итак, сравниваем. Так, когда выходит такая ошибка, это означает, что один из анализов не закрыт. Вот, вот здесь я забыл его закрыть. Так, давайте еще раз выберем сравнение. Как видите, мы уже больше, чем в 2 раза ускорили, но у нас 4 ядра. Давайте посмотрим на этот анализ. В данной вкладке есть раздел, посвященный OpenMP, и инструмент нам сообщает — вот здесь мы видим, отсвечивает красным, — что у нас имеется дисбаланс в работе потоков. Давайте для этого, для того, чтобы выяснить, почему так происходит, выполним concurrence analysis. Запускаем его и попробуем проанализировать: что же происходит у нас в параллельной области, почему не сбалансирована нагрузка на потоки? Так, видим. [ПАУЗА] Здесь та же самая информация, нас интересует CPU Usage Histogramme. Итак, давайте мы сделаем следующее: вот видите, здесь есть шкала. Каждый столбик показывает, сколько по времени работали. Вот столько времени работало только, только один поток. Вот столько 2 потока, 3 и 4. Как видите, 4 потока работали очень мало, чуть больше 3, также 2, и 1. Теперь перейдем во вкладку Bottom Up. Как видим, эти проблемы у нас имеются в функции факторизации. Ниже представлено окно: как у нас каждый из потоков работает. То есть видите, у нас есть нулевой поток, первый, второй и третий. Итак, нулевой, он начинал свою работу, работал, дошли до параллельной секции здесь. Дальше они начали работать, и, как видите, потом нулевой поток почему-то перестал считать. То есть он долго находился в ожидании, видимо, он свою работу выполнил и ждал пока все остальные завершат выполнение цикла. Чуть дольше работал первый поток, еще больше второй, и дольше всех работал третий поток. Как видите, налицо не сбалансирована нагрузка на потоки. Переходим в эту функцию. Исходный код. Да, действительно, здесь имеется несбалансированность, Итак, перейдем непосредственно в сам код и попробуем понять, что же здесь происходит. Когда мы поставили директиву pragma omp for, то воспользовались распределением итераций по умолчанию, мы ничего дополнительно сами не указывали пока. По умолчанию все итерации цикла поровну делятся между потоками. То есть предположим, что мы бы проверяли 100 чисел. Итак, тогда бы первые 25 достались бы нулевому потоку, следующие 25 — первому, следующие 25 — второму и следующие 25 — третьему. Но понятно, что для чисел от 1 до 25 найти простые множители проще, чем для чисел, начиная от 75 до 99, так как они больше. За счет этого и возникает у нас несбалансированность нагрузки. Чтобы это исправить, давайте мы воспользуемся опцией, которая позволяет задать тип распределения итераций между нитями schedule. Мы воспользуемся статическим распределением и за счет этого попытаемся сбалансировать нагрузку. Мы проверяем 100 тысяч чисел, и, если бы мы опцию не указали, нулевая нить проверила бы с первого по двадцать, по число 25 тысяч. Давайте мы сделаем так, чтобы по тысячу чисел приходилось на 1 блок. Тогда у нас нагрузка более равномерно распределится между потоками. Давайте скомпилируем приложение. Так, ошибка у нас здесь, компилятор нам сообщил. Еще раз исправили, и теперь запустим опять concurrence analysis и попробуем посмотреть, что получилось. И [ПАУЗА] видим, сейчас он все сделает, для нас красивую картинку. Итак, в основное время, в основное время работали все 4 потока. Замечательно, посмотрим в Bottom Up, как видим, ситуация улучшилась, то есть все 4 потока, в основном, работали. Вы можете самостоятельно еще поэкспериментировать с различными настройками. Допустим, выбрать динамический тип или поменять размер блока. Ну и в завершение давайте мы проведем еще раз Basic Hotspots Analysis и сравним последнюю версию нашей программы и самую первую последовательно. Итак, так, закрываем, [ПАУЗА] [ПАУЗА] Как видите, нам удалось существенно ускорить наше приложение. В 3 раза быстрее оно работает. Если мы сравним результаты — тогда у нас было разбалансировано, то увидим, что фактически за счет балансировки нагрузки между потоками, на 30% мы ускорили наше приложение и повысили его эффективность. Как вы видите, данный инструмент — Amplifier — позволил нам повысить эффективность нашей программы. В этом модуле мы рассмотрели компилятор и соответствующие опции, а также инструменты, которые позволяют провести оптимизацию и повысить эффективность наших программ. В первой части курса мы рассмотрели с вами технологию параллельного программирования OpenMP, а также инструменты, которые позволяют создавать параллельные программы для систем с общей памятью. Во второй части курса мой коллега Евгений расскажет о технологиях создания параллельных программ для систем с распределенной памятью. А я с вами на этом прощаюсь, до свиданья!