Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Численные методы для физиков-теоретиков. II
.pdf
2. ЭЛЕМЕНТАРНЫЕ СВЕДЕНИЯ О ПАРАЛЛЕЛЬНЫХ ВЫЧИСЛЕНИЯХ 11
Эту программу следует откомпилировать:
mpicc -o test test.c
Заметим, что для нашей игрушечной программы такая команда компиляции
вполне подходит, но для настоящей программы следовало бы написать чтонибудь вроде:
mpicc -o test -O3 -ffast-math -march=i686 test.c -lm
После компиляции программу можно запустить на 1-м процессоре:
mpirun -np 1 test
В результате получится выдача:
Total processors number: 1
Starting process N 0 on oryx1.bog.msu.ru
Total sum: 1, max: 1, time: 0.04
Эту же программу можно запустить и на 4-х процессорах:
mpirun -np 4 test
Тогда получится выдача:
Total processors number: 4
Starting process N 0 on oryx1.bog.msu.ru
Starting process N 2 on oryx3.bog.msu.ru
Starting process N 3 on oryx4.bog.msu.ru
Starting process N 1 on oryx2.bog.msu.ru
Total sum: 10, max: 4, time: 0.03
Ну а теперь дадим необходимые объяснения.
Как видно из приведенного примера, прежде всего в обычный перечень
директив #include необходимо добавить #include<mpi.h>.
Инициализация протокола MPI производится функцией
MPI_Init(&npar, &par);
Начиная с вызова этой функции, можно использовать другие команды MPI.
Аргументы функции суть адреса аргументов функции main.
3
Завершение
протокола MPI производится функцией
MPI_Finalize();
3
Полезно напомнить, что аргументы функции main описывают параметры командной строки при запуске программы: «npar» — число параметров командной строки, «par [0]» — строка,
содержащая имя исполняемого файла, «par [1]» — строка, содержащая первый параметр в командной строке, «par [2]» — строка, содержащая второй параметр, «par [npar−1]» — строка,
содержащая последний параметр.

12 ЧИСЛЕННЫЕ МЕТОДЫ ДЛЯ ФИЗИКОВ-ТЕОРЕТИКОВ
Начиная с момента вызова этой функции, использовать команды MPI уже
нельзя.
Первое, что должна сделать распараллеленная программа, работающая
на данном процессоре, это определить, сколько всего задействовано процессоров. Количество процессоров определяется с помощью функции
MPI_Comm_size( MPI_COMM_WORLD, &totnum);
После вызова этой функции целая переменная totnum будет равна полному числу процессоров. Первый аргумент функции введен из следующих
соображений: в MPI есть возможность определять группы процессоров, являющихся подмножествами множества всех задействованных процессоров.
Мы в дальнейшем будем игнорировать эту возможность, так что единственная группа, с которой мы будем иметь дело — это группа всех процессоров,
которая называется MPI_COMM_WORLD. Так что во всех функциях, где требуется имя группы, мы будем использовать MPI_COMM_WORLD.
Кроме того, надо определить, каков индивидуальный номер данного
процессора. Это делается с помощью функции
MPI_Comm_rank( MPI_COMM_WORLD, &mynum);
После вызова этой функции целая переменная mynum будет равна номеру
данного процессора, а именно: на одном из процессоров она будет равна 0,
на другом — 1, на третьем — 2, ...,напоследнем—(totnum − 1).
Наконец, можно определить имя данного процессора (для MPI на базе
локальной сети это попросту host name). Это делается с помощью функции
MPI_Get_processor_name(name, &len);
После вызова этой функции в строковой переменной name, которую надлежит определять как
char name[MPI_MAX_PROCESSOR_NAME];
будет содержаться имя процессора (для компьютерного кластера это будет
host name в формате mmm.hhh.ru), а целая переменная len будет равна
длине имени данного процессора.
Нередко выяснить имя машины действительно необходимо. Например, если задействованные компьютеры имеют разные производительности
и разные объемы памяти, то необходимо выделить каждому процессору
долю задачи, соответствующую его параметрам. Если поделить задачу поровну, то скорость счета будет определяться скоростью самого медленного
процессора, а остальные будут его ждать, т.е. простаивать.
Разумеется, должны существовать команды, реализующие обмен информацией между процессорами. Данный процессор (пусть его номер равен

2. ЭЛЕМЕНТАРНЫЕ СВЕДЕНИЯ О ПАРАЛЛЕЛЬНЫХ ВЫЧИСЛЕНИЯХ 13
numproc0) может послать numdat штук целых чисел, размещенных в массиве arrdat (число numdat может быть меньше декларированной длины
массива), процессору с номером numproc1, если использовать функцию
MPI_Send(arrdat, numdat, MPI_INT, numproc1,
index, MPI_COMM_WORLD);
Тип посылаемых данных указывается с помощью аргумента MPI_INT.
При необходимости послать содержимое одной целой переменной dat
имя массива arrdat
4
надо заменить на адрес переменной &dat,ачисло
numdat положить равным 1. Совершенно аналогично можно послать массив double-чисел, достаточно только заменить MPI_INT на MPI_DOUBLE.
Аргумент index (это переменная типа int) есть «метка» сообщения.
Метка, придаваемая сообщению, нередко действительно необходима: дело в том, что при посылке двух сообщений подряд не гарантируется, что
они придут в том же порядке, в котором были посланы. Чтобы можно было
различать разные сообщения, они снабжаются метками.
Чтобы процессор с номером numproc1 принял это сообщение, необ-
ходимо использовать функцию
MPI_Recv(arrdat, numdat, MPI_INT, numproc0,
index, MPI_COMM_WORLD, &stat);
В принципе нет необходимости принимать данные в тот же самый массив
arrdat, в котором они находились на процессоре с номером numproc0.
Обязаны совпадать только тип данных, их количество и метка сообщения
index. Дополнительный аргумент &stat содержит диагностику ошибок,
которые могут произойти при приеме сообщения. Опыт показывает, что
пользы от этой диагностики немного, поэтому мы не будем ее описывать.
Приведенные функции являются «блокирующими» функциями приема и передачи. Это означает, что программа ожидает, пока сообщение не
будет отослано или не будет принято, и только после этого выполнение
программы продолжится. Следует иметь в виду, что существуют и другие типы функций приема и передачи («неблокирующие», «синхронные»
и т. д.), однако опыт показывает, что в практической работе можно обойтись и приведенными здесь функциями.
В нашем примере программа выполняет следующие действия: процессор номер 0 присваивает переменной dat значение 1 и посылает его процессору номер 1. Остальные процессоры в это время простаивают, поскольку
дожидаются своих сообщений. Процессор номер 1 получает сообщение от
процессора номер 0 (одно целое число, равное 1) и помещает его в «свою»
4
Полезно напомнить, что имя массива есть адрес первого элемента массива.

14 ЧИСЛЕННЫЕ МЕТОДЫ ДЛЯ ФИЗИКОВ-ТЕОРЕТИКОВ
переменную dat. Затем увеличивает переменную dat на 1 и посылает
получившееся число 2 процессору номер 2. И так далее, вплоть до последнего процессора номер (totnum − 1), который получит от предыдущего
процессора число (totnum − 1),увеличитегодоtotnum, но уже никуда
не пошлет. Такова довольно бессмысленная цепочка обмена сообщениями
в этой программе.
Иногда возникает необходимость послать одно и то же сообщение от
одного из процессоров всем остальным. Разумеется, можно было бы заставить этот один процессор рассылать сообщения всем остальным с помощью
многократного вызова функции MPI_Send, а остальные — принимать эти
сообщения с помощью MPI_Recv. Однако гораздо удобнее воспользоваться функцией
MPI_Bcast(arrdat, numdat, MPI_INT, numproc0,
MPI_COMM_WORLD);
Смысл аргументов arrdat, numdat и MPI_INT очевиден, а аргумент
numproc0 указывает, что именно процессор с номером numproc0 рас-
сылает данные всем остальным процессорам. После вызова функции первые numdat элементов массива arrdat становятся идентичными на всех
процессорах — они становятся такими же, как на процессоре с номером
numproc0.
Ясно, что в разрезанной на кусочки задаче, скорее всего, возникнет
необходимость так или иначе объединить данные, полученные для каждого
отдельного кусочка: или найти полную энергию, которая есть сумма энергий
каждого из кусочков задачи, или найти максимум погрешности, которая есть
максимум среди максимальных погрешностей для каждого кусочка, и т. п.
Очевидно, что эти операции легко реализовать с помощью функций
MPI_Send и MPI_Recv. Однако, удобнее воспользоваться стандартной
функцией
MPI_Reduce(arrdat,totdat,numdat,MPI_INT,
MPI_SUM,numproc0,MPI_COMM_WORLD);
В результате вызова этой функции произойдет следующее: все элементы массива totdat на процессоре с номером numproc0 с индексами
k =0, 1,...(numdat − 1) примут значения
N−1
totdat[k]=
где N = totnum — полное число процессоров, а arrdat
arrdatn[k],
n=0
[k] — значение
n
k-го элемента массива arrdat на n-ом процессоре. Оба массива должны

2. ЭЛЕМЕНТАРНЫЕ СВЕДЕНИЯ О ПАРАЛЛЕЛЬНЫХ ВЫЧИСЛЕНИЯХ 15
иметь тип int. Заметим, что элементы массива totdat меняются только на
процессоре с номером numproc0, на остальных процессорах они остаются
неизменными.
Помимо очевидных аргументов arrdat, totdat, numdat, MPI_INT,
здесь есть еще и аргумент MPI_SUM, указывающий на тип операции, кото-
рую надлежит проделать над данными. Кроме суммирования (MPI_SUM),
существует MPI_PROD — умножение, MPI_MAX — максимум, MPI_MIN —
минимум.
В приведенном нами примере результатом выполнения первого
вызоваMPI_Reduce (с аргументом MPI_SUM)будет
N
m = N (N +1)/2.
m=1
Соответственно, результатом второго вызова MPI_Reduce (с аргументом
MPI_MAX)будет
max
m=1...N
m = N.
Если нам необходимо разослать результат вызова MPI_Reduce
по всем процессорам, то можно, конечно, воспользоваться функцией
MPI_Bcast. Удобнее, однако, использовать функцию
MPI_Allreduce(arrdat,totdat,numdat,MPI_INT,
MPI_SUM,MPI_COMM_WORLD);
которая отличается от функции MPI_Reduce только тем, что помещает
результаты вычислений в массив totdat не на одном процессоре, а сразу
на всех задействованных процессорах.
Упомянем и некоторые другие функции.
Функция
MPI_Barrier(MPI_COMM_WORLD);
останавливает выполнение программы на данном процессоре до тех пор,
пока остальные процессоры также не доберутся до этой функции. Тем самым реализуется синхронизация в выполнении программы. Эта функция,
безусловно, полезна при отладке, но в отлаженной программе, где используются блокирующие операторы MPI_Send и MPI_Recv, она не особенно
полезна.
Функция
MPI_Abort(MPI_COMM_WORLD,number);

16 ЧИСЛЕННЫЕ МЕТОДЫ ДЛЯ ФИЗИКОВ-ТЕОРЕТИКОВ
прерывает выполнение программы и возвращает системе целое число
number вкачествекодазавершения(кодаошибки).
5
Опять-таки, это довольно полезная при отладке функция, но завершать таким образом работу
отлаженной программы не рекомендуется (под Linux это вызывает огромное
количество системных ошибок типа broken pipe...).
Существует довольно полезная функция
MPI_Wtime();
которая возвращает double-число, равное времени (в секундах), которое
миновало с момента инициализации протокола MPI.
Необходимо подчеркнуть, что мы упомянули лишь небольшую часть
из более чем 300 функций и констант, определенных в MPI, однако, даже
этих элементарных сведений вполне достаточно для написания реальных
распараллеленных программ.
3. Задача Коши для обыкновенных дифференциальных
уравнений
Постановка задачи Коши для системы обыкновенных дифференциальных уравнений (ОДУ) вполне тривиальна. Прежде всего очевидно, что любая система ОДУ произвольного порядка может быть переписана в виде
системы ОДУ первого порядка:
d
x
(t)=f
i
dt
Переменную t мы будем в дальнейшем называть «временем», кроме того,
мы часто будем использовать векторные обозначения:
что нет необходимости отдельно рассматривать случай, когда правая часть
в системе ОДУ явно зависит от времени, поскольку при этом достаточно
определить «лишнюю» переменную x
уравнению
начальных данных x(t = t
d
x
(t)=1. Задача Коши состоит в том, что при известных
n+1
dt
вплоть до некоторого момента времени t
Решение дифференциальных уравнений (наряду с обработкой данных)
почему-то считается основным разделом курса численных методов, так что
большинство руководств уделяет основное внимание именно этому вопросу.
Мы, напротив, будем довольно лаконичны.
5
Это то же самое, что в функции main написать return number;.
x
(t),...xn(t),i=1,...n.
i
1
d
x =f(x). Заметим,
dt
(t) ≡ t, которая удовлетворяет
n+1
) необходимо проследить эволюцию системы
1
инайтиx(t2).
2

3. ЗАДАЧА КОШИ ДЛЯ ОБЫКНОВЕННЫХ ДИФ. УРАВНЕНИЙ 17
Мы не станем рассматривать методы, которые применимы только для
некоторого класса ОДУ, в частности, мы не будем обсуждать метод Нумерова. Этот метод действительно довольно эффективен для уравнений второго
порядка, но он не настолько превосходит по эффективности универсальные
методы, чтобы однозначно рекомендовать его применение. Вообще, при
практической работе зачастую целесообразнее использовать готовую отлаженную программу, использующую общий метод, чем заново создавать новую (пусть и чуть более эффективную) программу для каждого отдельного
случая.
3.1. Методы Рунге-Кутта
Группа методов, которые мы будем называть методами Рунге-Кутта,
основана на очень простой идее: мы хотим сдвинуться по времени на достаточно маленькую величину h (если правая часть ОДУ порядка единицы,
то h =10
−2
...10−4, в зависимости от заказанной точности). Мы исполь-
зуем разложение в ряд Тейлора
x(t + h)=x(t)+˙x(t)
1
h
1!
+¨x(t)
2
h
2!
+ ··· ,
при этом
˙
x(t)=fx(t),
¨x
(t)=∂
i
ит.д.
Вычисление производных
щупывания» траектории на отрезке [t, t + h], т. е. вычисления правой части
x
j
f
i
x(t)˙x
d
dt
(t)=∂
j
n
x(t) осуществляется посредством «про-
n
x
f
j
x(t)f
i
j
x(t)
ОДУ в промежуточных точках. Разные схемы типа Рунге-Кутта отличаются
друг от друга как порядком (т.е. количеством промежуточных точек и, следовательно, количеством вычисленных производных), так и положением
промежуточных точек.
3.1.1. Разнообразные n-точечные схемы
Простейшая схема типа Рунге-Кутта — это схема второго порядка. Мы
вычисляем правую часть ОДУ в двух точках:
2
,δx
1
,δx
2
=0,
1
= ha
,
f
2
1
f
=fx(t)+δx
1
f
=fx(t)+δx
2

18 ЧИСЛЕННЫЕ МЕТОДЫ ДЛЯ ФИЗИКОВ-ТЕОРЕТИКОВ
где a2— произвольная константа. При этом
=˙xi(t),
(f
1)i
(f
≈ (f1)i+ a2h∂
2)i
x
f
j
x(t)f
i
x(t)=˙x
j
(t)+a2h¨xi(t).
i
Ищем смещение по траектории в виде
δx = x(t + h) −x(t) ≈ w
hf1+ w2hf2.
1
Следовательно,
откуда
˙
x(t)
h
1!
1
+¨x(t)
2
h
= w
h ˙xi(t)+w2h˙xi(t)+a2h¨xi(t),
2!
1
w
1
=
2
2a
2
w1=1−w
2
с точностью до слагаемых O(h2) при произвольном a2.
Все семейство схем второго порядка с разнообразными a
имеет оди-
2
наковый порядок точности. Однако для каждой данной системы ОДУ фактическая погрешность будет (хотя и не слишком сильно) зависеть от a
Причем для разных задач мы будем получать разные зависимости от a
Из эстетических (симметрийных) соображений можно выбрать a
что соответствует δx = hf
δx =(h/2)f
+(h/2)f2. К сожалению, гипотеза о том, что симметричные
1
.Можнотакжевыбратьa2=1, что дает
2
=1/2,
2
схемы могут быть на один порядок точнее несимметричных схем (кажется,
что коэффициенты при нечетных степенях h могли бы обращаться в ноль,
как это имеет место в квадратурных формулах — см. часть I, раздел «Численное интегрирование»), опровергается непосредственной проверкой.
Разумеется, пользоваться схемами второго порядка не следует.
Совершенно аналогичным образом может быть построена схема третьего порядка. Правая часть ОДУ вычисляется в трех точках:
f
=fx(t)+δx
k
,
k
где
δx
=0,δx2= ha
1
,δx3= h(a3− b3)f1+ hb
f
2
1
.
f
3
2
Смещение по траектории ищем в виде
δx = w
f
1
1
+ w
f
2
2
+ w
.
f
3
3
.
2
.
2

3. ЗАДАЧА КОШИ ДЛЯ ОБЫКНОВЕННЫХ ДИФ. УРАВНЕНИЙ 19
Сравнивая ряд Тейлора с полученным выражением, получаем либо
2 − 3a
w
=
2
6a2(a2− a3)
при произвольных a
w
=1/(4b3) при произвольном b3;либоa2=2/3, a3=0, w2=3/4,
3
w
=1/(4b3) при произвольном b3.Вовсехслучаяхw1=1−w2−w3.Эти
3
3
,w
, a3;либоa2=2/3, a3=2/3, w2=3/4 − 1/(4b3)
2
наборы параметров обеспечивают точность O(h
=
3
6a3(a3− a2)
2 − 3a
2
,b
3
3
).
=
− a3)
a
3(a2
a2(3a2− 2)
Для каждой конкретной системы ОДУ фактическая погрешность зависит от выбранных параметров, но не слишком сильно. Кроме того, для
разных задач эта зависимость оказывается разной.
Разумеется, пользоваться схемой третьего порядка не следует.
Точно так же строится схема четвертого порядка:
f
=fx(t)+δx
k
,
k
где
δx
=0,δx2= ha
1
δx
4
,δx3= h(a3− b3)f1+ hb
f
2
1
= h(a4− b4− c4)f1+ hb
+ hc
f
4
2
f
4
,
f
3
2
.
3
Смещение по траектории ищем в виде
δx = w
f
1
1
+ w
f
2
2
+ w
f
3
3
+ w
.
f
4
4
Сравнивая ряд Тейлора с полученным выражением, легко получить решение
для параметров b
, b4, c4, w1, w2, w3, w4при произвольных a2, a3, a4.
3
Решение это не представляет особого интереса и слишком громоздко, чтобы
его здесь приводить. Мы ограничимся частным случаем, удовлетворяющим
условию симметрии:
a
=1/2 − m, a3=1/2+m, a4=1
2
при произвольном m.Тогда
b
w
1
1+2m
=
3
2 − 4m
= w4=
,b
c
4
1 − 12m
6 − 24m
4
=
2
2
2m(1 + 2m)
=
(1 − 2m)(1 −12m2)
1 − 2m
1 − 12m
,
2
,w2= w3=
1
3 − 12m
,
.
2

20 ЧИСЛЕННЫЕ МЕТОДЫ ДЛЯ ФИЗИКОВ-ТЕОРЕТИКОВ
«Классическая» схема, которую обыкновенно и называют методом
Рунге-Кутта, получается при m =0:
=fx(t),f2=fx(t)+
f
1
f
=fx(t)+
3
δx =
h
f
2
2
h
h
f
+
1
6
3
,f
=fx(t)+hf
4
h
f
+
2
3
h
,
f
1
2
,
3
h
f
f
+
3
.
4
6
При m =1/6 получим:
=1/3,a3=2/3,a4=1,b3=1,b4= −1,
a
2
c
=1,w1= w4=1/8,w2= w3=3/8.
4
При m =1/14 получим:
=3/7,a3=4/7,a4=1,b3=2/3,b4= −14/69,
a
2
c
=21/23,w1= w4=23/144,w2= w3=49/144.
4
И так далее.
Легко проверить, что ни по точности, ни по скорости счета эти схемы не
слишком отличаются друг от друга (для любой реальной задачи экономия
трех умножений в «классической» схеме незаметна на фоне вычисления
правой части ОДУ). Единственным достоинством «классической» схемы
является простота соответствующих выражений.
Следует ли применять схемы четвертого порядка? Обыкновенно утверждается, что схемы четвертого порядка являются оптимальными по производительности, т. е. при заданной точности они соответствуют минимальному количеству операций, затрачиваемых на единичный сдвиг t → t +1.
По нашему мнению, схемы пятого порядка все же в среднем более производительны.
Кроме того, как правило, утверждается, что ни в коем случае не следует
применять схемы Рунге-Кутта без контроля точности и без адаптивного
изменения шага. В общем случае это безусловно правильное утверждение.
Однако если речь идет о задаче, в которой общий характер поведения
правой части ОДУ известен, и эта правая часть определенно не будет испытывать резких изменений вдоль траектории, то вполне можно обойтись
без адаптивного изменения шага. Точность легко оценить по небольшому
участку траектории, пройдя его один раз с выбранным шагом, а второй
раз — с шагом, уменьшенным в 1.5 или 2 раза, и сравнив ответы.
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
