Основы параллельного программирования вычислительных систем с распределенной и разделяемой памятью

Автор работы: Пользователь скрыл имя, 05 Июня 2013 в 22:25, курсовая работа

Краткое описание

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

Содержание

Введение 4
1 Общие представления о параллельном программировании 6
2 Архитектура параллельных компьютеров 8
2.1 Развитие архитектуры компьютера 8
2.2 Организация памяти параллельных компьютеров 10
2.3 Сети межсоединений 15
2.3.1 Свойства сетей межсоединений. Статические сети межсоединений 16
2.3.2 Динамические сети межсоединений 19
3 Введение в программирование с использованием передачи сообщений 22
3.1 Введение в MPI 22
3.1.1 MPI-коммуникация типа «точка-точка» 25
3.1.2 Тупиковые ситуации при коммуникациях типа «точка-точка» 30
3.1.3 Неблокирующие операции и режимы коммуникации 33
3.1.4 Коммуникационный режим 35
3.2 Групповые коммуникационные операции 36
3.3 Группы процессов и коммуникаторы 41
3.3.1 Группы процессов в MPI 41
3.3.2 Топологии процессов 45
3.3.3 Временные и прерывающие процессы 49
4 Введение в потоковое программирование в OpenMP 50
4.1 Проблемы поточной обработки цикла 52
4.2 Условия гонок 52
4.3 Управление общими и приватными данными 53
4.4 Планирование и разбиение циклов 55
4.5 Библиотечные функции ОреnМР 57
4.6 Отладка 58
4.7 Производительность 59
4.8 Основные моменты 61
5 Протокол сеансового уровня SSH 63
6 Удаленный вход на кластер 66
7 Операционная система Linux 68
7.1 Интерфейс ОС Linux 68
7.2 Некоторые команды Linux 68
8 Компилирование последовательных программ (Fortran/C/C++) 71
9 Основные команды 72
10 Работа с кластером 74
Список использованных источников информации 83

Вложенные файлы: 1 файл

Cluster.doc

— 829.00 Кб (Скачать файл)

Например, значение по умолчанию для переменных типа int равно 0. В ОреnМР память может быть объявлена приватной одним из трех способов:

• Используйте предложение private, firstprivate, lastprivate или reduction, чтобы задать переменные, которые должны быть приватными для каждого потока.

• Используйте прагму threadprivate, чтобы  задать глобальные переменные, которые  должны быть приватными для каждого  потока.

• Объявите переменную внутри цикла (а фактически внутри параллельной области) без ключевого слова static. Поскольку статические переменные размещаются статично в определенной области памяти компилятором и компоновщиком, они реально не являются приватными, как другие переменные, объявленные внутри функции (которые размещаются внутри стека функции).

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

#pragma omp parallel for

for ( k - 0; k < 100; k++ ) {

x - array[k];

array[k] = do work(x);

}

Проблему можно устранить одним  из следующих двух способов, Причем в них обоих переменная х объявляется как приватная:

// Этот вариант работает. Переменная х задается как  приватная, 

fpragma omp parallel for private(x)

for ( k = 0; k < 100; k++ )

{

x = array[i];

array[k] - do work(x);

}

// Этот вариант также  работает. Переменная х теперь  приватная. 

#pragma omp parallel for

for ( k - 0: k < 100; k++ )

{

int x; // переменная, объявленная  внутри параллельной 

// конструкции, по умолчанию  является приватной 

х в аrrау[к];

array[к] - do_work(x);

}

Чтобы при использовании ОреnМР обеспечить параллельное выполнение цикла, всегда следует тщательно изучить  все ссылки на ячейки памяти, включая  ссылки из вызываемых функций.

4.4 Планирование и разбиение циклов

Для хорошего разделения нагрузки и, следовательно, для достижения  оптимальной производительности многопоточного приложения требуется  эффективное планирование и разбиение циклов. Конечная цель — обеспечить загрузку исполнительных ядер в течение практически всего времени (при минимальных непроизводительных издержках на планирование, переключение контекста и синхронизацию). При плохо сбалансированной нагрузке некоторые потоки могут завершаться значительно раньше других, что приведет к простою процессорных ресурсов и потере производительности. Чтобы упростить перераспределение нагрузки между ядрами, ОреnМР предлагает четыре схемы планирования, подходящие для многих ситуаций: статическое, динамическое, времени выполнения и управляемое. Компиляторы C++ и Fortran компании Intel поддерживают все эти четыре схемы планирования.

Плохая балансировка нагрузки часто  вызвана различным временем  прохождения  итераций цикла. Обычно путем изучения исходного кода не слишком сложно определить разницу во времени вычисления итераций цикла. В большинстве случаев вы обнаружите, что каждая итерация цикла занимает одно и то же время. Когда это не так, может быть есть возможность найти такие наборы итераций, которые длятся одно и то же время. Например, иногда все четные итерации занимают столько же времени, сколько все нечетные, либо первая половина цикла занимает столько же времени, сколько вторая. Тем не менее в некоторых ситуациях может быть невозможно отыскать наборы итераций, имеющие одинаковое время выполнения. В любом случае вы можете указать информацию о планировании циклов в предложении schedule {схема [.размер _фрагмента]), чтобы компилятор и библиотека времени выполнения могли лучше разделить и распределить итерации цикла по потокам (а следовательно, по ядрам) для оптимальной балансировки нагрузки.

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

Если имеется т итераций и N потоков  в группе, то каждый поток получит по m/N итераций, причем компилятор и библиотека корректно обработают случай, когда т не делится без остатка на N.

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

fpragma omp parallel for

for ( k - 0; k < 1000; k++ ) do_work(k);

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

Информация о планировании цикла  и разделении работы передается в  компилятор и библиотеку времени  выполнения в конструкции for с помощью  предложения schedule.

#pragma omp for schedule(cxeмa [.размер_фрагмента"])

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

4.5 Библиотечные функции ОреnМР

Как вы, возможно, помните, в дополнение к прагмам ОреnМР предоставляет  набор вызовов функций и переменных среды. До настоящего времени описывались только прагмы. Прагмы — это ключ к ОреnМР, поскольку они обеспечивают высочайшую степень простоты и переносимости, к тому же их легко отключить для создания «беспоточной» версии кода.

Наоборот, вызовы OpenMP-функций требуют добавления в программы конструкций условной компиляции, как показано в следующем фрагменте,  если вы хотите создать последовательную версию (кода):

#include <omp.h>

#ifdef _0PENMP

omp__set_num_thread5(4);

#endif

При возникновении сомнений всегда пытайтесь использовать прагмы и  обращайтесь к вызовам функций только в тех случаях, когда это совершенно  необходимо. Для поддержания вызовов функций не забудьте подключить заголовочный файл <omp. h>. Компилятор автоматически создаст связи с нужными библиотеками.

Четыре самые часто используемые OpenMP-функции возвращают общее количество потоков, номер текущего потока, количество имеющихся ядер, логических процессоров или физических процессоров соответственно. Полный список функций библиотеки ОреnМР можно найти в спецификации ОреnМР версии 2.5.

4.6 Отладка

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

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

Проблемы часто являются следствием ситуации гонок. Большинство  ситуаций гонок вызывается общими переменными, которые на самом деле нужно было объявить как private, reduction или threadprivate. Иногда ситуации гонок вызываются отсутствием необходимой синхронизации, такой как защита обновлений общих переменных прагмами critical и atomic. Начните с проверки переменных внутри параллельных областей и убедитесь, что переменные  объявлены приватными там, где это необходимо. Проверьте также функции, вызываемые внутри параллельных конструкций. По умолчанию в ОреnМР переменные, объявленные в стеке, являются приватными, но ключевое слово static языка C/C++ меняет положение переменной — она помещается в глобальную кучу и становится общей для циклов. Предложение default(none), показанное в следующем примере, может помочь в поиске этих трудно обнаруживаемых переменных. Если вы укажете предложение default (none), тогда каждая  переменная должна быть описана с атрибутом разделения данных.

#pragma omp parallel for default(none) private(x,,y) shared(a.b)

Еще одна обычная ошибка — неинициализированные переменные.  Помните, что приватные  переменные не имеют начальных  значений при входе или выходе из параллельной конструкции. Используйте предложения firstprivate и lastprivate (которые уже обсуждались ранее) при их инициализации или копировании. Но делайте это только в случае необходимости, так как подобное копирование увеличивает потери.

Если вы по-прежнему не можете найти  ошибку, возможно, вы работаете со слишком  большим объемом параллельного  кода. Может быть, имеет смысл  выполнять некоторые области  последовательно (путем отключения параллельного кода). Это, по крайней мере, укажет местонахождение ошибки. Простым способом сделать выполнение параллельной области последовательным является использование инструкции i f, которая может быть добавлена к любой параллельной конструкции, как показано в следующих двух примерах:

#pragma omp parallel if(0)

printf("Executed by thread *d\n", omp_get_threadjuim());

#pragma omp parallel for if(0)

for ( x = 0; x < 15; x++ ) fnl(x);

В общем виде инструкция i f может  быть любым скалярным выражением, например (в этом случае при количестве итераций меньше 16 происходит  последовательное выполнение):

#pragma omp parallel for if(n>=16)

for ( k - 0; k < n; k++ ) fn2(k);

Еще один способ — поместить область  ошибочного кода в критическую секцию либо внутрь конструкции single или master. Попытайтесь найти фрагмент кода, который работает внутри критической секции, но не работает без нее либо в однопоточном режиме.

Цель — использовать возможности  ОреnМР по быстрому переключению кода между параллельным и последовательным состояниями для определения положения ошибки. Однако этот подход способен помочь только в том случае, если программа фактически правильно работает в последовательном режиме.

Обратите внимание, что ОреnМР дает вам такую возможность тестирования кода без необходимости существенно  переписывать код. Стандартные методы программирования, используемые в Windows API и Pthreads, безвозвратно привязывают код к многопоточной модели и таким образом делают отладку  гораздо более сложной.

4.7 Производительность

ОреnМР — это простой и переносимый путь к параллельному выполнению ваших приложений или к разработке многопоточных приложений. 

Производительность многопоточного OpenMP-приложения в значительной степени  зависит от следующих факторов:

•  Производительность кода в однопоточном варианте.

•  Относительный объем части программы, выполняемой в параллельном режиме, ее масштабируемость.

•  Использование процессора, эффективное разделение данных,  локальность данных, балансировка нагрузки.

•  Объем операций синхронизации и обмена информацией между потоками.

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

•  Конфликты памяти, вызванные «нормальным» или ложным разделением памяти.

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

По существу, оценка производительности многопоточного кода сводится к ответам на два вопроса: насколько хорошо работает однопоточная версия и насколько хорошо (с наименьшими потерями) можно распределить работу между несколькими процессорами?

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

Информация о работе Основы параллельного программирования вычислительных систем с распределенной и разделяемой памятью