| 01.06.2009 00:00 | |
Резюме
Организациям, разрабатывающим программное обеспечение, необходимо учитывать развитие параллельных технологий, применяя доступные прикладные программные интерфейсы (API) и инструменты эффективной организации процесса разработки. В долгосрочной перспективе потребуется весьма высокая степень параллелизма, однако, полная переработка существующих приложений является трудновыполнимой задачей. В данной статье рассматривается поэтапный, циклический подход, который позволит вносить в проекты усовершенствования по мере необходимости.
Введение
Очевидно, что в обозримом будущем тенденция к распространению многоядерных процессоров сохранится, делая поддержку параллелизма на уровне программного обеспечения весьма актуальной как сейчас, так и в долгосрочной перспективе. Общепринятым способом реализации параллелизма является многопоточность приложений, для поддержки которой существует целый набор инструментов и практических методов. В то же время, разработчики зачастую расценивают необходимость распараллеливать приложения как неизбежное зло в лучшем случае, и как кратчайший путь к провалу проекта – в худшем.
Действительно, грамотная поддержка многопоточных технологий приложением представляет собой чрезвычайно сложное начинание. Однако оно может дать существенные преимущества, как в виде общего повышения производительности, так и с точки зрения новых функций, вплоть до появления новых способов обработки данных и взаимодействия с пользователями.
Разработчикам, поставившим перед собой цель обеспечить масштабируемость программного обеспечения с учетом текущих и будущих поколений процессоров, необходимо прежде всего определиться с подходом к распараллеливанию. Конечно, если приложение пишется с нуля, следует прежде всего позаботиться о адекватной масштабируемости, - наверняка за временя жизни нового приложения с появятся процессоры с большим количеством ядер. Теоретически, поддержку многоядерных процессоров необходимо добавлять и в уже существующие продукты, но даже ее частичная реализация таит в себе немало сложностей.
Существует мнение, что единственным надежным подходом к распараллеливанию уже написанных последовательных приложений является их «переписывание с нуля». К огромному сожалению, редко кто может позволить себе неизбежные в этом случае затраты времени и ресурсов. Джоэл Спольски (Joel Spolsky), автор популярного блога Joel on Software, упоминает тот факт, что программисты часто склонны забросить уже существующие наработки и начать все заново, с чистого листа. С точки зрения бизнеса этот путь может привести к катастрофическим последствиям. Более того, он отзывается о стремлении переписать код с нуля как о «единственной грубейшей стратегической ошибке, которую может допустить компания-разработчик».
Постепенное «добавление» параллелизма становится оптимальным подходом, объединяющим далеко идущие цели и ближайшие требования. Таким образом, необходимо найти сравнительно простые способы введения параллельного кода в приложение. Включая автоматизацию процесса разработки, использование библиотек с поддержкой многопоточности, а там, где это действительно необходимо, - низкоуровневое программирование потоков.
Потоки ОС – полный контроль по высокой цене
Помимо предложенного здесь поэтапного подхода к распараллеливанию, разработчикам также следует определиться с тем, насколько важен контроль над потоками в том или ином алгоритме, и насколько такой контроль оправдан. Следует заметить, что для некоторых приложений может потребоваться смешанная схема распараллеливания.
Варианты распараллеливания не являются взаимоисключающими, но в общем Intel рекомендует отказаться от прямого (explicit) разделения на потоки. Явное разделение на потоки осуществляется на очень низком уровне, это очень мощный и гибкий, но при этом чрезвычайно сложный механизм. Джеймс Рейндерс (James Reinders) из отдела решений для разработчиков Intel Software Products, говорит:
«Не используйте низкоуровневые собственные потоки (Pthreads, потоки Windows, библиотеки Boost.threads и аналогичные). С точки зрения параллелизма, потоки и интерфейсы с передачей сообщений (MPI) – это аналог ассемблера. Они обеспечивают наибольшую гибкость, но требуют слишком много времени для написания, отладки и поддержания работоспособности кода».[1]
Для объективного освещения вопроса следует признать, что работа с потоками на уровне ОС позволяет реализовать некоторые сложные механизмы, которые могут потребоваться разработчику, например, при планировании очередности потоков. Кроме того, это также позволяет добавить расширяемые механизмы управления обработкой ошибок, контроля привязки потоков к процессору, синхронизации подгрупп потоков и т.п..
Таким образом, в ряде случаев некоторым разработчикам имеет смысл использовать потоки ОС. В противоположность этому, рассматриваемый в нашей статье поэтапный подход основан на применении следующих методов:
- Используйте компилятор для автоматического распараллеливания кода. Возможности данного метода ограничены, зато он относительно прост и дает хорошие результаты при минимальных усилиях.
- Используйте директивы OpenMP. Этот сравнительно простой способ указать компилятору, каким образом следует выполнить декомпозицию блоков последовательного кода для параллельного выполнения.
- Используйте готовые многопоточные библиотеки для решения распространенных, типовых задач. Например, библиотека Intel® Threading Building Blocks (TBB) позволяет писать многопоточные приложения, фокусируясь на ваших прикладных задачах, а не на потоках.
Ловушки на пути к многопоточности
Многопоточность часто помогает добиться хорошей масштабируемости алгоритма, но вместе с тем может повлечь за собой непредвиденные результаты или падение производительности. Например, наличие нескольких потоков, обновляющих одну и ту же глобальную переменную, может вызвать потерю данных, а неправильная синхронизация потоков может привести к тому, что параллельная версия приложения будет работать гораздо хуже, чем последовательная.
В этом разделе рассматривается несколько распространенных проблем многопоточности, которые иллюстрируют всю сложность распараллеливания приложения вручную с помощью явного разделения на потоки. Конечно, подобные трудности могут возникнуть и вне зависимости от того, используете ли вы потоки ОС, или нет, но существует общая закономерность: по мере повышения сложности многопоточного приложения растет и вероятность возникновения проблем.
Когда в отсутствие правильной синхронизации к памяти обращается более чем один поток, может возникать конкуренция при доступе к данным (data race), что приводит к непредсказуемым результатам вычислений. Пример такой конкуренции представлен на Рисунке 1: в зависимости от того, какой поток выполняется раньше, итоговое значение переменной X будет различаться. Если поток 1 выполняется раньше потока 2, программа завершит параллельный участок со значением Х равным 3. Если поток 2 выполняется раньше потока 1, программа завершит параллельный участок со значением Х равным 43.
Рисунок 1. Конкуренция при доступе к данным.
Если несколько потоков приходят к состоянию, когда каждый поток ожидает завершения другого, произойдет взаимоблокировка потоков (Deadlock), что приведет к зависанию приложения. Такая ситуация представлена на Рисунке 2, где комбинация блокировок (lock) на переменных A и B для потоков 1 и 2 препятствует дальнейшему выполнению каждого из потоков, что фактически блокирует приложение. В гораздо более сложных реальных случаях подобные ошибки происходят при стечении нескольких определенных условий, что сильно затрудняет отладку.
Рисунок 2. Состояние взаимной блокировки.
Помимо рассмотренных здесь ошибок, многопоточный код также подвержен различным проблемам производительности, которые легко могут привести к тому, что распараллеленное приложение будет выполняться намного медленнее, чем предполагалось, и даже медленнее, чем его последовательная версия. Например, распространенной причиной снижения производительности является излишнее применение блокировок, так как последовательный характер блокированных участков ограничивает масштабируемость кода, а слишком частые блокировки приводят к вытеснению данных из кэша и замедляют работу потоков. Поиск выхода из подобных ситуаций сопряжен со значительными сложностями и временными затратами, при этом очень трудно воспроизвести сценарий возникновения ошибки.
Начните с существующего кода
Вместо того, чтобы приступать к грандиозной переработке вашего кода с использованием потоков ОС, задумайтесь о применении более осторожного подхода, в рамках которого вам не понадобится переписывать все приложение. Начните с самых простых изменений. Внедряйте параллелизм в необходимых для вашего приложения масштабах – в первую очередь думайте о пользователях, а не об оборудовании. Оптимизируйте последовательную версию вашего приложения, и если вам требуется более высокая производительность, рассмотрите следующие возможности:
- Используйте компилятор Intel® для распараллеливания внутренних циклов (tight loop):
- Используйте операторы автоматического распараллеливания /Qparallel (Windows) или -parallel (Linux и Mac OS)
- Используйте операторы OpenMP /Qopenmp (Windows) или -openmp (Linux и Mac OS) OpenMP, в сочетании с добавлением директив OpenMP в код
- Удостоверьтесь, что ваши библиотеки являются безопасными для потоков, – вызовы библиотечных функций могут выполняться из многопоточного кода.
- Определите те участки кода, которые можно эффективно распараллелить. Попробуйте инструменты анализа производительности, например Intel® VTune™ Performance Analyzer или его аналоги.
- Используйте библиотеки для распараллеливания на уровне задач, например Intel® Threading Building Blocks.
- Замените вызовы к типовым ресурсоемким функциям вызовами к библиотекам шаблонов с внутренней многопоточностью, таким как Intel® Integrated Performance Primitives и Intel® Math Kernel Library.
- Используйте инструменты анализа многопоточных приложений Intel® Thread Checker и Intel® Thread Profiler.
Дополнительные рекомендации по обоснованию и применению различных методов вы можете найти на страницах Intel® Software Network:
- Transitioning Software to Future Generations of Multi-Core
- Performance in Threaded Applications Using the Intel® VTune™ Performance Analyzer
- Threading Applications with the Intel® Compiler 10.0 Professional Editions
Использование директив OpenMP
Преимущества OpenMP по сравнению с низкоуровневыми потоками заключаются в его относительной простоте и кросс-платформенности. OpenMP представляет собой набор директив pragma, API, переменных среды и поддерживается компиляторами для платформ Windows, Linux, и Mac OS.
OpenMP был разработан для облегчения процесса распараллеливания приложения. Oн позволяет вам работать с нужной частью программы, не внося значительных изменений в код. В действительности, операторы последовательного кода сами по себе, в большинстве случаев, не требуют изменения, что снижает вероятность появления в коде новых ошибок. OpenMP позволяет создать параллельные участки кода путем вставки директив pragma в код на C/C++ или Fortran, как показано на Рисунке 3. После директивы основной (главный) поток, показанный на рисунке фиолетовым цветом, порождает необходимое число подчиненных потоков (в зависимости от задачи и доступных аппаратных ресурсов) для выполнения вычислений в параллельном режиме. В конце параллельного участка выполняемая ветвь приостанавливается до тех пор, пока все потоки не закончат свою работу, после чего подчиненные потоки завершаются, и выполнение программы продолжается в последовательном режиме.
Рисунок 3. Создание параллельного участка кода с помощью директивы OpenMP PARALLEL.
Одно из проявлений гибкости OpenMP состоит в том, что если компилятор не распознает директивы OpenMP, они просто игнорируются - будет сгенерирован последовательной код. Эта способность (предусмотренная стандартами ANSI) делает OpenMP в значительной мере неинвазивным, то есть щадящим по отношению к структуре приложения. Другой сильной стороной OpenMP является способность определять нужное количество потоков, автоматически регулируя распределение нагрузки, что особенно важно в свете постоянного увеличения числа ядер в процессорах.
Обратная сторона простоты заключается в том, что OpenMP сам по себе не может сделать ваш код безопасным для потоков (thread-safe). Он не избавит вас от необходимости выявлять и исправлять ошибки многопоточности. Конечно, вы можете воспользоваться инструментами вроде Intel® Thread Checker для отладки кода, но в данном случае простота использования OpenMP не означает простоту поиска потенциальных проблем выполнения. Вы также можете столкнуться с ситуациями, когда OpenMP не обеспечивает достаточного контроля, необходимого для оптимального планирования очередности потоков. Например, в тех случаях, когда различные части рабочей нагрузки обрабатываются быстрее, если им выделяется определенное количество потоков с определенным приоритетом, лучшие результаты можно получить с помощью собственных API создания потоков. Хотя и ценой возросшей сложности кода.
В конечном итоге, всегда можно найти компромисс между OpenMP и потоками ОС. OpenMP дает в ваше распоряжение простые механизмы распараллеливания, платой за которые являются ограниченные возможности по управлению и отслеживанию процессов. Хотя в OpenMP все же есть некоторые API, с помощью которых в ряде случаев можно получить более детальную картину происходящего. Тем не менее, OpenMP в целом представляет собой удобный инструмент получения адекватного прироста производительности в параллельной среде, который обладает несомненными преимуществами с точки зрения затрат вашего времени.
Библиотеки с поддержкой многопоточности – используем готовые решения
В последнее время на рынке стали доступны библиотеки с поддержкой многопоточности, благодаря которым вы можете использовать предварительно оптимизированные функции, без необходимости разбираться в низкоуровневых принципах их работы. Intel® Threading Building Blocks – это стандартная библиотека шаблонов на языке C++, предназначенная для того, чтобы облегчить задачу портирования часто встречающихся структур последовательных данных и алгоритмов в эффективный параллельный код. Созданные на ее основе приложения обладают отличной производительностью, масштабируемостью и надежностью на Windows*, Linux* и Mac OS*, и автоматически определяют количество доступных ядер, подстраиваясь под них.
По сравнению с потоками ОС, TBB представляет собой более простое, кросс-платформенное решение для программистов на C++, позволяющее им распараллелить приложение, тем самым повысив производительность, масштабируемость и надежность без необходимости переписывать, и заново оптимизировать распространенные структуры данных и алгоритмы. Ваше приложение просто обращается к библиотеке TBB, которая уже содержит необходимые параллельные функции. Библиотека TBB не предназначена для решения всех возможных задач, но она может успешно применяться вместе с другими методами распараллеливания.
Эта библиотека содержит тщательно оптимизированные параллельные версии распространенных алгоритмов, позволяя разработчикам приложений работать на более высоком уровне абстракции, оперируя задачами и масштабируемыми шаблонами. Благодаря TBB вы можете опираться на задачи, а не потоки. Как уже говорилось ранее, программировать потоки напрямую весьма сложно, и в итоге вы рискуете получить неэффективную программу, так как потоки представляют собой низкоуровневую и «тяжелую» логическую структуру, которая привязана к оборудованию. Библиотека TBB автоматически назначает выбранные задачи потокам таким образом, чтобы эффективно задействовать ресурсы процессора.
Intel® Threading Building Blocks обеспечивает масштабируемую архитектуру программы за счет распараллеливания на уровне данных. Подход, заключающийся в разбиении программы на отдельные функциональные блоки и назначении отдельного потока для каждого из них, не дает подобной масштабируемости, так как количество блоков обычно фиксировано. В противоположность этому, Intel Building Blocks позволяет реализовать параллелизм на уровне данных, что дает потокам возможность работать с разными частями данных. Программа, параллельно выполняющаяся на уровне данных, масштабируется по мере увеличения количества процессоров за счет разделения совокупности данных на более мелкие части, либо обращаясь к крупным структурам данных таким образом, чтобы оперировать большим количеством частей.
Наряду с Intel Threading Building Blocks, приложения также могут использовать алгоритмы из других библиотек Intel. Библиотека Intel® Integrated Performance Primitives предназначена для ускорения мультимедиа, коммуникационных и других приложений. Intel® Math Kernel Library содержит математические операции для научных, инженерных и финансовых приложений, требующих максимальной производительности. Обе библиотеки полностью безопасны для потоков и включают широкий набор многопоточных функций, облегчающих реализацию параллельных вычислений в приложениях.
Заключение
Внедряя параллельные технологии в программное обеспечение, следует поддерживать баланс между стремлением создать задел на будущее «железо» и необходимостью соблюдать текущие планы разработки. Хорошую многопоточную программу сделать весьма сложно, и это весомый аргумент в пользу того, чтобы распараллеливать ваш код с минимальной степенью вмешательства. Начните с самых простых изменений, а затем переходите к более сложным задачам, если этого требуют соображения производительности. По мере развития отрасли прикладные интерфейсы и инструменты для параллельного программирования будут становиться все мощнее, и компаниям-разработчикам следует использовать их уже сейчас, чтобы встретить будущее во всеоружии.
Независимо от того, работаете ли вы над новыми версиями существующих приложений, или над совершенно новыми приложениями, мыслите с параллельной точки зрения. Вместо того, чтобы рассматривать проблему в виде последовательного набора шагов для ее решения, подумайте, как можно разделить данные на отдельные части, чтобы обрабатывать их параллельно. В некотором смысле, эта разница в образе мышления очень близка к ситуации, когда вы думаете, решать ли некую задачу самостоятельно, либо организовать ее решение группой людей. Предполагаемая при этом подходе модульность будет хорошо понятна программистам, и останется только выбрать подходящие средства для воплощения такого параллелизма на практике.
Дополнительные ресурсы
В качестве отправной точки для более подробного изучения данного вопроса предлагаем вам ознакомиться со следующими материалами и ресурсами:
- Сообщество разработчиков параллельных приложений – здесь вы найдете техническую информацию, инструменты, форумы для обмена мнениями и консультаций специалистов.
- Продукты Intel® для разработки программного обеспечения – включают все необходимые вам инструменты для эффективной разработки, распараллеливания и оптимизации качественного программного кода.
- Техническая статья: Threading Applications with the Intel® Compiler 10.0 Professional Editions (PDF 907 КБ) – здесь освещены различные аспекты использования необходимых инструментов для распараллеливания приложений.
- Техническая статья: Achieving Application Scalability on Multi-Core Systems (PDF 590 КБ) – здесь вы найдете пояснения по использованию продуктов Intel® для разработки, которые помогут вам реализовать параллельные потоки в ваших приложениях, чтобы задействовать всю мощь современных многоядерных процессоров.
[1] Dr. Dobbs' Portal, September 5, 2007. "Rules for Parallel Programming for Multicore." http://www.ddj.com/hpc-high-performance-computing/201804248.
Пожалуйста, обратитесь к странице Уведомление об оптимизации для более подробной информации относительно производительности и оптимизации в программных продуктах компании Intel.



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