Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Математическое моделирование в астрофизике. В 2 частях. Ч.2. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
12.08.2026
Размер:
873 Кб
Скачать

Проверим работу этих директив на примере из листинга 4.7. Если мы заменим строку 17 на следующий фрагмент кода:

# pragma omp critical

{

s += b[i ];

}

результат станет правильным, но скорость выполнения упадет до скорости без распараллеливания. Таким образом, директива critical работает, но выигрыш от распараллеливания сводится к нулю, потому что потоки вынуждены ждать друг друга. Поскольку строка 17 содержит элементарную арифметическую операцию, можно заменить critical на atomic (при этом фигурные скобки не ставятся). Этот вариант работает чуть быстрее (0,022 c), но выигрыш незначителен.

Проблема решается при помощи редукции (листинг 4.9). При этом директивы omp parallel и omp for разделяются, затем в каждом потоке объявляется переменная sprivate (все переменные, объявленные внутри распараллеленной области, имеют свойства private). Эта переменная хранит сумму элементов массива b, принадлежащих этому потоку. Затем все локальные суммы складываются в окончательную под защитой директивы atomic. Программа с редукцией выдает правильный ответ за 0,002 секунды. Таким образом, удалось на порядок увеличить скорость работы программы благодаря параллельному счету.

Листинг 4.8. Редукция при использовании OpenMP

1

# include

" time .h"

2

# include

" stdio .h"

3

# include

" omp .h"

4

 

 

 

 

5

long

int

N =

100000;

6

 

 

 

 

7

int

main ( int

argc , char ** argv )

8

{

 

 

 

71

9clock_t t1 , t2 ;

10

double * b = new

double [N ];

11

for

( int i

=

0;

i < N;

i ++)

12

b[i]

= 2.0

*

double (i)

/ double (( N - 1) * N);

13double s = 0.0;

14t1 = clock () ;

15# pragma omp parallel

16{

17double sprivate = 0.0;

18# pragma omp for

19 for ( int i = 0; i < N; i ++)

20sprivate += b[i ];

21# pragma omp atomic

22s += sprivate ;

23}

24t2 = clock () ;

25printf ("s = %f working time = %f\n", s, double (t2 - t1) / CLOCKS_PER_SEC );

26}

ВOpenMP нет необходимости делать редукцию вручную. Достаточно

влистинге 4.7 заменить строку 15 на строку

# pragma omp parallel for reduction (+:s)

При этом действия из листинга 4.8 производятся автоматически. Распараллеливание можно комбинировать с векторизацией при нали-

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

for (i = 0; i < 1000; i ++) for (j = 0; j < 4; j ++)

for (k = 0; k < 10000; k++) a[i][j][k] = i * j * k;

72

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

4.4. Технология MPI. Передача сообщений, коллективные взаимодействия процессоров

Современные многопроцессорные кластеры состоят из многочисленных узлов, соединенных шиной InfiniBand. Физически каждый узел не имеет доступа к памяти соседнего, однако может посылать ему сообщения и принимать ответные. В этом случае технология OpenMP может применяться только внутри одного узла. Вместо нее может использоваться, например, технология MPI (Message Passing Interface), основанная на обмене сообщениями между процессорами.

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

Стандарт MPI поддерживает работу с языками C/C++, Fortran, Java. При этом устанавливается одна из библиотек (MPICH, OpenMPI, Intel MPI или другая версия MPI), в шапке программы пишется строка

#include <mpi.h>, компиляция осуществляется при помощи команды mpicc

(mpiicpc, mpic++ и т. п., в зависимости от конкретной реализации). Запуск программы осуществляется командой вида

mpirun -np 1024 ./ myprogram

где ключ -np задает число процессов. Чаще всего в качестве np выбирают это число узлов, умноженное на число ядер в одном узле.

Технология MPI может использоваться и на персональных компьютерах, при этом обмен сообщениями происходит не по InfiniBand, а внутри

73

оперативной памяти. Может показаться, что в этом случае разумнее использовать OpenMP, однако у MPI есть определенные преимущества, например, физическая невозможность конфликта данных.

Рассмотрим простейший пример с использованием MPI в листинге 4.9.

Листинг 4.9. Простейший пример программы

с использованием MPI

1

# include

< stdio .h >

2

# include

<mpi .h >

3

int main ( int argc , char * argv [])

4

{

 

5

6

7

8

9

int rank , size ;

MPI_Init (& argc , & argv );

MPI_Comm_rank ( MPI_COMM_WORLD , & rank ); MPI_Comm_size ( MPI_COMM_WORLD , & size );

printf (" Hello world from process %d of %d \n", rank , size );

10MPI_Finalize ();

11return 0;

12}

Встроке 2 подключается библиотека MPI, в строке 6 инициализируются параллельные процессы (эта команда обязательно должна присутствовать

вначале любой программы с MPI), в строке 7 каждый процесс узнает свой номер, а в строке 8 – общее число процессов. Эта информация выводится на экран, а затем процессы завершаются. Порядок вывода на экран случаен, например после работы программы может появиться следующий текст:

Hello world from process 2 of 4

Hello world from process 1 of 4

Hello world from process 3 of 4

Hello world from process 0 of 4

В простейшем примере увеличения скорости не было. Для того чтобы эффективно распараллелить программу с использованием MPI, разработ-

74

чик должен вручную прописать разделение данных. Например, если изначально предполагался массив a[1024], то при распараллеливании на 4 процесса пользователь задает в алгоритме массив a[256]. Если в нераспараллелленном коде предполагалось, что a[i] = i, то теперь необходимо задать a[i] = i + 256 * rank, где rank – номер процесса. Тогда нулевой процесс будет обрабатывать первые 256 элементов, первый – следующие 256 и т. д.

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

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

Приведем примеры директив обмена сообщениями между процессами. Операции с блокировкой:

MPI_Send ( void * buf , int count , MPI_Datatype datatype , int dest , int msgtag , MPI_Comm comm );

MPI_Recv ( void * buf , int count , MPI_Datatype datatype , int source , int msgtag , MPI_Comm comm , MPI_Status * status );

Они часто используются совместно. Первая процедура инициализирует передачу count элементов типа datatype, лежащих по адресу buf, процессу dest и ждет, когда это сообщение будет принято. До этого последующие

75

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

MPI_COMM_WORLD. Эта константа присутствовала в параметрах функций

MPI_Comm_rank и MPI_Comm_size в листинге 4.9.

Процедура MPI_Recv принимает count элементов типа datatype с идентификатором msgtag от процесса source и размещает их по адресу buf, при этом в переменную status записываются параметры принятого сообщения. Есть возможность принимать сообщения от произвольного процесса (MPI_ANY_SOURCE) и с произвольным идентификатором (MPI_ANY_TAG). Эта процедура, как и MPI_Send, блокирует дальнейшие действия до прихода ожидаемого сообщения. Пример применения функций с блокировкой приведен в листинге 4.10, где для пересылки целочисленных значений используется встроенный тип MPI_INT.

Листинг 4.10. Программа, в которой нулевой процесс

посылает первому целочисленный массив

1

# include

" mpi .h"

2

# include

< stdio .h >

3

 

 

4

int main ( int argc , char ** argv )

5

{

 

6

7

8

9

int rank , size , i; int buffer [10];

MPI_Status status ;

10MPI_Init (& argc , & argv );

11MPI_Comm_size ( MPI_COMM_WORLD , & size );

12MPI_Comm_rank ( MPI_COMM_WORLD , & rank );

13if ( size < 2)

14{

76

15printf (" Please run with two processes .\n");

16fflush ( stdout );

17MPI_Finalize ();

18return 0;

19}

20if ( rank == 0)

21{

22

23

24

for (i = 0;

i

< 10;

i++)

buffer [i]

=

i;

 

MPI_Send (buffer , 10,

MPI_INT , 1, 123 ,

MPI_COMM_WORLD );

 

25

26

27

28

29

30

31

}

if ( rank == 1)

{

for

(i

=

0;

i <

10; i++)

 

buffer [i] = -1;

 

MPI_Recv (buffer , 10,

MPI_INT , 0, 123 ,

MPI_COMM_WORLD , & status );

for

(i

=

0;

i <

10;

i++)

32{

33if ( buffer [i] != i)

34printf (" Error : buffer [%d] = %d but is

35

36

expected to be %d\n", i, buffer [i], i);

}

fflush ( stdout );

37}

38MPI_Finalize ();

39return 0;

40}

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

77

MPI_Isend ( void * buf , int count , MPI_Datatype datatype , int dest , int msgtag , MPI_Comm comm , MPI_Request * request );

MPI_Irecv ( void * buf , int count , MPI_Datatype datatype , int source , int msgtag , MPI_Comm comm , MPI_Request * request );

в которых присутствует параметр request (идентификатор асинхронной операции). Обращаясь к этой переменной, можно проверить, завершилась ли посылка. Проверка осуществляется командой MPI_Test(MPI_Request* request, int* flag, MPI_Status* status). В переменную flag записывается единица, если операция завершена, и ноль, если нет. Также можно дождаться завершения операции с помощью команды MPI_Wait(MPI_Request* request, MPI_Status* status). Переменная status по-прежнему используется для параметров сообщения. Для проверки или ожидания нескольких пересылок можно использовать директивы MPI_Testall и MPI_Waitall. Пример использования операций без блокировки для обмена сообщениями между процессами приведен в листинге 4.11.

Листинг 4.11. Программа, в которой процессы

обмениваются сообщениями по кругу

1

# include

" mpi .h"

 

2

# include

< stdio .h >

 

3

# include

< stdlib .h >

 

4

 

 

 

 

5

int main ( int argc ,

char ** argv )

6

{

 

 

 

7

int numtasks , rank , next , prev , buf [2] , tag1 =

 

1, tag2 =

2;

 

8

MPI_Request

reqs

[4];

9MPI_Status stats [4];

10

 

11

MPI_Init (& argc , & argv );

78

12MPI_Comm_size ( MPI_COMM_WORLD , & numtasks );

13MPI_Comm_rank ( MPI_COMM_WORLD , & rank );

14

 

 

 

 

 

15

prev = rank - 1;

 

 

 

 

16

next = rank + 1;

 

 

 

 

17

if ( rank == 0)

 

 

 

 

18

prev = numtasks - 1;

 

 

 

19

if ( rank == ( numtasks - 1) )

 

 

 

20

next = 0;

 

 

 

 

21

 

 

 

 

 

22

MPI_Irecv (& buf [0] ,

1, MPI_INT , prev ,

tag1 ,

 

MPI_COMM_WORLD ,

& reqs [0]) ;

 

 

 

23

MPI_Irecv (& buf [1] ,

1, MPI_INT , next ,

tag2 ,

 

MPI_COMM_WORLD ,

& reqs [1]) ;

 

 

 

24

 

 

 

 

 

25

MPI_Isend (& rank , 1, MPI_INT ,

prev ,

tag2 ,

 

MPI_COMM_WORLD ,

& reqs [2]) ;

 

 

 

26

MPI_Isend (& rank , 1, MPI_INT ,

next ,

tag1 ,

 

MPI_COMM_WORLD ,

& reqs [3]) ;

 

 

 

27

 

 

 

 

 

28

MPI_Waitall (4 , reqs , stats );

 

 

 

29

printf (" Task %d communicated with tasks %d &

 

%d\n", rank , prev , next );

 

 

30

 

 

 

 

 

31MPI_Finalize ();

32}

Также для обмена данными может использоваться процедура

MPI_Sendrecv ( void * sbuf , int scount , MPI_Datatype stype , int dest , int stag , void * rbuf , int rcount , MPI_Datatype rtype , int source , int rtag , MPI_Comm comm , MPI_Status * status )

Вней первая группа параметров относится к посылаемому сообщению,

авторая – к принимаемому. Пример ее применения приведен в листинге 4.12.

79

Листинг 4.12. Программа с использованием

процедуры MPI_Sendrecv

1

# include

" mpi .h"

2

# include

< stdio .h >

3

 

 

4

int main ( int argc , char ** argv )

5

{

 

6

7

8

9

int myid , numprocs , left , right ; int buffer [10] , buffer2 [10];

MPI_Status status ;

10MPI_Init (& argc , & argv );

11MPI_Comm_size ( MPI_COMM_WORLD , & numprocs );

12MPI_Comm_rank ( MPI_COMM_WORLD , & myid );

13

14

15

16

17

18

19

right = ( myid + 1) % numprocs ; left = myid - 1;

if ( left < 0)

left = numprocs - 1;

MPI_Sendrecv ( buffer , 10 , MPI_INT , left , 123 , buffer2 , 10 , MPI_INT , right , 123 ,

MPI_COMM_WORLD , & status );

20

21MPI_Finalize () ;

22return 0;

23}

Также существуют процедуры коллективных пересылок, в которых задействованы все процессы коммуникатора.

1. Рассылка сообщения от source всем процессам:

MPI_Bcast ( void * buf , int count , MPI_Datatype datatype ,

80

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]