Отладка и оптимизация многопоточных OpenMP-программ
Upcoming SlideShare
Loading in...5
×

Like this? Share it with your network

Share

Отладка и оптимизация многопоточных OpenMP-программ

  • 520 views
Uploaded on

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

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

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
    Be the first to like this
No Downloads

Views

Total Views
520
On Slideshare
520
From Embeds
0
Number of Embeds
0

Actions

Shares
Downloads
2
Comments
0
Likes
0

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. Отладка и оптимизациямногопоточных OpenMP-программАвторы: Андрей Карпов, Евгений РомановскийДата: 24.01.2009АннотацияЗадача знакомства программистов с областью разработки параллельных приложений становитсявсе актуальней. Данная статья является кратким введением в создание многопоточныхприложений, основанных на технологии OpenMP. Описаны подходы к отладке и оптимизациипараллельных приложений.В предложенной статье рассматривается технология OpenMP, главная ценность которой ввозможности доработки и оптимизации уже созданного последовательного кода. СтандартOpenMP предоставляет собой набор спецификаций для распараллеливания кода в среде с общейпамятью. Ключевое условие для использования OpenMP - поддержка этого стандарта со стороныкомпилятора. Кроме того, требуются принципиально новые инструменты для этапа отладки, накотором обнаруживаются, локализуются и устраняются ошибки, и производится оптимизация.Отладчик для последовательного кода это хорошо знакомый и активно используемыйпрограммистом инструмент. Он предоставляет разработчику возможность отслеживатьизменения значений переменных при пошаговом выполнении программы с помощью развитогопользовательского интерфейса. Однако ситуация значительно меняется, когда заходит речь оботладке и тестировании многопоточных приложений. А именно создание многопоточныхприложений становится основным направлением в создании эффективных приложений.Отладка последовательной программы основана на том, что степень предсказуемости начальногои текущего состояний программы определяется входными данными. Когда программистпереходит к отладке многопоточного кода, то он обычно сталкивается с совершенно уникальнымипроблемами: в различных операционных системах применяются разные стратегии планирования,нагрузка на вычислительную систему динамически изменяется, приоритеты процессов могутразличаться и т. д. Точное воссоздание состояния программы в некоторый момент ее выполнения(тривиальная задача для последовательной отладки) значительно усложняется при переходе кпараллельной программе, что связано с недетерминированным поведением последней. Инымисловами, поведение запущенных в системе процессов, а именно их выполнение и ожиданиевыполнения, взаимные блокировки и прочее, зависит от случайных событий, происходящих всистеме. Как же быть? Очевидно, для отладки параллельного кода требуются совершенно другиесредства.По мере того, как параллельные вычислительные системы стали обычным явлением впотребительском сегменте рынка, спрос на средства отладки многопоточных приложенийсущественно увеличился. Мы рассмотрим отладку и повышение производительностимногопоточного приложения, построенного на основе технологии OpenMP. Полный текстпрограммы, из которого мы будем приводить отдельные участки кода содержится в конце статьив приложении N1.
  • 2. Для примера возьмем последовательный программный код функции Function, приведенный влистинге 1. Эта простая подпрограмма вычисляет значения некоторой математической функции,имеющей один аргумент.double Function(int N){ double x, y, s=0; for (int i=1; i<=N; i++) { x = (double)i/N; y = x; for (int j=1; j<=N; j++) { s += j * y; y = y * x; }; }; return s;}При вызове этой функции с аргументом N, равным 15000, мы получим резуьтат 287305025.528.Эту функцию можно легко распараллелить с помощью средств стандарта OpenMP. Добавим однуединственную строку перед первым оператором for (листинг 2).double FunctionOpenMP(int N){ double x, y, s=0; #pragma omp parallel for num_threads(2) for (int i=1; i<=N; i++) { x = (double)i/N; y = x; for (int j=1; j<=N; j++) { s += j * y; y = y * x; }; };
  • 3. return s;}К сожалению, созданный нами код является некорректным и результат работы функции в общемслучае не определен. Например, он может быть равен 298441282.231. Попробуем разобраться впричинах.Основная причина ошибок в параллельных программах — некорректная работа с разделяемыми,т. е. общими для всех запущенных процессов ресурсами, в частном случае — с общимипеременными.Данная программа успешно компилируется в среде Microsoft Visual Studio 2005, причемкомпилятор даже не выдает никаких предупреждений. Однако она некорректна. Чтобы этопонять, надо вспомнить, что в OpenMP-программах переменные делятся на общие (shared),существующие в одном экземпляре и доступные всем потокам, и частные (private),локализованные в конкретном процессе. Кроме того, есть правило, гласящее, что по умолчаниювсе переменные в параллельных регионах OpenMP общие, за исключением индексовпараллельных циклов и переменных, объявленных внутри этих параллельных регионов.В приведенном выше примере видно, что переменные x, y и s — общие, что совершеннонеправильно. Переменная s обязательно должна быть общей, так как в рассматриваемомалгоритме она является, по сути, сумматором. Однако при работе с переменными x или y каждыйпроцесс вычисляет очередное их значение и записывает в соответствующую переменную (x илиy). И тогда результат вычислений зависит от того, в какой последовательности выполнялисьпараллельные потоки. Иначе говоря, если первый поток вычислит значение для x, запишет его впеременную x, а потом такие же действия произведет второй поток, то при попытке прочитатьзначение переменной x первым потоком он получит то значение, которые было записано тудапоследним по времени, а значит, вычисленное вторым потоком. Подобные ошибки в случае,когда работа программы зависит от порядка выполнения различных фрагментов кода,называются race condition или data race (состояние "гонки" или "гонки" вычислительных потоков;подразумевается, что имеют место несинхронизированные обращения к памяти).Для поиска таких ошибок необходимы специальные программные средства. Одно из них - IntelThread Checker. Адрес продукта: http://www.viva64.com/go.php?url=526. Данная программапоставляется как модуль к профилировщику Intel VTune Performance Analyzer, дополняяимеющиеся средства для работы с многопоточным кодом. Intel Thread Checker позволяетобнаружить как описанные выше ошибки, так и многие другие, например deadlocks ("тупики",места взаимной блокировки вычислительных нитей) и утечки памяти.После установки Intel Thread Checker в диалоге New Project приложения Intel VTune PerformanceAnalyzer появится новая категория проектов — Threading Wizards (мастера для работы с потоками),среди которых будет Intel Thread Checker Wizard. Для запуска примера необходимо выбрать его, ав следующем окне мастера указать путь к запускаемой программе. После запуска программаначнет выполняться, а профилировщик соберет все сведения о работе приложения. Пример такойинформации, выдаваемой Intel Thread Checker, приведен на рисунке 1.
  • 4. Рисунок 1 - В результате работы Thread Checker обнаружено множество критических ошибокКак видно, даже для такой небольшой программы количество ошибок достаточно велико. ThreadChecker группирует обнаруженные ошибки, одновременно оценивая их критичность для работыпрограммы, и приводит их описание, что существенно повышает эффективность работыпрограммиста. Кроме того, на вкладке Source View представлен программный код приложения суказанием тех мест в коде, где имеются ошибки (рисунок 2).
  • 5. Рисунок 2 - Анализ многопоточного кода Intel Thread CheckerСледует учитывать, что Intel Thread Checker в ряде случаях не может выявить ошибку. Этоотносится к коду, который редко получает управление или выполняется на системе с другойархитектурой. Ошибка также может быть пропущена, когда набор входных тестовых данныхсильно отличается от данных обрабатываемых программой при ее эксплуатации конечнымипользователями. Все это не позволяет быть уверенным в отсутствии ошибок в многопоточнойпрограмме, после проверки ее с использованием динамических средств анализа, результаткоторых зависит от среды и времени исполнения.
  • 6. Но хорошей новостью для разработчиков OpenMP является существование и другого инструмента- VivaMP, предлагающего альтернативный подход к верификации параллельных программ.VivaMP построен по принципу статического анализатора кода и позволяет проверять кодприложения без его запуска. Более подробно с инструментом VivaMP можно познакомиться насайте разработчиков http://www.viva64.com/ru/vivamp-tool/.Области применения VivaMP: • Контроль корректности кода разрабатываемых приложений на базе технологии OpenMP. • Помощь в освоении технологии OpenMP и интеграция ее в уже существующие проекты. • Создание более эффективных в использовании ресурсов параллельных приложений. • Поиск ошибок в существующих OpenMP приложениях.Анализатор VivaMP интегрируется в среду Visual Studio 2005/2008 и предоставляет простойинтерфейс для проверки приложений (рисунок 3).
  • 7. Рисунок 3 - Запуск инструмента VivaMP, интегрированного в Visual Studio 2005 VivaMP,Если мы запустим VivaMP для нашего примера, то получим сообщение об ошибках в 4 различныхстроках, где происходит некорректная модификация переменных (рисунок 4). Рисунок 4 - Результат работы статического анализатора VivaMP анализатораКонечно, статический анализ также имеет ряд недостатков, как и динамический анализ. Но вместеэти две методологии (два инструмента Intel Thread Checker и VivaMP) отлично дополнят другдруга. И их совместное использование является достаточно надежным методом выявления надежнымошибок в многопоточных приложениях.Описанную выше и обнаруженную средствами Intel Thread Checker и VivaMP ошибку записи впеременные x и y исправить довольно просто: нужно лишь добавить в конструкцию #pragma ompparallel for еще одну директиву private (x, y). Таким образом, эти две переменные будут директиву:объявлены как частные, и в каждом вычислительном потоке будут свои копии x и y. Следует такжеобратить внимание, что все потоки сохраняют вычисленный результат добавлением его кпеременной s. Подобные ошибки происходят тогда, когда один вычислительный поток пытается ннойзаписать некоторое значение в общую память, а другой в то же время выполняет операциючтения. В рассматриваемом примере это может привести к некорректному результату.Рассмотрим инструкцию s += j*y. Изначально предполагается, что каждый поток суммирует тримвычисленный результат с текущим значением переменной s, а потом такие же действиявыполняют остальные потоки. Однако возможна ситуация, когда, например, два потокаодновременно начали выполнять инструкцию s += j*y, т. е. каждый из них сначала прочитаеттекущее значение переменной s, затем прибавит к этому значению результат умножения j*y иполученное запишет в общую переменную s.В отличие от операции чтения, которая может быть реализована параллельно и являетсядостаточно быстрой, операция записи всегда последовательна. Следовательно, если сначала
  • 8. первый поток записал новое значение, то второй поток, выполнив после этого запись, затретрезультат вычислений первого, потому что оба вычислительных потока сначала прочитали одно ито же значение s, а потом стали записывать свои данные в эту переменную. Иными словами, тозначение s, которое второй поток в итоге запишет в общую память, никак не учитывает результатвычислений, полученный в первом потоке. Можно избежать подобной ситуации, еслигарантировать, что в любой момент времени операцию s += j*y разрешается выполнять толькоодному из потоков. Такие операции называются неделимыми или атомарными. Когда нужноуказать компилятору, что какая-либо инструкция является атомарной, используется конструкция#pragma omp atomic. Программный код, в котором исправлены указанные ошибки, приведен влистинге 3.double FixedFunctionOpenMP(int N){ double x, y, s=0; #pragma omp parallel for private(x,y) num_threads(2) for (int i=1; i<=N; i++) { x = (double)i/N; y = x; for (int j=1; j<=N; j++) { #pragma omp atomic s += j * y; y = y * x; }; }; return s;}После перекомпиляции программы и ее повторного анализа в Thread Checker мы увидим, чтопрограмма не содержит критических ошибок. Выводятся только два информационных сообщенияо том, что параллельные потоки завершаются при достижении оператора return в функцииMathFunction. В рассматриваемом примере так и должно быть, потому что распараллеливаетсятолько код внутри данной функции. Статический анализатор VivaMP не выдаст на этот код вообщеникаких диагностических сообщений, так как он полностью корректен с его точки зрения.Но отдыхать еще рано. Давайте уточним, действительно ли наш код стал более эффективнымпосле распараллеливания. Замерим время выполнения трех функций: 1 - последовательной, 2 -параллельной некорректной, 3 - параллельной корректной. Результаты такого измерения дляN=15000 приведены в таблице 1.
  • 9. Функция Результат Время выполненияПоследовательный вариант функции 287305025.528 0.5781 секундНекорректный вариант параллельной функции 298441282.231 2.9531 секундКорректный вариант параллельной функции, использующий 287305025.528 36.8281 секунддирективу atomicТаблица 1 - Результат работы функцийИ что мы видим в таблице? А то, что параллельный вариант некорректной функции работает внесколько раз медленнее. Но нас эта функция не интересует. Беда в том, что правильный вариантработает вообще более чем в 60 раз медленнее. Нам нужна такая параллельность? Конечно, нет.Все дело в том, что мы выбрали крайне неэффективный метод решения проблемы ссуммированием результата в переменной s, использованием директивы atomic. Такой подходприводит к частому ожиданию потоками друг друга. Чтобы избежать постоянных взаимныхблокировок при выполнении атомарной операции суммирования мы можем использоватьспециальную директиву reduction. Опция reduction определяет, что на выходе из параллельногоблока переменная получит комбинированное значение. Допустимы следующие операции: +, *, -,&, |, ^, &&, ||. Модифицированный вариант функции показан в листинге 4.double OptimizedFunction(int N){ double x, y, s=0; #pragma omp parallel for private(x,y) num_threads(2) reduction(+: s) for (int i=1; i<=N; i++) { x = (double)i/N; y = x; for (int j=1; j<=N; j++) { s += j * y; y = y * x; }; }; return s;}На этот мы получим не только корректный, но и более производительный вариант функции(таблица 2). Скорость вычисления возросла почти в 2 раза (в 1.85 раз), что является оченьхорошим показателем для подобных функций.Функция Результат Время
  • 10. выполненияПоследовательный вариант функции 287305025.528 0.5781 секундНекорректный вариант параллельной функции 298441282.231 2.9531 секундКорректный вариант параллельной функции, использующий 287305025.528 36.8281 секунддирективу atomicКорректный вариант параллельной функции, использующий 287305025.528 0.3125 секунддирективу reductionТаблица 2 - Результат работы функцийВ заключение еще раз хочется подчеркнуть, что работоспособная параллельная программа можетдалеко не всегда являться эффективной. И хотя параллельное программирование предоставляетмножество способов повышения эффективности кода, оно требует от программиставнимательности и хороших знаний используемых им технологий. К счастью существуют такиеинструменты, как Intel Thread Checker и VivaMP, существенно облегчающих создание и проверкимногопоточных приложений. Удачи вам уважаемые читатели в освоении новой области знаний.Приложение N1. Текст демонстрационной программы#include "stdafx.h"#include <omp.h>#include <stdlib.h>#include <windows.h>class VivaMeteringTimeStruct {public: VivaMeteringTimeStruct() { m_userTime = GetCurrentUserTime(); } ~VivaMeteringTimeStruct() { printf("Time = %.4f secondsn", GetUserSeconds()); } double GetUserSeconds();private: __int64 GetCurrentUserTime() const; __int64 m_userTime;};__int64 VivaMeteringTimeStruct::GetCurrentUserTime() const{ FILETIME creationTime, exitTime, kernelTime, userTime; GetThreadTimes(GetCurrentThread(), &creationTime,
  • 11. &exitTime, &kernelTime, &userTime); __int64 curTime; curTime = userTime.dwHighDateTime; curTime <<= 32; curTime += userTime.dwLowDateTime; return curTime;}double VivaMeteringTimeStruct::GetUserSeconds(){ __int64 delta = GetCurrentUserTime() - m_userTime; return double(delta) / 10000000.0;}double Function(int N){ double x, y, s=0; for (int i=1; i<=N; i++) { x = (double)i/N; y = x; for (int j=1; j<=N; j++) { s += j * y; y = y * x; }; }; return s;}double FunctionOpenMP(int N){ double x, y, s=0; #pragma omp parallel for num_threads(2) for (int i=1; i<=N; i++) {
  • 12. x = (double)i/N; y = x; for (int j=1; j<=N; j++) { s += j * y; y = y * x; }; }; return s;}double FixedFunctionOpenMP(int N){ double x, y, s=0; #pragma omp parallel for private(x,y) num_threads(2) for (int i=1; i<=N; i++) { x = (double)i/N; y = x; for (int j=1; j<=N; j++) { #pragma omp atomic s += j * y; y = y * x; }; }; return s;}double OptimizedFunction(int N){ double x, y, s=0; #pragma omp parallel for private(x,y) num_threads(2) reduction(+: s) for (int i=1; i<=N; i++) {
  • 13. x = (double)i/N; y = x; for (int j=1; j<=N; j++) { s += j * y; y = y * x; }; }; return s;}int _tmain(int , _TCHAR* []){ int N = 15000; { VivaMeteringTimeStruct Timer; printf("Result = %.3f ", Function(N)); } { VivaMeteringTimeStruct Timer; printf("Result = %.3f ", FunctionOpenMP(N)); } { VivaMeteringTimeStruct Timer; printf("Result = %.3f ", FixedFunctionOpenMP(N)); } { VivaMeteringTimeStruct Timer; printf("Result = %.3f ", OptimizedFunction(N)); } return 0;}