Общие и частные переменные в openmp программа скрытая ошибка

The golden rule of OpenMP is that all variables (with some exclusions), that are defined in an outer scope, are shared by default in the parallel region. Since in Fortran before 2008 there are no local scopes (i.e. there is no BLOCK ... END BLOCK in earlier versions), all variables (except threadprivate ones) are shared, which is very natural for me (unlike Ian Bush, I am not a big fan of using default(none) and then redeclaring the visibility of all 100+ local variables in various complex scientific codes).

Here is how to determine the sharing class of each variable:

  • N — shared, because it should be the same in all threads and they only read its value.
  • ii — it is the counter of loop, subject to a worksharing directive, so its sharing class is predetermined to be private. It doesn’t hurt to explicitly declare it in a PRIVATE clause, but that is not really necessary.
  • jj — loop counter of a loop, which is not subject to a worksharing directive, hence jj should be private.
  • X — shared, because all threads reference and only read from it.
  • distance_vector — obviously should be private as each thread works on different pairs of particles.
  • distance, distance2, and coff — ditto.
  • M — should be shared for the same reasons as X.
  • PE — acts as an accumulator variable (I guess this is the potential energy of the system) and should be a subject of an reduction operation, i.e. should be put in a REDUCTION(+:....) clause.
  • A — this one is tricky. It could be either shared and updates to A(jj,:) protected with synchronising constructs, or you could use reduction (OpenMP allows reductions over array variables in Fortran unlike in C/C++). A(ii,:) is never modified by more than one thread so it does not need special treatment.

With reduction over A in place, each thread would get its private copy of A and this could be a memory hog, although I doubt you would use this direct O(N2) simulation code to compute systems with very large number of particles. There is also a certain overhead associated with the reduction implementation. In this case you simply need to add A to the list of the REDUCTION(+:...) clause.

With synchronising constructs you have two options. You could either use the ATOMIC construct or the CRITICAL construct. As ATOMIC is only applicable to scalar contexts, you would have to «unvectorise» the assignment loop and apply ATOMIC to each statement separately, e.g.:

!$OMP ATOMIC UPDATE
A(jj,1)=A(jj,1)+(M(ii)/coff)*(distance_vector(1))
!$OMP ATOMIC UPDATE
A(jj,2)=A(jj,2)+(M(ii)/coff)*(distance_vector(2))
!$OMP ATOMIC UPDATE
A(jj,3)=A(jj,3)+(M(ii)/coff)*(distance_vector(3))

You may also rewrite this as a loop — do not forget to declare the loop counter private.

With CRITICAL there is no need to unvectorise the loop:

!$OMP CRITICAL (forceloop)
A(jj,:)=A(jj,:)+(M(ii)/coff)*(distance_vector)
!$OMP END CRITICAL (forceloop)

Naming critical regions is optional and a bit unnecessary in this particular case but in general it allows to separate unrelated critical regions.

Which is faster? Unrolled with ATOMIC or CRITICAL? It depends on many things. Usually CRITICAL is way slower since it often involves function calls to the OpenMP runtime while atomic increments, at least on x86, are implemented with locked addition instructions. As they often say, YMMV.

To recapitulate, a working version of your loop should be something like:

!$OMP PARALLEL DO PRIVATE(jj,kk,distance_vector,distance2,distance,coff) &
!$OMP& REDUCTION(+:PE)
do ii=1,N-1
   do jj=ii+1,N
      distance_vector=X(ii,:)-X(jj,:)
      distance2=sum(distance_vector*distance_vector)
      distance=DSQRT(distance2)
      coff=distance*distance*distance
      PE=PE-M(II)*M(JJ)/distance
      do kk=1,3
         !$OMP ATOMIC UPDATE
         A(jj,kk)=A(jj,kk)+(M(ii)/coff)*(distance_vector(kk))
      end do
      A(ii,:)=A(ii,:)-(M(jj)/coff)*(distance_vector)
   end do
end do
!$OMP END PARALLEL DO

I’ve assumed that your system is 3-dimensional.


With all this said, I second Ian Bush that you need to rethink how position and acceleration matrices are laid out in memory. Proper cache usage could boost your code and would also allow for certain operations, e.g. X(:,ii)-X(:,jj) to be vectorised, i.e. implemented using vector SIMD instructions.

МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ Федеральное государственное бюджетное образовательное учреждение высшего профессионального образования

«Южно-Уральский государственный университет» (национальный исследовательский университет)

ЗАДАНИЯ ДЛЯ ПРАКТИЧЕСКИХ РАБОТ

и

МЕТОДИЧЕСКИЕ УКАЗАНИЯ ПО ИХ ВЫПОЛНЕНИЮ по дисциплине

«Технологии параллельного программирования»

Разработчики:

М.Л. Цымблер, к.ф.-м.н., доцент Е.В. Аксенова, ст. преподаватель К.С. Пан, преподаватель

Челябинск-2012

ОГЛАВЛЕНИЕ

1.

Технология программирования OpenMP ………………………………………………………………….

5

Задание 1. Создание проекта в среде MS Visual Studio с поддержкой OpenMP ……………..

5

Задание 2. Многопоточная программа «Hello World!»………………………………………………….

5

Задание 3. Программа «I am!»……………………………………………………………………………………..

5

Задание 4. Общие и частные переменные в OpenMP: программа «Скрытая ошибка» ……

6

Задание 5. Общие и частные переменные в OpenMP: параметр reduction ……………………..

6

Задание 6. Распараллеливание циклов в OpenMP: программа «Сумма чисел» ………………

7

Задание 7. Распараллеливание циклов в OpenMP: параметр schedule ……………………………

7

Задание 8. Распараллеливание циклов в OpenMP: программа «Число »………………………

8

Задание 9. Распараллеливание циклов в OpenMP: программа «Матрица» …………………….

8

Задание 10. Параллельные секции в OpenMP: программа «I’m here» …………………………….

9

Задание 11. Гонка потоков в OpenMP: программа «Сумма чисел» с atomic ………………..

10

Задание 12. Гонка потоков в OpenMP: программа «Число » с critical ……………………….

10

Задание 13. Исследование масштабируемости OpenMP-программ ……………………………..

10

2.

Технология программирования MPI……………………………………………………………………….

12

Задание 14. Создание проекта в среде MS Visual Studio с поддержкой MPI ………………..

12

Задание 15. Программа «I am!»………………………………………………………………………………….

12

Задание 16. Программа «На первый-второй рассчитайся!»…………………………………………

12

Задание 17. Коммуникации «точка-точка»: простые блокирующие обмены………………..

12

Задание 18. Коммуникации «точка-точка»: схема «эстафетная палочка» …………………….

13

Задание 19. Коммуникации «точка-точка»: схема «мастер-рабочие» ………………………….

13

Задание 20. Коммуникации «точка-точка»: простые неблокирующие обмены…………….

14

Задание 21. Коммуникации «точка-точка»: схема «сдвиг по кольцу»………………………….

14

Задание 22. Коммуникации «точка-точка»: схема «каждый каждому»………………………..

15

Задание 23. Коллективные коммуникации: широковещательная рассылка данных ……..

16

Задание 24. Коллективные коммуникации: операции редукции ………………………………….

17

Задание 25. Коллективные коммуникации: функции распределения и сбора данных ….

17

Задание 26. Группы и коммуникаторы……………………………………………………………………….

18

Задание 27*. MPI-2: динамическое создание процессов ……………………………………………..

18

Задание 28*. MPI-2: односторонние коммуникации……………………………………………………

19

Задание 29. Исследование масштабируемости MPI-программ…………………………………….

19

3.

Технология программирования MPI+OpenMP ………………………………………………………..

20

Задание 30. Проект в среде Visual Studio 2010 с поддержкой MPI и OpenMP………………

20

Задание 31. Программа «I am» …………………………………………………………………………………..

20

Задание 32. Программа «Число »……………………………………………………………………………..

21

4.

Методические указания …………………………………………………………………………………………

22

1. Технология программирования OpenMP ………………………………………………………………..

22

2

Указания к заданию 1. Создание проекта в среде MS Visual Studio с поддержкой

OpenMP…………………………………………………………………………………………………………………

22

Указания к заданию 2. Многопоточная программа «Hello World!» ………………………….

25

Указания к заданию 3. Программа «I am!» ……………………………………………………………..

26

Указания к заданию 4. Общие и частные переменные в OpenMP: программа «Скрытая

ошибка» ………………………………………………………………………………………………………………..

27

Указания к заданию 5. Общие и частные переменные в OpenMP: параметр reduction 27

Указания к заданию 6. Распараллеливание циклов в OpenMP: программа «Сумма

чисел» …………………………………………………………………………………………………………………..

28

Указания к заданию 7. Распараллеливание циклов в OpenMP: параметр schedule ……

29

Указания к заданию 8. Распараллеливание циклов в OpenMP: программа «Число » 30

Указания к заданию 9. Распараллеливание циклов в OpenMP: программа «Матрица»

……………………………………………………………………………………………………………………………..

30

Указания к заданию 10. Параллельные секции в OpenMP: программа «I’m here» …….

31

Указания к заданию 11. Гонка потоков в OpenMP: программа «Сумма чисел» с

atomic ……………………………………………………………………………………………………………………

32

Указания к заданию 12. Гонка потоков в OpenMP: программа «Число » с critical …

32

Указания к заданию 13. Исследование масштабируемости OpenMP-программ………..

33

2. Технология программирования MPI……………………………………………………………………….

37

Указания к заданию 14. Создание проекта в среде MS Visual Studio с поддержкой MPI

……………………………………………………………………………………………………………………………..

37

Указания к заданию 15. Программа «I am!» ……………………………………………………………

41

Указания к заданию 16. Программа «На первый-второй рассчитайся!» …………………..

43

Указания к заданию 17. Коммуникации «точка-точка»: простые блокирующие

обмены ………………………………………………………………………………………………………………….

43

Указания к заданию 18. Коммуникации «точка-точка»: схема «эстафетная палочка».45

Указания к заданию 19. Коммуникации «точка-точка»: схема «мастер-рабочие»…….

46

Указания к заданию 20. Коммуникации «точка-точка»: простые неблокирующие

обмены ………………………………………………………………………………………………………………….

46

Указания к заданию 21. Коммуникации «точка-точка»: схема «сдвиг по кольцу»……

47

Указания к заданию 22. Коммуникации «точка-точка»: схема «каждый каждому» ….

48

Указания к заданию 23. Коллективные коммуникации: широковещательная рассылка

данных ………………………………………………………………………………………………………………….

49

Указания к заданию 24. Коллективные коммуникации: операции редукции ……………

50

Указания к заданию 25. Коллективные коммуникации: функции распределения и

сбора данных…………………………………………………………………………………………………………

51

Указания к заданию 26. Группы и коммуникаторы …………………………………………………

52

Указания к заданию 27. MPI-2: динамическое создание процессов …………………………

55

Указания к заданию 28. MPI-2: односторонние коммуникации ……………………………….

55

Указания к заданию 29. Исследование масштабируемости MPI-программ ………………

57

3. Технология программирования MPI+OpenMP………………………………………………………..

57

3

Указания к заданию 30.

Проект в среде Visual Studio 2010 с поддержкой MPI и

OpenMP…………………………………………………………………………………………………………………

57

Указания к заданию 31.

Программа «I am» …………………………………………………………….

57

Указания к заданию 31.

Программа «I am» …………………………………………………………….

58

4

1. Технология программирования OpenMP

Задание 1. Создание проекта в среде MS Visual Studio с поддержкой

OpenMP

Создайте проект в среде MS Visual Studio 2010 с поддержкой OpenMP.

Задание 2. Многопоточная программа «Hello World!»

Напишите OpenMP-программу, в которой создается 4 нити и каждая нить выводит на экран строку «Hello World!».

Входные данные: нет.

Выходные данные: 4-е строки «Hello World!».

Пример входных и выходных данных

Входные данные

Выходные данные

Hello World!

Hello World!

Hello World!

Hello World!

Задание 3. Программа «I am!»

1. Напишите программу, в которой создается k нитей, и каждая нить выводит на экран свой номер и общее количество нитей в параллельной области в формате:

I am <Номер нити> thread from <Количество нитей> threads!

Входные данные: k – количество нитей в параллельной области.

Выходные данные: k строк вида «I am <Номер нити> thread from <Ко-

личество нитей> threads!».

Пример входных и выходных данных

Входные данные

Выходные данные

3

I am 0

thread from 3

threads!

I am 1

thread from

3

threads!

I am 2

thread from

3

threads!

2. Модифицируйте программу таким образом, чтобы строку I am <Но-

мер нити> thread from <Количество нитей> threads! выводили только

нити с четным номером.

Пример входных и выходных данных

Входные данные

Выходные данные

3

I am 0

thread from

3

threads!

I am 2

thread from

3

threads!

5

Задание 4. Общие и частные переменные в OpenMP: программа «Скрытая ошибка»

Изучите конструкции для управления работой с данными shared и private. Напишите программу, в которой создается k нитей, и каждая нить выводит на экран свой номер через переменную rank следующим образом:

rank = omp_get_thread_num(); printf(«I am %d thread.n», rank);

Экспериментами определите, общей или частной должна быть перемен-

ная rank.

Входные данные: целое число k – количество нитей в параллельной области.

Выходные данные: k строк вида «I am <Номер нити>.».

Пример входных и выходных данных

Входные данные

Выходные данные

3

I am 0

thread.

I am 1

thread.

I am 2

thread.

Задание 5. Общие и частные переменные в OpenMP: параметр reduction

1. Напишите программу, в которой две нити параллельно вычисляют сумму чисел от 1 до N. Распределите работу по нитям с помощью оператора if языка С. Для сложения результатов вычисления нитей воспользуйтесь OpenMP-параметром reduction.

Входные данные: целое число N – количество чисел.

Выходные данные: каждая нить выводит свою частичную сумму в формате

«[Номер_нити]: Sum = <частичная_сумма>», один раз выводится общая

сумма в формате «Sum = <сумма>».

Пример входных и выходных данных

Входные данные

Выходные данные

4

[0]: Sum = 3

[1]: Sum = 7

Sum = 10

2*. Модифицируйте программу таким образом, чтобы она работала для k нитей.

Входные данные: целое число k – количество нитей, целое число N – количество чисел.

Выходные данные: каждая нить выводит свою частичную сумму в формате

«[Номер_нити]: Sum = <частичная_сумма>», один раз выводится общая

сумма в формате «Sum = <сумма>».

6

Пример входных и выходных данных

Входные данные

Выходные данные

2

[0]: Sum = 3

4

[1]: Sum = 7

Sum = 10

2

[0]: Sum = 1

2

[1]: Sum = 2

Sum = 3

3

[0]: Sum = 1

2

[1]: Sum = 2

[2]: Sum = 0

Sum = 3

Задание 6. Распараллеливание циклов в OpenMP: программа «Сумма чисел»

Изучите OpenMP-директиву параллельного выполнения цикла for. Напишите программу, в которой k нитей параллельно вычисляют сумму чисел от 1 до N. Распределите работу по нитям с помощью OpenMP-директивы for.

Входные данные: целое число k – количество нитей, целое число N – количество чисел.

Выходные данные: каждая нить выводит свою частичную сумму в формате

«[Номер_нити]: Sum = <частичная_сумма>», один раз выводится общая

сумма в формате «Sum = <сумма>».

Пример входных и выходных данных

Входные данные

Выходные данные

2

[0]: Sum = 3

4

[1]: Sum = 7

Sum = 10

2

[0]: Sum = 1

2

[1]: Sum = 2

Sum = 3

3

[0]: Sum = 1

2

[1]: Sum = 2

[2]: Sum = 0

Sum = 3

Задание 7. Распараллеливание циклов в OpenMP: параметр schedule

Изучите параметр schedule директивы for. Модифицируйте программу «Сумма чисел» из задания 6 таким образом, чтобы дополнительно выводилось на экран сообщение о том, какая нить, какую итерацию цикла выполняет:

[<Номер нити>]: calculation of the iteration number <Номер итерации>.

Задайте k = 4, N = 10. Заполните следующую таблицу распределения итераций цикла по нитям в зависимости от параметра schedule:

7

Номер

Значение параметра schedule

итерации

static

static, 1

static, 2

dynamic

dynamic, 2

guided

guided, 2

1

2

3

4

5

6

7

8

9

10

Задание 8. Распараллеливание циклов в OpenMP: программа «Число »

1. Напишите OpenMP-программу, которая вычисляет число с точностью до N знаков после запятой. Используйте следующую формулу:

Распределите работу по нитям с помощью OpenMP-директивы for. Входные данные: одно целое число N (точность вычисления). Выходные данные: одно вещественное число pi.

Пример входных и выходных данных

Входные данные

Выходные данные

1000000000

3.14159265

Задание 9. Распараллеливание циклов в OpenMP: программа «Матрица»

Напишите OpenMP-программу, которая вычисляет произведение двух квадратных матриц × = С размера × . Используйте следующую формулу:

8

Входные данные: целое число n, 1 ≤ ≤ 10, n2 вещественных элементов матрицы A и n2 вещественных элементов матрицы B.

Выходные данные: n2 вещественных элементов матрицы С.

Пример входных и выходных данных

Входные данные

Выходные данные

2

14 4

1

3

44 16

4

8

5

4

3

0

Задание 10. Параллельные секции в OpenMP: программа «I’m here»

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

[<Номер нити>]: came in section <Номер секции>

Вне секций внутри параллельной области должно выводиться следующее сообщение:

[<Номер нити>]: parallel region

Запустите приложение на 2-х, 3-х, 4-х нитях. Проследите, как нити распределяются по параллельным секциям.

Входные данные: k – количество нитей в параллельной области.

Выходные данные: k-строк вида «[<Номер нити>]: came in section <Номер секции>», k-строк вида «[<Номер нити>]: parallel region».

Пример входных и выходных данных

Входные данные

Выходные данные

3

[0]: came in section 1

[1]: came in section 2

[2]: came in section 3

[0]: parallel region

[1]: parallel region

[2]: parallel region

9

Задание 11. Гонка потоков1 в OpenMP: программа «Сумма чисел» с atomic

Перепишите программу, в которой параллельно вычисляется сумма чисел от 1 до N (см. задание 6), без использования параметра reduction. Вместо параметра reduction используйте директиву atomic.

Задание 12. Гонка потоков в OpenMP: программа «Число » с critical

Перепишите параллельную программу вычисления числа (см. задание 8) без использования параметра reduction. Вместо параметра reduction используйте директиву critical.

Задание 13. Исследование масштабируемости OpenMP-программ

1. Проведите серию экспериментов на персональном компьютере по исследованию масштабируемости OpenMP-программ. Заполните следующую таблицу:

Количество

Время

Программа

Параметр N

выполнения

п/п

нитей

(сек.)

1

«Матрица» (см. задание 9)

100

1

2

1 000 000

1

3

100

2

4

1 000 000

2

5

100

4

6

1 000 000

4

7

100

6

8

1 000 000

6

9

100

8

10

1 000 000

8

11

100

10

12

1 000 000

10

13

100

12

14

1 000 000

12

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

2. Проведите серию экспериментов на суперкомпьютере по исследованию масштабируемости OpenMP-программ. Заполните следующую таблицу:

1 Гонка потоков (race conditions) – ситуация когда результат вычислений зависит от темпа выполнения программы нитями. Для исключения гонки необходимо обеспечить, чтобы изменение значений общих переменных осуществлялось в каждый момент времени только одним единственным потоком. В OpenMP это может быть организовано следующими основными механизмами:

неделимые (atomic) операции,

механизм критических секций (critical sections).

10

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

  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #

Время на прочтение
5 мин

Количество просмотров 82K

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

При использовании OpenMP мы добавляем в программу два вида конструкций: функции исполняющей среды OpenMP и специальные директивы #pragma.

Функции

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

Директивы

Конструкция #pragma в языке Си/Си++ используется для задания дополнительных указаний компилятору. С помощью этих конструкций можно указать как осуществлять выравнивание данных в структурах, запретить выдавать определенные предупреждения и так далее. Форма записи:

#pragma директивы

Использование специальной ключевой директивы «omp» указывает на то, что команды относятся к OpenMP. Таким образом директивы #pragma для работы с OpenMP имеют следующий формат:

#pragma omp <директива> [раздел [ [,] раздел]...] 

Как и любые другие директивы pragma, они игнорируются теми компиляторами, которые не поддерживают данную технологию. При этом программа компилируется без ошибок как последовательная. Это особенность позволяет создавать хорошо переносимый код на базе технологии OpenMP. Код содержащий директивы OpenMP может быть скомпилирован Си/Си++ компилятором, который ничего не знает об этой технологии. Код будет выполнятся как последовательный, но это лучше, чем делать две ветки кода или расставлять множество #ifdef.
OpenMP поддерживает директивы private, parallel, for, section, sections, single, master, critical, flush, ordered и atomic и ряд других, которые определяют механизмы разделения работы или конструкции синхронизации.

Директива parallel

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

#pragma omp parallel [другие директивы]
  структурированный блок

Директива parallel указывает, что структурный блок кода должен быть выполнен параллельно в несколько потоков. Каждый из созданных потоков выполнит одинаковый код содержащийся в блоке, но не одинаковый набор команд. В разных потоках могут выполняться различные ветви или обрабатываться различные данные, что зависит от таких операторов как if-else или использования директив распределения работы.

Чтобы продемонстрировать запуск нескольких потоков, распечатаем в распараллеливаемом блоке текст:

#pragma omp parallel
{
  cout << "OpenMP Test" << endl;
}

На 4-х ядерной машине мы можем ожидать увидеть следующей вывод

OpenMP Test
OpenMP Test
OpenMP Test
OpenMP Test

Но на практике я получил следующий вывод:

OpenMP TestOpenMP Test
OpenMP Test

OpenMP Test

Это объясняется совместным использованием одного ресурса из нескольких потоков. В данном случае мы выводим на одну консоль текст в четырех потоках, которые никак не договариваются между собой о последовательности вывода. Здесь мы наблюдаем возникновение состояния гонки (race condition).

Состояние гонки — ошибка проектирования или реализации многозадачной системы, при которой работа системы зависит от того, в каком порядке выполняются части кода. Эта разновидность ошибки являются наиболее распространенной при параллельном программировании и весьма коварна. Воспроизведение и локализация этой ошибки часто бывает затруднена в силу непостоянства своего проявления (смотри также термин Гейзенбаг).

Директива for

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

void VSqrt(double *src, double *dst, ptrdiff_t n)
{
  for (ptrdiff_t i = 0; i < n; i++)
    dst[i] = sqrt(src[i]);
}

Если мы напишем:

#pragma omp parallel
{
  for (ptrdiff_t i = 0; i < n; i++)
    dst[i] = sqrt(src[i]);
}

то мы вместо ускорения впустую проделаем массу лишней работы. Мы извлечем корень из всех элементов массива в каждом потоке. Для того, чтобы распараллелить цикл нам необходимо использовать директиву разделения работы «for». Директива #pragma omp for сообщает, что при выполнении цикла for в параллельном регионе итерации цикла должны быть распределены между потоками группы:

#pragma omp parallel
{
  #pragma omp for
  for (ptrdiff_t i = 0; i < n; i++)
    dst[i] = sqrt(src[i]);
}

Теперь каждый создаваемый поток будет обрабатывать только отданную ему часть массива. Например, если у нас 8000 элементов, то на машине с четырьмя ядрами работа может быть распределена следующим образом. В первом потоке переменная i принимает значения от 0 до 1999. Во втором от 2000 до 3999. В третьем от 4000 до 5999. В четвертом от 6000 до 7999. Теоретически мы получаем ускорение в 4 раза. На практике ускорение будет чуть меньше из-за необходимости создать потоки и дождаться их завершения. В конце параллельного региона выполняется барьерная синхронизация. Иначе говоря, достигнув конца региона, все потоки блокируются до тех пор, пока последний поток не завершит свою работу.

Можно использовать сокращенную запись, комбинируя несколько директив в одну управляющую строку. Приведенный выше код будет эквивалентен:

#pragma omp parallel for
for (ptrdiff_t i = 0; i < n; i++)
  dst[i] = sqrt(src[i]); 

Директивы private и shared

Относительно параллельных регионов данные могут быть общими (shared) или частными (private). Частные данные принадлежат потоку и могут быть модифицированы только им. Общие данные доступны всем потокам. В рассматриваемом ранее примере массив представлял общие данные. Если переменная объявлена вне параллельного региона, то по умолчанию она считается общей, а если внутри то частной. Предположим, что для вычисления квадратного корня нам необходимо использовать промежуточную переменную value:

double value;
#pragma omp parallel for
for (ptrdiff_t i = 0; i < n; i++)
{
  value = sqrt(src[i]);
  dst[i] = value;
}

В приведенном коде переменная value объявлена вне параллельного региона, задаваемого директивами «#pragma omp parallel for», а значит является общей (shared). В результате переменная value начнет использоваться всеми потоками одновременно, что приведет к ошибке состояния гонки и на выходе мы получим мусор.

Чтобы сделать переменную для каждого потока частной (private) мы можем использовать два способа. Первый — объявить переменную внутри параллельного региона:

#pragma omp parallel for
for (ptrdiff_t i = 0; i < n; i++)
{
  double value;
  value = sqrt(src[i]);
  dst[i] = value;
}

Второй — воспользоваться директивой private. Теперь каждый поток будет работать со своей переменной value:

double value;
#pragma omp parallel for private(value)
for (ptrdiff_t i = 0; i < n; i++)
{
  value = sqrt(src[i]);
  dst[i] = value;
}

Помимо директивы private, существует директива shared. Но эту директиву обычно не используют, так как и без нее все переменные объявленные вне параллельного региона будут общими. Директиву можно использовать для повышения наглядности кода.

Мы рассмотрели только малую часть директив OpenMP и продолжим знакомство с ними в следующих уроках.

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

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

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

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

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

Рассмотрим несколько таких средств, предположив, что будем отлаживать OpenMP-программу, созданную в среде Microsoft Visual Studio 2005. Более подробно о стандарте OpenMP см. «Мир ПК», №10/07, с. 60.

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

 

Эта простая программа вычисляет значения некоторой функции. Ее можно легко распараллелить с помощью средств стандарта OpenMP. Добавим одну строку перед первым оператором for (листинг 2).

Данная программа успешно компилируется в среде MS Visual Studio 2005, причем компилятор даже не выдает никаких предупреждений. Однако она совершенно некорректна. Чтобы это понять, надо вспомнить, что в OpenMP-программах переменные делятся на общие (shared), существующие в одном экземпляре и доступные всем потокам, и частные (private), локализованные в конкретном процессе. Кроме того, есть правило, гласящее, что по умолчанию все переменные в параллельных регионах OpenMP общие, за исключением индексов параллельных циклов и переменных, объявленных внутри этих параллельных регионов.

В приведенном выше примере видно, что переменные x, y и s — общие, что совершенно неправильно. Переменная s обязательно должна быть общей, так как в рассматриваемом алгоритме она является, по сути, сумматором. Однако при работе с переменными x или y каждый процесс вычисляет очередное их значение и записывает в соответствующую из них. И тогда результат вычислений зависит от того, в какой последовательности выполнялись параллельные потоки. Иначе говоря, если первый поток вычислит значение для x, запишет его в переменную x, а потом такие же действия произведет второй поток, то при попытке прочитать значение переменной x первым потоком он получит то значение, которые было записано туда последним по времени, а значит, вычисленное вторым потоком. Подобные ошибки в случае, когда работа программы зависит от порядка выполнения различных фрагментов кода, называются race condition или data race (состояние «гонки» или «гонки» вычислительных потоков; подразумевается, что имеют место несинхронизированные обращения к памяти).

Для поиска таких ошибок необходимы специальные программные средства. Одно из них — Intel Thread Checker. Данная программа поставляется как модуль к профилировщику Intel VTune Performance Analyzer, дополняя имеющиеся средства для работы с многопоточным кодом. Intel Thread Checker позволяет обнаружить как описанные выше ошибки, так и многие другие, например deadlocks («тупики», места взаимной блокировки вычислительных нитей) и утечки памяти.

После установки Intel Thread Checker в диалоге New Project приложения Intel VTune Performance Analyzer появится новая категория проектов — Threading Wizards (мастера для работы с потоками), среди которых будет Intel Thread Checker Wizard. Необходимо выбрать его, а в следующем окне мастера указать путь к запускаемой программе. После запуска программа начнет выполняться, а профилировщик соберет все сведения о работе приложения. Пример такой информации, выдаваемой Intel Thread Checker, приведен на рис. 1.

Как видно, даже для такой небольшой программы количество ошибок достаточно велико. Thread Checker группирует обнаруженные ошибки, одновременно оценивая их критичность для работы программы, и приводит их описание, что существенно повышает эффективность работы программиста. Кроме того, на вкладке Source View представлен программный код приложения с указанием тех мест в коде, где имеются ошибки (рис. 2).

Итак, описанную выше и обнаруженную средствами Intel Thread Checker ошибку записи в переменные x и y исправить довольно просто: нужно лишь добавить в конструкцию #pragma omp parallel for еще одну директиву: private (x, y). Таким образом, эти две переменные будут объявлены как частные, и в каждом вычислительном потоке будут свои копии x и y. Следует также обратить внимание, что все потоки сохраняют вычисленный результат добавлением его к переменной s. И здесь кроется ошибка, для которой Thread Checker приводит такое описание — Write‡Read data race (состояние «гонки» вычислительных потоков). Подобные ошибки происходят тогда, когда один вычислительный поток пытается записать некоторое значение в общую память, а другой в то же время выполняет операцию чтения. В рассматриваемом примере это может привести к некорректному результату.

Рассмотрим инструкцию s = s + j*y. Изначально предполагается, что каждый поток суммирует вычисленный результат с текущим значением переменной s, а потом такие же действия выполняют остальные потоки. Однако возможна ситуация, когда, например, два потока одновременно начали выполнять инструкцию s = s + j*y, т. е. каждый из них сначала прочитает текущее значение переменной s, затем прибавит к этому значению результат умножения j*y и полученное запишет в общую переменную s.

В отличие от операции чтения, которая может быть реализована параллельно и является достаточно быстрой, операция записи всегда последовательна. Следовательно, если сначала первый поток записал новое значение, то второй поток, выполнив после этого запись, затрет результат вычислений первого, потому что оба вычислительных потока сначала прочитали одно и то же значение s, а потом стали записывать свои данные в эту переменную. Иными словами, то значение s, которое второй поток в итоге запишет в общую память, никак не учитывает результат вычислений, полученный в первом потоке. Можно избежать подобной ситуации, если гарантировать, что в любой момент времени операцию s = s + j*y разрешается выполнять только одному из потоков. Такие операции называются неделимыми или атомарными. Когда нужно указать компилятору, что какая-либо инструкция является атомарной, используется конструкция #pragma omp atomic. Программный код, в котором исправлены указанные ошибки, приведен в листинге 3.

 

После перекомпиляции программы и ее повторного анализа в Thread Checker получим результат, приведенный на рис. 3.

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

Отладчик Intel Thread Checker поддерживает анализ 32- и 64-разрядных приложений в операционных системах MS Windows и Linux. Среди других средств отладки для параллельных приложений можно выделить широко распространенный отладчик TotalView, разрабатываемый компанией TotalView Technologies, а также отладчик dbx и утилиту Thread Analyzer из пакета Sun Studio компании Sun Microsystems. Хотя TotalView и является коммерческим продуктом, он доступен для операционных систем Linux, UNIX, Mac OS, поддерживает языки Си, С++, Фортран и технологии параллельного программирования OpenMP и MPI (Message Passing Interface).

Компилятор Sun Studio имеет поддержку OpenMP, а с помощью отладчика dbx можно выполнять код в области параллельности в пошаговом режиме, устанавливать в параллельном коде точки останова, контролировать значения переменных, которые определены как частные, и т. д. Указанная выше утилита для анализа многопоточного кода Thread Analyzer во многом схожа с Intel Thread Checker: ее работа также строится по принципу сбора данных для дальнейшего анализа. Она может применяться для поиска конфликтов доступа к данным (data races) и тупиков (deadlocks). Для использования Thread Analyzer программа должна быть собрана со специальным ключом компилятора Sun Studio.

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

A variable in an OpenMP parallel region can be either shared or private. If a
variable is shared, then there exists one instance of this variable which is
shared among all threads. If a variable is private, then each thread in a team of
threads has its own local copy of the private variable.

In this article, we look how OpenMP specifies if a variable is shared or private.

Implicit Rules

OpenMP has a set of rules, which deduce the data-sharing attributes of variables.

For example, let us consider the following snippet of code.

int i = 0;
int n = 10;
int a = 7;

#pragma omp parallel for 
for (i = 0; i < n; i++)
{
    int b = a + i;
    ...
}

There are four variables i, n,
a and b.

The data-sharing attribute of variables, which are declared outside the
parallel region, is usually shared. Therefore, n and
a are shared variables.

The loop iteration variables, however, are private by default. Therefore,
i is private.

The variables which are declared locally within the parallel region are
private. Thus b is private.

I recommend to declare the loop iteration variables inside the parallel
region. In this case, it is clearer that this variables are private. The upper
snippet of code then looks like:

int n = 10;                 // shared
int a = 7;                  // shared

#pragma omp parallel for 
for (int i = 0; i < n; i++) // i private
{
    int b = a + i;          // b private
    ...
}

Explicit rules

We can explicitly set the data-sharing attribute of a variable.

The shared(list) clause declares that all the variables in
list are shared. In the next example

#pragma omp parallel for shared(n, a)
for (int i = 0; i < n; i++)
{
    int b = a + i;
    ...        
}

n and a are shared variables.

OpenMP does not put any restriction to prevent data races between shared
variables. This is a responsibility of a programmer.

Shared variables introduce an overhead, because one instance of a variable is
shared between multiple threads. Therefore, it is often best to minimize the
number of shared variables when a good performance is desired.

Private

The private(list) clause declares that all the variables in
list are private. In the next example

#pragma omp parallel for shared(n, a) private(b)
for (int i = 0; i < n; i++)
{
    b = a + i;
    ...
}

b is a private variable. When a variable is declared private,
OpenMP replicates this variable and assigns its local copy to each thread.

The behavior of private variables is sometimes unintuitive. Let us assume that a
private variable has a value before a parallel region. However, the value of the
variable at the beginning of the parallel region is undefined. Additionally, the
value of the variable is undefined also after the parallel region.

For example:

int p = 0; 
// the value of p is 0

#pragma omp parallel private(p)
{
    // the value of p is undefined
    p = omp_get_thread_num();
    // the value of p is defined
    ...
}
// the value of p is undefined

On several occasions, we can avoid listing private variables in the OpenMP
constructs by declaring them inside a parallel region. For example, instead of

int p;
#pragma omp parallel private(p)
{
    p = omp_get_thread_num();
}

we can do

#pragma omp parallel
{
    int p = omp_get_thread_num();
    ...
}

I highly encourage declaring private variables inside a parallel region
whenever possible. This guideline simplifies the code and increases its
readability.

Default

There are two versions of the default clause. First, we focus
on default(shared) option and then we consider
default(none) clause.

These two versions are specific for C++ programmers of OpenMP. There are some
additional default possibilities for Fortran programmers. For
more details look at the OpenMP specification on the page
189.

Default (shared)

The default(shared) clause sets the data-sharing attributes
of all variables in the construct to shared. In the following example

int a, b, c, n;
...

#pragma omp parallel for default(shared)
for (int i = 0; i < n; i++)
{
    // using a, b, c
}

a, b, c and
n are shared variables.

Another usage of default(shared) clause is to specify the
data-sharing attributes of the majority of the variables and then additionally
define the private variables. Such usage is presented below:

int a, b, c, n;

#pragma omp parallel for default(shared) private(a, b)
for (int i = 0; i < n; i++)
{
    // a and b are private variables
    // c and n are shared variables 
}

Default (none)

The default(none) clause forces a programmer to explicitly
specify the data-sharing attributes of all variables.

A distracted programmer might write the following piece of code

int n = 10;
std::vector<int> vector(n);
int a = 10;

#pragma omp parallel for default(none) shared(n, vector)
for (int i = 0; i < n; i++)
{
    vector[i] = i * a;
}

But then the compiler would complain

error: a not specified in enclosing parallel
         vector[i] = i * a;
                       ^
error: enclosing parallel
     #pragma omp parallel for default(none) shared(n, vector)
             ^

The reason for the unhappy compiler is that the programmer used
default(none) clause and then she/he forgot to explicitly
specify the data-sharing attribute of a. The correct version of the
program would be

int n = 10;
std::vector<int> vector(n);
int a = 10;

#pragma omp parallel for default(none) shared(n, vector, a)
for (int i = 0; i < n; i++)
{
    vector[i] = i * a;
}

Good practices

I warmly encourage that a programmer follows the next two guidelines.

The first guideline is to always write parallel regions with the
default(none) clause
. This forces the programmer to
explicitly think about the data-sharing attributes of all variables.

The second guideline is to declare private variables inside parallel regions
whenever possible
. This guideline improves the readability of the code and
makes it clearer.

Summary

We studied the implicit and explicit rules for deducing the data-sharing
attributes of variables. We also recommended a couple of good practices that a
programmer should follow.

Links:

  • Data-Sharing Attribute Clauses [OpenMP API, page
    188]

Jaka’s Corner OpenMP series:

  • OpenMP: Introduction
  • OpenMP: For
  • OpenMP: Sections
  • OpenMP: Monte Carlo method for Pi
  • OpenMP: For & Reduction
  • OpenMP: For & Scheduling
  • OpenMP: Single

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