Tw-city.info

IT Новости
0 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Параллельное программирование c

Параллельное программирование

Вероятно, самым главным среди новых средств, внедренных в версию 4.0 среды .NET Framework, является библиотека распараллеливания задач (TPL) . Эта библиотека усовершенствует многопоточное программирование двумя основными способами. Во-первых, она упрощает создание и применение многих потоков. И во-вторых, она позволяет автоматически использовать несколько процессоров. Иными словами, TPL открывает возможности для автоматического масштабирования приложений с целью эффективного использования ряда доступных процессоров.

Благодаря этим двум особенностям библиотеки TPL она рекомендуется в большинстве случаев к применению для организации многопоточной обработки.

Еще одним средством параллельного программирования, внедренным в версию 4.0 среды .NET Framework, является параллельный язык интегрированных запросов (PLINQ). Язык PLINQ дает возможность составлять запросы, для обработки которых автоматически используется несколько процессоров, а также принцип параллелизма, когда это уместно.

Как станет ясно из дальнейшего, запросить параллельную обработку запроса очень просто. Следовательно, с помощью PLINQ можно без особого труда внедрить параллелизм в запрос.

Главной причиной появления таких важных новшеств, как TPL и PLINQ, служит возросшее значение параллелизма в современном программировании. В настоящее время многоядерные процессоры уже стали обычным явлением. Кроме того, постоянно растет потребность в повышении производительности программ. Все это, в свою очередь, вызвало растущую потребность в механизме, который позволял бы с выгодой использовать несколько процессов для повышения производительности программного обеспечения.

Но дело в том, что в прошлом это было не так-то просто сделать ясным и допускающим масштабирование способом. Изменить это положение, собственно, и призваны TPL и PLINQ. Ведь они дают возможность легче (и безопаснее) использовать системные ресурсы.

Библиотека TPL определена в пространстве имен System.Threading.Tasks. Но для работы с ней обычно требуется также включать в программу класс System.Threading, поскольку он поддерживает синхронизацию и другие средства многопоточной обработки, в том числе и те, что входят в класс Interlocked.

Несмотря на то что применение TPL и PLINQ рекомендуется теперь для разработки большинства многопоточных приложений, организация многопоточной обработки на основе класса Thread, по-прежнему находит широкое распространение.

Применяя TPL, параллелизм в программу можно ввести двумя основными способами. Первый из них называется параллелизмом данных. При таком подходе одна операция над совокупностью данных разбивается на два параллельно выполняемых потока или больше, в каждом из которых обрабатывается часть данных. Так, если изменяется каждый элемент массива, то, применяя параллелизм данных, можно организовать параллельную обработку разных областей массива в двух или больше потоках. Нетрудно догадаться, что такие параллельно выполняющиеся действия могут привести к значительному ускорению обработки данных по сравнению с последовательным подходом.

Несмотря на то что параллелизм данных был всегда возможен и с помощью класса Thread, построение масштабируемых решений средствами этого класса требовало немало усилий и времени. Это положение изменилось с появлением библиотеки TPL, с помощью которой масштабируемый параллелизм данных без особого труда вводится в программу.

Второй способ ввода параллелизма называется параллелизмом задач. При таком подходе две операции или больше выполняются параллельно. Следовательно, параллелизм задач представляет собой разновидность параллелизма, который достигался в прошлом средствами класса Thread. А к преимуществам, которые сулит применение TPL, относится простота применения и возможность автоматически масштабировать исполнение кода на несколько процессоров.

Библиотека TPL позволяет автоматически распределять нагрузку приложений между доступными процессорами в динамическом режиме, используя пул потоков CLR. Библиотека TPL занимается распределением работы, планированием потоков, управлением состоянием и прочими низкоуровневыми деталями. В результате появляется возможность максимизировать производительность приложений .NET. не имея дела со сложностями прямой работы с потоками. На рисунке показаны члены нового пространства имен .NET 4.0:

Используем параллельные алгоритмы C++17 для улучшения производительности

  • Переводы, 17 октября 2018 в 16:50
  • Никита Прияцелюк

Мы перевели пост из блога Microsoft, в котором разработчик рассказывает, как пользоваться параллельными алгоритмами, поддержка которых появилась в стандартной библиотеке C++17.

Как использовать параллельные алгоритмы

Чтобы использовать библиотеку параллельных алгоритмов, следуйте данным шагам:

  1. Найдите вызов алгоритма, который вы хотите оптимизировать с помощью распараллеливания. На эту роль хорошо подходят алгоритмы, которые делают больше чем O(n) работы, например, сортировка, и занимают значительное количество времени при профилировании приложения.
  2. Убедитесь, что код, используемый в алгоритме, безопасен для распараллеливания.
  3. Выберите политику параллельного исполнения (они будут описаны ниже).
  4. Если вы ещё этого не сделали, добавьте строку #include , чтобы сделать доступными политики параллельного исполнения.
  5. Добавьте одну из политик в качестве первого параметра вызова алгоритма для распараллеливания.
  6. Протестируйте результат, чтобы убедиться, что новая версия работает лучше. Распараллеливание не всегда работает быстрее, особенно когда используются итераторы непроизвольного доступа, когда набор входных данных мал или когда дополнительное распараллеливание создаёт конфликт внешних ресурсов вроде диска.

Вот пример программы, которую мы хотим сделать быстрее. Она считает, сколько времени требуется для сортировки миллиона чисел:

Параллельные алгоритмы зависят от доступного параллелизма оборудования, поэтому убедитесь, что вы проводите тесты на железе, производительность которого вам важна. Вам не нужно много ядер, чтобы показать прогресс, к тому же многие из алгоритмов построены по принципу «разделяй и властвуй», и поэтому не будут идеально ускоряться соответственно количеству потоков; но больше — всё равно лучше. В этом примере тестирование проводилось на системе с Intel 7980XE с 18 ядрами и 36 потоками. В этом тесте отладочная и релизная сборки программы показали следующий результат:

Теперь нам нужно убедиться, что вызов сортировки безопасен для распараллеливания. Алгоритмы безопасны для распараллеливания, если «функции доступа к элементу» — операции итерирования, предикаты и всё остальное, что вы можете попросить алгоритм сделать, — следуют обычному правилу для состояния гонки: «любое количество операций чтения или максимум одна операции записи». Более того, они не должны выбрасывать исключения (или выбрасывать достаточно редко, чтобы завершение программы не имело негативных последствий).

Политики исполнения

Теперь нужно выбрать политику исполнения. На данный момент стандарт включает в себя параллельную политику, обозначаемую как std::execution::par , и параллельную непоследовательную политику, обозначаемую как std::execution::par_unseq . В дополнение к требованиям первой, вторая требует, чтобы функции доступа к элементам допускали гарантии прогресса слабее параллельного выполнения. Это значит, что они не должны устанавливать блокировку или делать ещё что-то, что потребует от потоков конкурентного исполнения. Например, если алгоритм работает на графическом процессоре и пытается установить спинлок, поток, который держит этот спинлок, может помешать выполнению других потоков, и спинлок не будет снят. Больше о требованиях можно прочитать в разделах [algorithms.parallel.defns] и [algorithms.parallel.exec] стандарта C++. Если у вас есть сомнения, используйте параллельную политику. В этом примере мы используем оператор «меньше» для типа double , который не устанавливает никаких блокировок, и тип итератора, предоставленный стандартной библиотекой, поэтому мы можем использовать параллельную непоследовательную политику.

Обратите внимание, Visual C++ реализует параллельные и параллельные непоследовательные политики одинаковым образом, поэтому не стоит ожидать лучшей производительности при использовании par_unseq , хотя могут существовать реализации, которые однажды смогут использовать эту дополнительную свободу.

В пример сортировки чисел мы теперь можем добавить #include . Так как мы используем параллельную непоследовательную политику, мы добавляем std::execution::par_unseq в вызов алгоритма (при использовании параллельной политики нужно было бы использовать std::execution::par ). Теперь for -цикл в main() выглядит так:

Для этих входных данных программа сработала быстрее. Как вы будете тестировать программу зависит от выбранных вами критериев. Распараллеливание добавляет некоторую нагрузку и будет работать медленнее, чем последовательная версия, для малого числа N в зависимости от памяти и эффектов кеша, а также других факторов, специфичных для конкретной нагрузки. Если в этом примере установить значение N равным 1000, параллельная и последовательная версии будут работать примерно с одной скоростью, а если изменить значение на 100, то последовательная версия будет в 10 раз быстрее. Распараллеливание может оказать положительный эффект, но важно понимать, где его применять.

Текущие ограничения MSVC-реализации параллельных алгоритмов

Мы написали параллельную версию reverse() , и она оказалась в 1.6 раза медленнее последовательной версии на тестовом оборудовании даже при больших значениях N. Также мы протестировали другую реализацию, HPX, и получили схожие результаты. Это не значит, что добавление параллельных алгоритмов в STL было ошибкой со стороны комитета по стандартизации C++. Это просто значит, что оборудование, на которое нацелена наша реализация, не заметило улучшений. В результате мы предоставляем сигнатуры для алгоритмов, которые просто переставляют, копируют или размещают элементы в последовательном порядке, но не распараллеливаем их. Если нам покажут пример, в котором параллелизм будет работать быстрее, мы посмотрим, что можно сделать. Затронутые алгоритмы:

Реализация некоторых алгоритмов будет закончена в будущем релизе. В Visual Studio 2017 15.8 мы распараллелим:

  • adjacent_difference()
  • adjacent_find()
  • all_of()
  • any_of()
  • count()
  • count_if()
  • equal()
  • exclusive_scan()
  • find()
  • find_end()
  • find_first_of()
  • find_if()
  • for_each()
  • for_each_n()
  • inclusive_scan()
  • mismatch()
  • none_of()
  • reduce()
  • remove()
  • remove_if()
  • search()
  • search_n()
  • sort()
  • stable_sort()
  • transform()
  • transform_exclusive_scan()
  • transform_inclusive_scan()
  • transform_reduce()

Цели разработки MSVC-реализации параллельных алгоритмов

Хотя стандарт определяет интерфейс библиотеки параллельных алгоритмов, он ничего не говорит о том, как нужно распараллеливать алгоритмы или даже на каком оборудовании это нужно делать. Некоторые реализации C++ могут распараллеливать с помощью графического процессора или другого вычислительного оборудования при наличии такового. В нашей реализации нет смысла распараллеливать copy() , но есть смысл для реализации, которая нацелена на графический процессор или подобный ускоритель. Далее мы перечислим аспекты, которые ценим.

Сочетание с платформенными механизмами блокировки

Ранее Microsoft поставляла фреймворк для распараллеливания, ConcRT, который использовался в некоторых местах стандартной библиотеки. ConcRT позволяет разнородным нагрузкам прозрачно использовать доступное оборудование и позволяет потокам доделывать работу друг друга, что может увеличить общую производительность. В сущности, когда поток с рабочей нагрузкой ConcRT уходит в спящий режим, он приостанавливает выполнение текущей задачи и запускает другие готовые задачи. Такое неблокирующее поведение уменьшает переключение контекста и может обеспечить большую производительность, чем пул потоков Windows, используемый в нашей реализации параллельных алгоритмов. Тем не менее, это также означает, что нагрузка ConcRT не сочетается с примитивами синхронизации операционной системы вроде SRWLOCK, NT-событий, семафоров, оконных процедур и т. д. Мы считаем, что это неприемлемый компромисс для реализации «по умолчанию», используемой в стандартной библиотеке.

LATOKEN, Москва, от 3500 до 5000 $

Параллельная непоследовательная политика стандарта позволяет пользователю объявлять, что он поддерживает разные ограничения, присущие легковесным пользовательским библиотекам планирования вроде ConcRT, поэтому мы, возможно, рассмотрим предоставление похожего на ConcRT поведения в будущем. Однако в настоящий момент мы планируем использовать только параллельную политику. Если вы можете удовлетворить требованиям, то вам стоит использовать параллельную непоследовательную политику, так как это может улучшить производительность либо на других реализациях, либо на этой тоже в будущих версиях.

Производительность в отладочных сборках

Мы заботимся об эффективности отладки. Решения, которым нужен включённый оптимизатор для нормальной работы, не подходят для использования в стандартной библиотеке. Если добавить вызов Concurrency::parallel_sort в предыдущий пример, то мы увидим, что параллельная сортировка ConcRT немного быстрее в релизе, но при этом почти в 100 раз медленней во время отладки:

Сочетание с другими системными программами и библиотеками параллелизма

Планированием в нашей реализации занимается системный пул потоков Windows. Пул потоков использует информацию, недоступную стандартной библиотеке, например, какие ещё потоки выполняются в системе, каких ресурсов ядра они ожидают и т. д. Он решает, когда создать больше потоков и когда их завершать. Он также используется совместно с другими системными компонентами, включая те, которые не используют C++.

Для получения дополнительной информации о том, какие оптимизации делает пул потоков, посмотрите доклад Педро Тейшеры, а также официальную документацию для функций CreateThreadpoolWork() , SubmitThreadpoolWork() , WaitForThreadpoolWorkCallbacks() и CloseThreadpoolWork() .

Прежде всего, параллелизм — это оптимизация

Если в ходе тестов параллельный алгоритм не даёт преимуществ для разумных значений N, то мы его не распараллеливаем. Мы считаем, что в два раза большая скорость для N = 1,000,000 и на три порядка меньшая для N = 100 является неприемлемым компромиссом. Если вам нужен «параллелизм любой ценой», существует множество других реализаций, которые работают с MSVC, включая HPX и Threading Building Blocks.

Аналогично, стандарт C++ позволяет параллельным алгоритмам выделять память и выбрасывать std::bad_alloc , когда они не могут получить к ней доступ. В нашей реализации мы возвращаемся к последовательной версии алгоритма, если нельзя получить дополнительные ресурсы.

Параллельное программирование c

В этой части документации мы рассмотрим то новое в многопоточном API для использования с многоядерными процессорами, которое появилось в версии Framework 4.0:

• Parallel LINQ или PLINQ.
• Класс Parallel.
• Конструкции для параллельного выполнения задач (task parallelism).
• Коллекции одновременного выполнения (concurrent collections).
• SpinLock и SpinWait.

Это API также известно как PFX (Parallel Framework, аббревиатура ИМХО несколько ошибочная). Класс Parallel вместе с конструкциями параллелизма задач называется библиотекой параллельных задач, или TPL (Task Parallel Library).

Framework 4.0 также добавил несколько низкоуровневых конструкций для потоков, которые выполняют традиционные для многопоточности задачи. Мы их уже рассматривали ранее [5]:

• Конструкции сигнализации с малыми задержками (low-latency signaling constructs) SemaphoreSlim, ManualResetEventSlim, CountdownEvent и Barrier.
• Маркеры отмены (cancellation tokens) для кооперативной отмены действий (cooperative cancellation).
• Классы ленивой инициализации (lazy initialization classes).
• ThreadLocal .

Для комфортного понимания перед дальнейшим чтением желательно ознакомиться с частями 1..4 документации [2, 3, 4, 5] — в частности для изучения блокировок (locking) и безопасности совместной работы потоков (thread safety).

Все листинги кода секций параллельного программирования доступны как интерактивные примеры в LINQPad [6]. LINQPad это электронный блокнот, идеально подходящий для проверки блоков кода C# без необходимости создания окружающего класса, проекта или решения (имеется в виду среда Microsoft Visual Studio). Для получения доступа к примерам кликните на «Download More Samples» закладки » на закладке» в LINQPad, находящейся слева внизу, и выберите «C# 4.0 in a Nutshell: More Chapters».

[Почему PFX?]

В последнее время тактовые частоты CPU перестали расти, и производители переключились увеличение процессорных ядер. Это создало некоторую проблему для нас как для программистов, потому что стандартный однопоточный код не будет автоматически работать быстрее из-за увеличения числа ядер.

Задействование многих ядер проще реализовать в большинстве серверных приложений, где каждый поток независимо от других потоков работает над обработкой отдельного запроса пользователя, но сложнее то же самое сделать в приложении декстопа — потому что это обычно требует от нагруженного вычислениями кода реализации следующих функций:

1. Разбиение всей вычислительной задачи на малые части.
2. Запуск параллельной обработки этих частей с помощью многопоточности.
3. Собрать все результаты работы вместе, когда они станут доступны.

Последнее нужно сделать таким образом, чтобы не повредить совместной работе потоков ни с точки зрения безопасности и устойчивости (thread-safe), ни с точки производительности. Хотя Вы можете сделать все это классическими конструкциями многопоточности, это будет грубым подходом — в частности на шагах разделения общей задачи на части и последующей сборки результатов работы потоков. Другая проблема в том, что обычная стратегия блокировки для безопасности потоков вызывает большую конкуренцию потоков, когда много потоков работают над одними и теми же данными сразу.

Библиотеки PFX разработаны специально для того, чтобы помочь программировать в этих сценариях.

Программирование с целью задействования нескольких ядер или нескольких процессоров называется параллельным программированием. Это подмножество более широкой концепции многопоточности.

Концепции PFX. Существует 2 стратегии для разделения общей работы между потоками: параллелизм данных (data parallelism) и параллелизм задач (task parallelism).

Когда набор задач нужно выполнить на многих значениях данных, мы можем распараллелить всю задачу так, чтобы каждый поток выполнял (одинаковый) набор действий над подмножеством значений. Такой подход называется data parallelism, потому что мы распределяем обрабатываемые данные между потоками. В отличие от этого task parallelism подразумевает выполнение разными потоками разных задач; другими словами, каждый поток выполняет отличающий от других потоков алгоритм действий.

В общем случае data parallelism проще реализуется и лучше масштабируется для повышения количества ядер, потому что такой подход снижает или вовсе убирает работу потоков над общими данными (благодаря чему снижается конкуренция потоков и легче решать проблемы thread-safety). Также параллелизм данных усиливает тот факт, что чаще есть больше значений данных, чем каких-то дискретных задач, что увеличивает потенциал параллелизма.

Параллелизм данных также способствует структурированному параллелизму, что означает, что параллельные единицы общей работы запускаются и завершаются в одном и том же месте программы. В отличие от этого параллелизм задач имеет тенденцию быть не структурированным, это означает, что элементы параллельной работы могут начаться и завершиться в местах, рассеянных по программе. Структурированный параллелизм проще, меньше подвержен ошибкам, и позволяет Вам обрабатывать трудное задание разделения и координацию потоков (и даже финальное сопоставление результатов) из библиотек.

Компоненты PFX. PFX представляет два слоя функциональности. Верхний слой состоит из двух видов data parallelism API: библиотека PLINQ и класс Parallel. Нижний слой содержит классы параллелизма задач, плюс набор дополнительных конструкций, чтобы помочь с параллельным программированием.

PLINQ предоставляет самый богатый функционал: автоматизация всех шагов параллелизации, включая разделение работы на задачи, запуск выполнение этих задач в потоках и конечное сопоставление результатов в одну выходную последовательность. Она называется декларативной — потому что Вы просто декларируете, что хотите распараллелить свою работу (что структурируете как запрос LINQ), и позволяете платформе позаботиться о деталях реализации. В отличие от такого подхода другой вариант работы императивный, здесь Вам нужно специально написать код для разделения задачи на части и сопоставления результатов обработки. В случае использования класса Parallel Вы должны собрать результаты работы потоков самостоятельно; с конструкциями параллелизма задач Вы должны также самостоятельно разделить работу на части:

Используем параллельные алгоритмы C++17 для улучшения производительности

  • Переводы, 17 октября 2018 в 16:50
  • Никита Прияцелюк

Мы перевели пост из блога Microsoft, в котором разработчик рассказывает, как пользоваться параллельными алгоритмами, поддержка которых появилась в стандартной библиотеке C++17.

Как использовать параллельные алгоритмы

Чтобы использовать библиотеку параллельных алгоритмов, следуйте данным шагам:

  1. Найдите вызов алгоритма, который вы хотите оптимизировать с помощью распараллеливания. На эту роль хорошо подходят алгоритмы, которые делают больше чем O(n) работы, например, сортировка, и занимают значительное количество времени при профилировании приложения.
  2. Убедитесь, что код, используемый в алгоритме, безопасен для распараллеливания.
  3. Выберите политику параллельного исполнения (они будут описаны ниже).
  4. Если вы ещё этого не сделали, добавьте строку #include , чтобы сделать доступными политики параллельного исполнения.
  5. Добавьте одну из политик в качестве первого параметра вызова алгоритма для распараллеливания.
  6. Протестируйте результат, чтобы убедиться, что новая версия работает лучше. Распараллеливание не всегда работает быстрее, особенно когда используются итераторы непроизвольного доступа, когда набор входных данных мал или когда дополнительное распараллеливание создаёт конфликт внешних ресурсов вроде диска.

Вот пример программы, которую мы хотим сделать быстрее. Она считает, сколько времени требуется для сортировки миллиона чисел:

Параллельные алгоритмы зависят от доступного параллелизма оборудования, поэтому убедитесь, что вы проводите тесты на железе, производительность которого вам важна. Вам не нужно много ядер, чтобы показать прогресс, к тому же многие из алгоритмов построены по принципу «разделяй и властвуй», и поэтому не будут идеально ускоряться соответственно количеству потоков; но больше — всё равно лучше. В этом примере тестирование проводилось на системе с Intel 7980XE с 18 ядрами и 36 потоками. В этом тесте отладочная и релизная сборки программы показали следующий результат:

Теперь нам нужно убедиться, что вызов сортировки безопасен для распараллеливания. Алгоритмы безопасны для распараллеливания, если «функции доступа к элементу» — операции итерирования, предикаты и всё остальное, что вы можете попросить алгоритм сделать, — следуют обычному правилу для состояния гонки: «любое количество операций чтения или максимум одна операции записи». Более того, они не должны выбрасывать исключения (или выбрасывать достаточно редко, чтобы завершение программы не имело негативных последствий).

Политики исполнения

Теперь нужно выбрать политику исполнения. На данный момент стандарт включает в себя параллельную политику, обозначаемую как std::execution::par , и параллельную непоследовательную политику, обозначаемую как std::execution::par_unseq . В дополнение к требованиям первой, вторая требует, чтобы функции доступа к элементам допускали гарантии прогресса слабее параллельного выполнения. Это значит, что они не должны устанавливать блокировку или делать ещё что-то, что потребует от потоков конкурентного исполнения. Например, если алгоритм работает на графическом процессоре и пытается установить спинлок, поток, который держит этот спинлок, может помешать выполнению других потоков, и спинлок не будет снят. Больше о требованиях можно прочитать в разделах [algorithms.parallel.defns] и [algorithms.parallel.exec] стандарта C++. Если у вас есть сомнения, используйте параллельную политику. В этом примере мы используем оператор «меньше» для типа double , который не устанавливает никаких блокировок, и тип итератора, предоставленный стандартной библиотекой, поэтому мы можем использовать параллельную непоследовательную политику.

Обратите внимание, Visual C++ реализует параллельные и параллельные непоследовательные политики одинаковым образом, поэтому не стоит ожидать лучшей производительности при использовании par_unseq , хотя могут существовать реализации, которые однажды смогут использовать эту дополнительную свободу.

В пример сортировки чисел мы теперь можем добавить #include . Так как мы используем параллельную непоследовательную политику, мы добавляем std::execution::par_unseq в вызов алгоритма (при использовании параллельной политики нужно было бы использовать std::execution::par ). Теперь for -цикл в main() выглядит так:

Для этих входных данных программа сработала быстрее. Как вы будете тестировать программу зависит от выбранных вами критериев. Распараллеливание добавляет некоторую нагрузку и будет работать медленнее, чем последовательная версия, для малого числа N в зависимости от памяти и эффектов кеша, а также других факторов, специфичных для конкретной нагрузки. Если в этом примере установить значение N равным 1000, параллельная и последовательная версии будут работать примерно с одной скоростью, а если изменить значение на 100, то последовательная версия будет в 10 раз быстрее. Распараллеливание может оказать положительный эффект, но важно понимать, где его применять.

Текущие ограничения MSVC-реализации параллельных алгоритмов

Мы написали параллельную версию reverse() , и она оказалась в 1.6 раза медленнее последовательной версии на тестовом оборудовании даже при больших значениях N. Также мы протестировали другую реализацию, HPX, и получили схожие результаты. Это не значит, что добавление параллельных алгоритмов в STL было ошибкой со стороны комитета по стандартизации C++. Это просто значит, что оборудование, на которое нацелена наша реализация, не заметило улучшений. В результате мы предоставляем сигнатуры для алгоритмов, которые просто переставляют, копируют или размещают элементы в последовательном порядке, но не распараллеливаем их. Если нам покажут пример, в котором параллелизм будет работать быстрее, мы посмотрим, что можно сделать. Затронутые алгоритмы:

Реализация некоторых алгоритмов будет закончена в будущем релизе. В Visual Studio 2017 15.8 мы распараллелим:

  • adjacent_difference()
  • adjacent_find()
  • all_of()
  • any_of()
  • count()
  • count_if()
  • equal()
  • exclusive_scan()
  • find()
  • find_end()
  • find_first_of()
  • find_if()
  • for_each()
  • for_each_n()
  • inclusive_scan()
  • mismatch()
  • none_of()
  • reduce()
  • remove()
  • remove_if()
  • search()
  • search_n()
  • sort()
  • stable_sort()
  • transform()
  • transform_exclusive_scan()
  • transform_inclusive_scan()
  • transform_reduce()

Цели разработки MSVC-реализации параллельных алгоритмов

Хотя стандарт определяет интерфейс библиотеки параллельных алгоритмов, он ничего не говорит о том, как нужно распараллеливать алгоритмы или даже на каком оборудовании это нужно делать. Некоторые реализации C++ могут распараллеливать с помощью графического процессора или другого вычислительного оборудования при наличии такового. В нашей реализации нет смысла распараллеливать copy() , но есть смысл для реализации, которая нацелена на графический процессор или подобный ускоритель. Далее мы перечислим аспекты, которые ценим.

Сочетание с платформенными механизмами блокировки

Ранее Microsoft поставляла фреймворк для распараллеливания, ConcRT, который использовался в некоторых местах стандартной библиотеки. ConcRT позволяет разнородным нагрузкам прозрачно использовать доступное оборудование и позволяет потокам доделывать работу друг друга, что может увеличить общую производительность. В сущности, когда поток с рабочей нагрузкой ConcRT уходит в спящий режим, он приостанавливает выполнение текущей задачи и запускает другие готовые задачи. Такое неблокирующее поведение уменьшает переключение контекста и может обеспечить большую производительность, чем пул потоков Windows, используемый в нашей реализации параллельных алгоритмов. Тем не менее, это также означает, что нагрузка ConcRT не сочетается с примитивами синхронизации операционной системы вроде SRWLOCK, NT-событий, семафоров, оконных процедур и т. д. Мы считаем, что это неприемлемый компромисс для реализации «по умолчанию», используемой в стандартной библиотеке.

LATOKEN, Москва, от 3500 до 5000 $

Параллельная непоследовательная политика стандарта позволяет пользователю объявлять, что он поддерживает разные ограничения, присущие легковесным пользовательским библиотекам планирования вроде ConcRT, поэтому мы, возможно, рассмотрим предоставление похожего на ConcRT поведения в будущем. Однако в настоящий момент мы планируем использовать только параллельную политику. Если вы можете удовлетворить требованиям, то вам стоит использовать параллельную непоследовательную политику, так как это может улучшить производительность либо на других реализациях, либо на этой тоже в будущих версиях.

Производительность в отладочных сборках

Мы заботимся об эффективности отладки. Решения, которым нужен включённый оптимизатор для нормальной работы, не подходят для использования в стандартной библиотеке. Если добавить вызов Concurrency::parallel_sort в предыдущий пример, то мы увидим, что параллельная сортировка ConcRT немного быстрее в релизе, но при этом почти в 100 раз медленней во время отладки:

Сочетание с другими системными программами и библиотеками параллелизма

Планированием в нашей реализации занимается системный пул потоков Windows. Пул потоков использует информацию, недоступную стандартной библиотеке, например, какие ещё потоки выполняются в системе, каких ресурсов ядра они ожидают и т. д. Он решает, когда создать больше потоков и когда их завершать. Он также используется совместно с другими системными компонентами, включая те, которые не используют C++.

Для получения дополнительной информации о том, какие оптимизации делает пул потоков, посмотрите доклад Педро Тейшеры, а также официальную документацию для функций CreateThreadpoolWork() , SubmitThreadpoolWork() , WaitForThreadpoolWorkCallbacks() и CloseThreadpoolWork() .

Прежде всего, параллелизм — это оптимизация

Если в ходе тестов параллельный алгоритм не даёт преимуществ для разумных значений N, то мы его не распараллеливаем. Мы считаем, что в два раза большая скорость для N = 1,000,000 и на три порядка меньшая для N = 100 является неприемлемым компромиссом. Если вам нужен «параллелизм любой ценой», существует множество других реализаций, которые работают с MSVC, включая HPX и Threading Building Blocks.

Аналогично, стандарт C++ позволяет параллельным алгоритмам выделять память и выбрасывать std::bad_alloc , когда они не могут получить к ней доступ. В нашей реализации мы возвращаемся к последовательной версии алгоритма, если нельзя получить дополнительные ресурсы.

32 подводных камня OpenMP при программировании на C++


Авторы: Алексей Колосов
Евгений Рыжков
Андрей Карпов
ООО «СиПроВер»
Источник: RSDN Magazine #2-2008

Опубликовано: 26.08.2008
Исправлено: 15.04.2009
Версия текста: 1.0

Аннотация

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

Введение

Параллельное программирование появилось уже достаточно давно. Первый многопроцессорный компьютер был создан еще в 60-х годах прошлого века. Однако до недавних пор прирост производительности процессоров обеспечивался в основном благодаря росту тактовой частоты, и многопроцессорные системы были редкостью. Сейчас рост тактовой частоты замедляется, и прирост производительности обеспечивается за счет использования нескольких ядер. Многоядерные процессоры приобретают широкое распространение, и в связи с этим задача написания параллельных программ приобретает все большую и большую актуальность. Если ранее для увеличения производительности программы пользователь мог просто установить процессор с большей тактовой частотой и большим кешем, то теперь такой подход невозможен, и существенное увеличение производительности в любом случае потребует усилий от программиста.

В связи с тем, что параллельное программирование начинает набирать популярность только сейчас, процесс распараллеливания существующего приложения или написания нового параллельного кода может вызвать проблемы даже для опытных программистов, так как данная область является для них новой. Существующие компиляторы и анализаторы кода позволяют диагностировать лишь некоторые из возможных ошибок. Остальные ошибки (а таких большинство) остаются неучтенными и могут существенно увеличить время тестирования и отладки, особенно с учетом того, что такие ошибки почти всегда воспроизводятся нестабильно. В данной статье рассматривается язык C++, поскольку к коду именно на этом языке чаще всего предъявляются требования высокой производительности. Так как поддержка технологии OpenMP встроена в Microsoft Visual Studio 2005 и 2008 (заявлено соответствие стандарту OpenMP 2.0), мы будем рассматривать именно эту технологию. OpenMP позволяет с минимальными затратами переделать существующий код – достаточно лишь включить дополнительный флаг компилятора /openmp и добавить в свой код соответствующие директивы, описывающие, как именно выполнение программы будет распределено на несколько процессоров.

В данной статье рассматриваются далеко не все ошибки, которые могут возникнуть при программировании на C++ с использованием OpenMP и не диагностируются при этом компилятором или существующими статическими или динамическими анализаторами кода. Тем не менее, хочется надеяться, что приведенный материал поможет лучше понять особенности разработки параллельных систем и избежать множества ошибок.

Также стоит отметить, что данная статья носит исследовательский характер, и ее материал послужит при разработке статического анализатора VivaMP (http://www.viva64.com/vivamp.php), предназначенного для поиска ошибок в параллельных программах, создаваемых на основе технологии OpenMP. Мы будем рады получить отзыв о статье и узнать новые паттерны ошибок параллельного программирования.

По аналогии с одной из использованных статей [1] ошибки в данной статье разделены на логические ошибки и ошибки, приводящие к потере производительности. Логические ошибки – это ошибки, приводящие к неожиданным результатам, то есть к некорректной работе программы. Под ошибками производительности понимаются ошибки, приводящие к снижению быстродействия программы.

Прежде чем перейти к рассмотрению конкретных ошибок, определим некоторые специфические термины, которые будут использоваться в этой статье:

Директивами (directives) назовем собственно директивы OpenMP, определяющие способ распараллеливания кода. Все директивы OpenMP имеют вид #pragma omp .

Выражением (clause) будем называть вспомогательные части директив, определяющие количество потоков, режим распределения работы между потоками, режим доступа к переменным, и т. п.

Распараллеливаемой секцией (section) назовем фрагмент кода, на который распространяется действие директивы #pragma omp parallel.

Данная статья предполагает, что читатель уже знаком с основами OpenMP и собирается применять эту технологию в своих программах. Если же читатель еще не знаком с OpenMP, для первоначального знакомства можно посмотреть документ [2]. Более подробное описание директив, выражений, функций и глобальных переменных OpenMP можно найти в спецификации OpenMP 2.0 [3]. Также эта спецификация продублирована в справочной системе MSDN Library, в более удобной форме, чем формат PDF.

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

Логические ошибки


1. Отсутствие /openmp

Начнем с простейшей ошибки: если поддержка OpenMP не будет включена в настройках компилятора, директивы OpenMP будут попросту игнорироваться. Компилятор не выдаст ни ошибки, ни даже предупреждения, код просто будет выполняться не так, как этого ожидает программист.

Поддержку OpenMP можно включить в диалоговом окне свойств проекта (раздел «Configuration Properties | C/C++ | Language»).

2. Отсутствие parallel

Директивы OpenMP имеют достаточно сложный формат, поэтому сначала рассмотрим простейшие ошибки, которые могут возникнуть при неправильном написании самих директив. Ниже приведены примеры корректного и некорректного кода:

Читать еще:  Программирование в ворде
Ссылка на основную публикацию
ВсеИнструменты 220 Вольт
Adblock
detector
×
×