Выбор между OpenMP* и методами явной многопоточности

Введение

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

Автор: Эндрю Бинсток (Andrew Binstock)

Другие статьи на этом веб-сайте описывают преимущества программирования с помощью OpenMP, не ориентированного на конкретных поставщиков интерфейса для простого и удобного разбиения программ на потоки. Этот интерфейс состоит из набора прагм, программных интерфейсов и переменных сред и поддерживается компиляторами большого количества платформ. Самое большое преимущество OpenMP — компактность и простота, которые он привносит в параллельное программирование. Давайте рассмотрим небольшой пример, написанный на языке C/C++:

int j;

#pragma omp parallel for
for ( j = 0; j < ARRAY_SIZE; j++);
    array[j] +=j;
      

 Рис 1. Использование OpenMP для параллелизации простого цикла for.

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


Достоинства OpenMP

Если компилятор не распознает оператор прагмы, он пропускается (по стандартам ANSI для C и C++). Таким образом, код, содержащий эти прагмы, компилируется как однопоточный, если компилятор не поддерживает OpenMP, и как многопоточный, если компилятор поддерживает OpenMP. Следует помнить, что OpenMP не требует изменения однопоточного кода для разбиения его на потоки. OpenMP только добавляет директивы компилятора в форме прагм. С помощью отключения OpenMP база кода будет выполнять компиляцию и работу так же, как раньше. (OpenMP поддерживает такую же функциональность в языке Fortran с помощью использования директив, а не прагм. Более подробные сведения о синтаксисе и работе OpenMP можно найти в openmp.org*.)

Разработчики, которые изучили производительность программ, знают, что внутри циклов существует тенденция к возникновению активных точек и один из самых простых способов их устранения заключается в использовании декомпозиции данных для распределения работы цикла на несколько потоков. Это простое эффективное решение страдает одним недостатком в программных интерфейсах с явной многопоточностью, таких как Win32* и UNIX/Linux* Pthreads. Как узнать, сколько потоков окажется доступно во время исполнения программы? Если только код не будет всегда выполняться в строго определенной системе, ответ на этот вопрос всегда будет один: неизвестно. Имеются способы извлечения этой информации из системы во время выполнения и динамического создания соответствующего количества потоков, однако этот процесс может оказаться неупорядоченным и, в случае использования технологии Hyper-Threading, чреватым ошибками.

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

for-цикл на рис. 1 — классический пример возможностей OpenMP. Специфику OpenMP можно рассмотреть на примере этого простого оператора. Открывающая фигурная скобка for-цикла начинается с вызова OpenMP параллельного региона: того, который будет опираться на несколько потоков под управлением OpenMP. Все параллельные регионы заканчиваются барьером. Достигнув такого барьера, программа приостанавливается, пока все потоки OpenMP не закончат свою работу. Эта приостановка очень важна. В случае, изображенном на рис. 1, скорее всего не следует продолжать, пока не будет выполнена инициализация всего массива.

Любой переход от параллельного кода к последовательному имеет скрытый барьер. Иногда, однако, имеется несколько работающих циклов, между которыми нежелательно устанавливать какие-либо барьеры. Требуется, чтобы потоки одного цикла немедленно использовались в качестве потоков второго параллелизированного цикла сразу после первого. Это можно сделать с помощью ключевого слова nowait, как в следующей прагме, использованной в первом цикле.

#pragma omp for nowait

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

Не вся параллелизированная работа, конечно, отображается в контексте цикла. Часто программы содержат независимые задачи, которые могут быть исполнены одновременно с помощью назначения отдельных задач разным потокам. Такое проектное решение известно как функциональная декомпозиция, оно поддерживается в OpenMP с помощью прагмы sections:

#pragma omp sections
{
    #pragma omp section
    {
        TaskA();
    }

    #pragma om section
    {
        TaskB();
    }

    #pragma omp section
    {
        TaskC();
    }
}

Рис. 2. Способ параллелизации задач в OpenMP.

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

Когда OpenMP обнаруживает этот код, каждая задача назначена потоку, который в конце концов и исполняет ее. Так же как во встроенных многопоточных прикладных программных интерфейсах, OpenMP не дает никаких гарантий в отношении того, как эти задачи будут распределены. Задача TaskC() может вполне быть исполнена первой.

Те разработчики, которые используют потоки, знают, что если два или несколько потоков выполняются параллельно, необходимо использовать средства защиты для решения одной из проблем параллельного программирования: предотвращения обновления общих данных двумя потоками одновременно (эта ситуация известна под названием гонка данных). Естественно, OpenMP удовлетворяет эту потребность.

Прагма, приведенная ниже, обозначает раздел кода, который может быть исполнен только одним потоком одновременно:

#pragma omp critical
{
   ...some code here...
}

Рис. 3. Блокировка для разделов кода в OpenMP.

Ключевое слово critical – это аллюзия на концепцию критических регионов, реализованную во встроенных программных интерфейсах, таких как Pthreads и Win32. Если код выполняется одним потоком, любой другой поток, который хочет исполнить его, должен дождаться, пока первый поток достигнет закрывающей фигурной скобки. (Обратите внимание на то, что фигурные скобки играют здесь ключевую роль, как показано на рис. 1 и 2. Скобки точно сообщают OpenMP, какие части кода охватывает прагма, поэтому за прагмами OpenMP сразу же следует один оператор, как на рис. 1, или открывающаяся фигурная скобка, как на рис. 2 и 3.)

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


Ограничения OpenMP

Не все циклы могут быть разбиты на потоки. Например, циклы, результаты которых используются другими итерациями того же цикла (такая ситуация называется зависимость от потока), не будут работать правильно. Более того, компилятор и код OpenMP не смогут обнаружить возникновение подобной ситуации, и поэтому многопоточный код генерирует неверный результат. На рис. 4 показан конкретный пример.

Рис. 4. Код, который не будет работать в OpenMP из-за зависимости потока.

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

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

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

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

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

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

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

В многопоточном программном интерфейсе Win32 имеется параметр многопоточности под названием нити, который дает возможность пользователям писать собственный планировщик потоков и таким образом осуществлять детализированное управление операциями многопоточности. Это также невозможно в OpenMP.

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

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


Вывод: что выбрать?

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

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

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


Об авторе

Эндрю Бинсток (Andrew Binstock) – главный аналитик Pacific Data Works LLC. Ранее он был директором отдела глобальных технологических прогнозов аудиторской компании PricewaterhouseCoopers. Он ведет колонку по бизнес-интеграции в издании SD Times. Его последняя книга Programming with Hyper-Threading Technology: How to Write Multithreaded Software for Intel IA-32 Processors (Программирование с использованием технологии Hyper-Threading: как писать многопоточные программы для процессоров Intel IA-32) вышла в издательстве Intel Press. С ним можно связаться по адресу abinstock@pacificdataworks.com


Дополнительные ресурсы

Полезные ресурсы в дополнение к данной статье:


Загрузить PDF (124 KБ)


Per informazioni più dettagliate sulle ottimizzazioni basate su compilatore, vedere il nostro Avviso sull'ottimizzazione.
Contrassegni: