Главная страница

1. Лабораторная работа 01 Вводные замечания


Скачать 104.44 Kb.
Название1. Лабораторная работа 01 Вводные замечания
Дата07.10.2021
Размер104.44 Kb.
Формат файлаpdf
Имя файла00.pdf
ТипЛабораторная работа
#243323

1. Лабораторная работа №0
1.1. Вводные замечания
Каждая лабораторная состоит из нескольких заданий. Выполнение каждого задания заключается в напи- сании одной или нескольких функций или подпрограмм на языке Fortran. Перед началом выполнения заданий прочтите теоретическую часть, где приводятся необходимые формулы и даются дополнительные пояснения. Всю информацию по языку Fortran и OpenMP можно найти в лекциях и методическом пособии.
При написании программы следует организовать код определенным образом. Для примера смотрите тре- нировочную лабораторную № 0.
В каждом задании необходимо провести замеры времени работы функций и добиться того, чтобы парал- лельная версия работала быстрее последовательной. Кроме того, при увеличении количества потоков время выполнения программы должно уменьшаться. Как производить замеры и как нарисовать графики времени выполнения, ускорения и эффективности смотрите в нижеследующем описании лабораторной №0. Продемон- стрируем образец выполнения лабораторной работы на примере двух простейших заданий. Основное внимание уделим тому, как правильно оформлять код и тестировать созданную программу. Принципы оформления и тестирования в своей основе не зависят от выбранного языка программирования и их понимание пригодится при написании программ на любом другом языке программирования.
1.2. Задание №1
Написать функцию или подпрограмму, которая решает квадратное уравнение 𝑎𝑥
2
+ 𝑏𝑥 + 𝑐 = 0
с действи- тельными коэффициентами 𝑎, 𝑏 и 𝑐. Протестировать программу на корректность работы.
1.3. Задание №2
Написать функцию, находящую сумму элементов одномерного массива с использованием OpenMP. Про- тестировать функцию на корректность и на производительность, то есть удостоверится, что при увеличении количества потоков время вычислений уменьшается.
1.4. Общие пояснения
Вышеприведенные задания крайне просты и обычно даются учащимся в самом начале обучения програм- мирования. Однако мы подойдем к выполнению данных заданий используя методику написания крупных программ.
• В первую очередь мы организуем программу следуя принципам структурного программирования: разо- бьем ее на функции, подпрограммы и исходные файлы.
• Во вторую очередь мы напишем тесты, автоматически проверяющие наш код, что позволит отловить большинство потенциальных ошибок.
Начнем с того, что создадим каталог lab00 и все исходные файлы, касающиеся лабораторной №0 будем хранить исключительно в нем. Далее мысленно представим из каких частей будет состоять наша первая про- грамма, решающая квадратное уравнение.
• Непосредственное решение уравнения. Можно оформить его в виде главной программы, но так как мы хотим далее протестировать этот код, указывая разные коэффициенты 𝑎, 𝑏 и 𝑐, то выделить его в виде отдельной подпрограммы или функции.
• Проверка работы нашей программы (тестирование). На первый взгляд проверять в такой простой про- грамме нечего, но далее мы увидим, что это не так.
Для второго задания программа кроме вышеперечисленных двух частей будет содержать еще две.
• Проверка эффективности работы параллельной версии.
1

• Визуализация результатов замеров быстродействия параллельной версии программы, то есть построение графиков времени выполнения, ускорения и эффективности.
Кроме исходного кода в процессе сборки и компиляции программы могут появится временные служебные файлы .mod и .o, непосредственно исполняемые файлы программ, а также файлы с изображениями графиков.
Даже в такой небольшой программе как наша, будет неудобно, если все эти файлы будут находиться в одном каталоге. Поэтому создадим следующую иерархию каталогов.
• bin — в данный каталог будем сохранять созданные исполняемые файлы; изначально пуст.
• imgs— в данный каталог будем сохранять графики времени выполнения, ускорения и эффективности;
изначально пуст.
• mod — в данный каталог сохраняются созданные компилятором mod файлы; изначально пуст.
• src — в данном каталоге сохраним файлы с исходным кодом, который непосредственно ответственен за выполнение заданий.
• test — в данном каталоге создадим файлы с исходным кодом тестирующих программ.
Так как наши программы будут сопровождаться проверками на корректность выполнения и на произво- дительность, разумно команды компиляции и запуска не вводить каждый раз с консоли, а автоматизировать.
Существует масса утилит для такой автоматизации (системы сборки). Для Unix (GNU Linux, macOS) простей- шей является утилита Make, а для Windows можно обойтись bat или powershell скриптом. Поэтому у нас в каталоге будут присутствовать еще два файла.
• Makefile.bat — сценарий для запуска компиляции и тестирующих программ под Windows.
• Makefile — make файл для сборки программ и запуска тестирования под GNU Linux и macOS.
Незабудем также, что для второго задания следует нарисовать ряд графиков. Будем использовать язык Python с библиотекой Matplotlib. Скрипт назовем plot.py и напишем так, чтобы с минимальными изменениями он работал и для других заданий.
Скриптами для сборки можно и не пользоваться, однако они существенно облегчат работу.
1.5. Выполнение задания №1
Начнем выполнение задания №1. Для вычисления корней квадратного уравнения 𝑎𝑥
2
+𝑏𝑥+𝑐 = 0
необходимо вычислить дискриминант 𝐷 = 𝑏
2
− 4𝑎𝑐
и далее корни по формуле
𝑥
1,2
=
−𝑏 ±

𝐷
2𝑎
Следует отдельно учесть случаи 𝐷 > 0, 𝐷 < 0 и 𝐷 = 0. Так как Fortran поддерживает комплексные числа,
то наша программа может выдать корректный результат и при 𝐷 < 0, а не сообщение об отсутствии дей- ствительных корней. Кроме того, с вычислительной точки зрения эффективней будет поделить уравнение на коэффициент 𝑎 и решать его в виде 𝑥
2
+ 𝑝𝑥 + 𝑞 = 0
, где 𝑝 = 𝑏/𝑎 и 𝑞 = 𝑐/𝑎. В результате подпрограмма решающая квадратное уравнение может выглядить следующим образом.
31
subroutine
quadroots(a, b, c, real_roots, complex_roots, is_real)
32
implicit none
33
real
(real64),
intent
(in)
::
a, b, c
34
real
(real64),
intent
(out),
dimension
(
1
:
2
)
::
real_roots
35
complex
(real64),
intent
(out),
dimension
(
1
:
2
)
::
complex_roots
36
logical
,
intent
(out)
::
is_real
37 38
real
(real64)
::
p, q
39
real
(real64)
::
D
40 2

41
p
=
b
/
a
42
q
=
c
/
a
43
D
=
p
*
p
-
4.0
*
q
44
is_real
=
.true.
45
if
(D
>
0
)
then
46
real_roots(
1
)
=
0.5
*
(
- p
- sqrt
(D))
47
reaL_roots(
2
)
=
0.5
*
(
- p
+
sqrt
(D))
48
else if
(D
<
0
)
then
49
is_real
=
.false.
50
complex_roots(
1
)
=
0.5
*
(
- p
- sqrt
(
cmplx
(D,
kind
=
8
)))
51
complex_roots(
2
)
=
0.5
*
(
- p
+
sqrt
(
cmplx
(D,
kind
=
8
)))
52
else if
(
abs
(D)
<
epsilon
(
1.0d0
))
then
53
real_roots(
1
)
= -
0.5
*
p
54
real_roots(
2
)
= -
0.5
*
p
55
end if
56 57
end subroutine
quadroots
Рассмотрим какие аргументы принимает данная подпрограмма.
• Аргументы a, b, c соответствуют коэффициентами уравнения 𝑎, 𝑏 и 𝑐 и имеют тип real(real64) или иначе double precision. Подпрограмма не должна в процессе своей работы каким либо образом моди- фицировать аргументы a, b, c, на что указывает атрибут intent(in).
• Аргумент real_roots является одномерным массивом из двух элементов типа double precision. В этот аргумент подпрограмма должна записать результат вычисления корней уравнения, в случае если корни действительные.
• Аргумент complex_roots является одномерным массивом из двух элементов типа complex(real64). В
этот аргумент подпрограмма должна записать результат вычисления корней уравнения, в случае если корни комплексные или чисто мнимые.
• Аргумент is_real имеет тип logical(kind=1). Ему присваивается значение .true. в случае если корни уравнения действительные и .false. в случае комплексных корней.
Обратите внимание, что проверка на равенство нулю дискриминанта сделана как
52
else if
(
abs
(D)
<
epsilon
(
1.0d0
))
then
так как для действительных чисел необходимо учитывать погрешность представления с помощью чисел с плавающей запятой.
Дополним нашу подпрограмму еще двумя функциями, вычисляющими значение квадратного трехчлена.
Эти функции понадобятся при тестировании результатов вычисления.
10
! Вычисление квадратного трехчлена
11
pure function
real_p2(a, b, c, x)
12
implicit none
13
real
(real64),
intent
(in)
::
a, b, c
14
real
(real64),
intent
(in)
::
x
15
real
(real64)
::
real_p2 16
! Оптимизируем число операций
17
real_p2
=
(x
+
(b
/
a))
*
x
+
(c
/
a)
18
end function
real_p2 19 20
! Вычисление квадратного трехчлена от комплексных чисел
21
pure function
complex_p2(a, b, c, x)
22
implicit none
3

23
real
(real64),
intent
(in)
::
a, b, c
24
complex
(real64),
intent
(in)
::
x
25
real
(real64)
::
complex_p2 26
! оптимизируем число операций
27
complex_p2
=
dble
((x
+
(b
/
a))
*
x
+
(c
/
a))
28
end function
complex_p2
Сохраним все процедуры в файле quadequation.f90, заключив их в модуль quadequation для последую- щего удобного вызова. В результате получим следующий исходный код.
1
module
quadequation
2
use
iso_fortran_env,
only
: int32, int64, real32, real64 3
implicit none
4
private
5 6
! Доступные вовне функции и подпрограммы
7
public ::
real_p2, complex_p2, quadroots
8 9
contains
10
! Вычисление квадратного трехчлена
11
pure function
real_p2(a, b, c, x)
12
implicit none
13
real
(real64),
intent
(in)
::
a, b, c
14
real
(real64),
intent
(in)
::
x
15
real
(real64)
::
real_p2 16
! Оптимизируем число операций
17
real_p2
=
(x
+
(b
/
a))
*
x
+
(c
/
a)
18
end function
real_p2 19 20
! Вычисление квадратного трехчлена от комплексных чисел
21
pure function
complex_p2(a, b, c, x)
22
implicit none
23
real
(real64),
intent
(in)
::
a, b, c
24
complex
(real64),
intent
(in)
::
x
25
real
(real64)
::
complex_p2 26
! оптимизируем число операций
27
complex_p2
=
dble
((x
+
(b
/
a))
*
x
+
(c
/
a))
28
end function
complex_p2 29 30
! Вычисление корней квадратного уравнения
31
subroutine
quadroots(a, b, c, real_roots, complex_roots, is_real)
32
implicit none
33
real
(real64),
intent
(in)
::
a, b, c
34
real
(real64),
intent
(out),
dimension
(
1
:
2
)
::
real_roots
35
complex
(real64),
intent
(out),
dimension
(
1
:
2
)
::
complex_roots
36
logical
,
intent
(out)
::
is_real
37 38
real
(real64)
::
p, q
39
real
(real64)
::
D
40 41
p
=
b
/
a
42
q
=
c
/
a
43
D
=
p
*
p
-
4.0
*
q
44
is_real
=
.true.
45
if
(D
>
0
)
then
4

46
real_roots(
1
)
=
0.5
*
(
- p
- sqrt
(D))
47
reaL_roots(
2
)
=
0.5
*
(
- p
+
sqrt
(D))
48
else if
(D
<
0
)
then
49
is_real
=
.false.
50
complex_roots(
1
)
=
0.5
*
(
- p
- sqrt
(
cmplx
(D,
kind
=
8
)))
51
complex_roots(
2
)
=
0.5
*
(
- p
+
sqrt
(
cmplx
(D,
kind
=
8
)))
52
else if
(
abs
(D)
<
epsilon
(
1.0d0
))
then
53
real_roots(
1
)
= -
0.5
*
p
54
real_roots(
2
)
= -
0.5
*
p
55
end if
56 57
end subroutine
quadroots
58
end module
quadequation
После того, как написали процедуру можно приступать к ее проверке. Необходима подавать на вход процедуре quadroots разные коэффициенты 𝑎, 𝑏 и 𝑐 и проверять правильность вычисленных корней. Даже для такой про- стой программы как наша можно придумать большое количество проверок. Проверяющая программа может выглядеть следующим образом.
1
include
"../src/quadequation.f90"
2 3
! Тестирование решений квадратного уравнения
4
subroutine
test(a, b, c)
5
use
iso_fortran_env,
only
: int32, int64, real32, real64 6
use
quadequation
7
implicit none
8 9
real
(real64),
intent
(in)
::
a, b, c
10 11
real
(real64),
dimension
(
1
:
2
)
::
x
12
complex
(real64),
dimension
(
1
:
2
)
::
cx
13
logical
::
is_real
14 15
is_real
=
.true.
16 17
call
quadroots(a, b, c, x, cx, is_real)
18 19
! действительные корни
20
if
(is_real)
then
21
! проверяем корни
22
print
*
,
"test 01: "
,
abs
(real_p2(a, b, c, x(
1
)))
<=
epsilon
(
1.0d0
)
23
print
*
,
"test 02: "
,
abs
(real_p2(a, b, c, x(
2
)))
<=
epsilon
(
1.0d0
)
24
! теорема Виета
25
print
*
,
"test 03: "
,
abs
(x(
1
)
+
x(
2
)
+
b
/
a)
<=
epsilon
(
1.0d0
)
26
print
*
,
"test 04: "
,
abs
(x(
1
)
*
x(
2
)
- c
/
a)
<=
epsilon
(
1.0d0
)
27
! комплексные корни
28
else if
(.
not
. is_real)
then
29
! проверяем корни
30
print
*
,
"test 01: "
,
abs
(complex_p2(a, b, c, cx(
1
)))
<=
epsilon
(
1.0d0
)
31
print
*
,
"test 02: "
,
abs
(complex_p2(a, b, c, cx(
2
)))
<=
epsilon
(
1.0d0
)
32
! теорема Виета
33
print
*
,
"test 03: "
,
abs
(cx(
1
)
+
cx(
2
)
+
b
/
a)
<=
epsilon
(
1.0d0
)
34
print
*
,
"test 04: "
,
abs
(cx(
1
)
*
cx(
2
)
- c
/
a)
<=
epsilon
(
1.0d0
)
35
end if
36
end subroutine
test
5

37 38
program
test_quad
39
use
iso_fortran_env,
only
: int32, int64, real32, real64 40
implicit none
41
real
(real64)
::
a, b, c
42 43
print
*
,
"Целые коэффициенты"
44
a
=
1
; b
=
2
; c
=
1 45
call
test(a, b, c)
46 47
print
*
,
"Рациональные коэффициенты"
48
a
=
1.2
; b
=
2.5
; c
=
0.5 49
call
test(a, b, c)
50 51
print
*
,
"Иррациональные коэффициенты"
52
a
=
1
; b
=
sqrt
(
2.0d0
); c
=
0.5 53
call
test(a, b, c)
54 55
print
*
,
"Комплексные корни"
56
a
=
4.0
; b
=
2.0
; c
=
1.0 57
call
test(a, b, c)
58 59
print
*
,
"Большие числа"
60
a
=
45678907.0
; b
=
20987654.0
; c
=
109876.0 61
call
test(a, b, c)
62 63
print
*
,
"Большие отрицательные числа"
64
a
= -
45678907.0
; b
=
20987654.0
; c
= -
109876.0 65
call
test(a, b, c)
66
end program
test_quad
Мы едва ли перебрали все возможные проверки. Например, наша программа даст сбой, при 𝑎 = 0, так как в теле самой программы происходит деление на 𝑎. Для более крупных программ перебрать все возможные случаи и протестировать все ветки бывает зачастую невозможно.
Для запуска программы выполним следующую команду
1
gfortran -Wall ./test/test_quad.f90 -o ./bin/test_quad.exe -J mod
Так как в файле test_quad.f90 в первой строке содержится директива
1
include
"../src/quadequation.f90"
то компилятор автоматически подключит текст модуля quadequation и все переменные и процедуры до- ступные вовне модуля станет возможно вызывать и использовать в основной программе. Опция -J указывает,
что файл mod, который компилятор генерирует автоматически, надо сохранить в каталог mod, а не создавать непосредственно в корневом каталоге.
1.6. Выполнение задания №2
Во втором задании следует используя директивы OpenMP реализовать итеративное суммирование эле- ментов массива. Организуем код точно также в виде файла с кодом модуля и заключенных в нем процедур.
Хотя в данном случае можно обойтись всего одной функцией, но модуль все равно нужен, чтобы компилятор автоматически создал интерфейс для вызова функции.
1
module
summation
2
use
iso_fortran_env,
only
: int32, int64, real32, real64 3
implicit none
6

4
private
5
public ::
iterative_sum
6
contains
7 8
function
iterative_sum(
array
, threads_num)
result
(res)
9
implicit none
10
real
(real64),
dimension
(
1
:),
intent
(in)
:: array
11
integer
(int32),
intent
(in)
::
threads_num
12
real
(real64)
::
res
13
integer
(int32)
::
length, i
14 15
length
=
size(
array
,
1
)
16
res
=
0.0 17
!$omp parallel shared(array) num_threads(threads_num)
18
!$omp do reduction(+ : res)
19
do
i
=
1
,length,
1 20
res
=
res
+
array
(i)
21
end do
22
!$omp end do
23
!$omp end parallel
24
end function
iterative_sum
25 26
end module
summation
• Аргумент array — массив типа double precision, сумму элементов которого необходимо найти.
• Аргумент threads_num — число потоков, которые функция может создать для проведения вычислений.
Имеет тип integer.
В отличие от предыдущего задания, в данном задании есть две тестирующие программы. Первая про- грамма проверяет функцию на корректность, а вторая замеряет время работы, ускорение и эффективность в зависимости от числа потоков. Приведем исходный код этих программ.
1
include
"../src/summation.f90"
2 3
program
test_sum
4
use
iso_fortran_env,
only
: int32, int64, real32, real64 5
use
summation
6
implicit none
7 8
! Количество испытаний
9
integer
(int32),
parameter ::
iterations_num
=
100 10
! Длина массива
11
integer
(int64)
::
array_length
12
! Количество нитей
13
! integer(int32) :: threads_num
14 15
! Динамический массив для проверки функции
16
real
(real64),
dimension
(:),
allocatable ::
test_array
17
real
(real64)
::
correct_sum
18
real
(real64)
::
test_res
19
integer
(int64)
::
i
20
! Вначале проверяем на корректность вычислений
21 22 7

23
! -------- Тест 1 ---------
24
array_length
=
10 25
allocate
(test_array(
1
:array_length))
26 27
! Массив натуральных чисел от 1 до конца массива
28
test_array
=
[(i, i
=
1
,array_length)]
29
correct_sum
=
0.5
*
(test_array(
1
)
+
test_array(array_length))
*
array_length
30
test_res
=
iterative_sum(test_array,
1
)
31 32
print
*
, test_res
==
correct_sum
33 34
deallocate
(test_array)
35
! -----------------
36 37
! -------- Тест 2 ---------
38
array_length
=
1000 39
allocate
(test_array(
1
:array_length))
40 41
! Массив отрицательных целых чисел
42
test_array
=
[(
- i, i
=
1
,array_length)]
43
correct_sum
=
0.5
*
(test_array(
1
)
+
test_array(array_length))
*
array_length
44
test_res
=
iterative_sum(test_array,
1
)
45
print
*
, test_res
==
correct_sum
46 47
deallocate
(test_array)
48
! -----------------
49 50
! -------- Тест 3 ---------
51
array_length
=
1000 52
allocate
(test_array(
1
:array_length))
53 54
! Наполняем случайными числами
55
call random_number
(test_array)
56 57
correct_sum
=
sum
(test_array)
58
test_res
=
iterative_sum(test_array,
1
)
59
print
*
,
abs
(test_res
- correct_sum)
<=
epsilon
(
1.0d0
)
60 61
deallocate
(test_array)
62
! -----------------
63
end program
test_sum
1
include
"../src/summation.f90"
2 3
program
test_sum
4
use
iso_fortran_env,
only
: int32, int64, real32, real64 5
use
summation
6
implicit none
7
include
"omp_lib.h"
8 9
! Количество испытаний
10
integer
(int32),
parameter ::
iterations_num
=
1000 11
! Максимальное количество нитей
12
integer
(int32),
parameter ::
max_threads_num
=
8 8

13
! Длина массива
14
integer
(int64)
::
array_length
15
! Количество нитей
16
integer
(int32)
::
threads_num
17
! Для замера времени
18
real
(real64)
::
t1, t2 19
real
(real64),
dimension
(iterations_num)
::
T
20 21
! Динамический массив для проверки функции
22
real
(real64),
dimension
(:),
allocatable ::
test_array
23
real
(real64)
::
test_res
24
integer
(int64)
::
i
25 26
! Создаем большой массив для тестирования производительности
27
array_length
=
1000
*
1000 28
allocate
(test_array(
1
:array_length))
29 30
call random_number
(test_array)
31 32
do
threads_num
=
1
,max_threads_num,
1 33
do
i
=
1
,iterations_num,
1 34
t1
=
omp_get_wtime()
35
test_res
=
iterative_sum(test_array, threads_num)
36
t2
=
omp_get_wtime()
37
T(i)
=
t2
- t1 38
end do
39
print
(i2,",",G0)
, threads_num,
sum
(T)
/
dble
(iterations_num)
40
end do
41 42
deallocate
(test_array)
43
end program
test_sum
Для компиляции проверяющих программ и сборки исполняемых файлов следует выполнить следующие команды:
1
gfortran -fopenmp -Wall test_sum.f90 -o test_sum -J mod
2
gfortran -fopenmp -Wall test_sum_omp.f90 -o test_sum_omp -J mod
В результате компиляции будет созданы два исполняемых файла: test_sum и test_sum_omp. Для визуализации результатов замеров времени выполняем следующую команду
1
./test_sum_omp | python plot.py
Пример получившихся графиков можно видеть на рисунках 1, 2 и 3. Обратите внимание, что вам необходимо выяснить сколько параллельных потоков поддерживает ваш процессор и указать в исходном коде программы test_sum_omp соответствующее число. Код программы plot.py специально разбирать не будем. При желании за пояснениями можете обратиться к преподавателю.
9

1 2
3 4
5 6
7 8
Количество потоков 𝑝
0.0005 0.0010 0.0015 0.0020 0.0025 0.0030 0.0035
Время выполнения [сек.]
Рис. 1: Время, затраченное на вычисление суммы элементов массива в зависимости от количества порожденных потоков
1 2
3 4
5 6
7 8
Количество потоков 𝑝
1 2
3 4
5 6
7
Ускорение 𝑆
𝑝
= 𝑇
1
/𝑇
𝑝
Рис. 2: Время, затраченное на вычисление суммы элементов массива в зависимости от количества порожденных потоков
10

1 2
3 4
5 6
7 8
Количество потоков 𝑝
0.88 0.90 0.92 0.94 0.96 0.98 1.00
Эффективность 𝑆
𝑝
/𝑝
Рис. 3: Время, затраченное на вычисление суммы элементов массива в зависимости от количества порожденных потоков
11


написать администратору сайта