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

Паралпрог / Паралпрог_Книжка

.pdf
Скачиваний:
264
Добавлен:
03.06.2015
Размер:
590.89 Кб
Скачать

Введение в распараллеливание алгоритмов и программ

251

пересечение W(S1) и W(S2) пусто,

пересечение W(S1) с R(S2) пусто,

пересечение R(S1) и W(S2) пусто,

то операторы S1 и S2 могут быть выполнены одновременно разными исполнителями на параллельной вычислительной системе.

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

Давайте разберемся, что будет происходить в том случае, когда для двух операторов S1 и S2, динамически следующих друг за другом, условия Бернстайна нарушаются (непосредственное следование здесь не играет роли). Здесь мы будем следовать изложению материала в [Jordan, Alaghband, 2003].

Начнем с нарушения первого условия. Итак, пусть для двух операторов (или блоков операторов) S1 и S2 последовательной программы, динамически следующих друг за другом, выполнено:

пересечение W(S1 ) и W(S2 ) не пусто,

пересечение W(S1 ) с R(S2) пусто,

пересечение R(S1 ) и W(S2) пусто. Рассмотрим пример:

S1 : x = 2 * y + z; S2 : х = a − b;

В большинстве случаев в последовательной программе (а я напомню, что мы работаем

впонятиях выполняемой программы, а не исходных текстов) такой случай связан либо с экономией памяти, когда под разные данные отводится одна и та же область памяти, либо с небрежностью (не сказать плохого слова) программистов. Простое переименование переменной «x»

воператоре S2 снимает возникающие вопросы. Тем не менее, в первоначальном виде операторы зависят друг от друга, и такой вид зависимости мы будем называть зависимостью по выходным данным (output dependence). Обычно такая зависимость записывается как S1 δo S2 и графически изображается следующим образом (рис. 6).

Рис. 6. Зависимость по выходным данным

Как правило, зависимость по выходным данным не мешает распараллеливанию. Переименуйте переменные и, если оба оператора исполняются непосредственно один за другим, распараллеливайте на здоровье!

Пусть нарушено третье условие Бернстайна. Для двух операторов (или блоков операторов) S1 и S2 последовательной программы, динамически следующих друг за другом, выполнено следующее:

пересечение W(S1 ) и W(S2 ) пусто,

пересечение W(S1 ) с R(S2) пусто,

пересечение R(S1 ) и W(S2) не пусто. Рассмотрим пример:

S1 : x = 2 * y + z; S2 : y = a − b;

2010, Т. 2, № 3, С. 231–272

252

В. Е. Карпов

Переменная «y» в последовательной программе сначала используется для вычислений в операторе S1, а затем переопределяется в операторе S2 . Если исполнитель, взявший на себя выполнение оператора S1, использует значение «y» до того, как это значение переопределит исполнитель, выполняющий S2, то распараллеливание безопасно. Давайте поступим следующим образом. Перед выполнением соответствующих операторов скопируем значение данной переменой в локальную память исполнителей и лишь потом разрешим им выполнить операции. Тогда расчеты окажутся верными. Эту форму зависимости операторов называют антизависимостью (antidependence). Обычная форма ее записи выглядит как S1 δ−1 S2, а графическое представление имеет вид (рис. 7):

Рис. 7. Антизависимость

Наконец, осталось рассмотреть нарушение второго условия Бернстайна, т. е. ситуацию, когда для двух операторов (или блоков операторов) S1 и S2 последовательной программы, динамически следующих друг за другом, выполнено следующее:

пересечение W(S1 ) и W(S2 ) пусто,

пересечение W(S1 ) с R(S2) не пусто,

пересечение R(S1 ) и W(S2) пусто. Рассмотрим пример:

S1 : x = 2 * y + z; S2 : y = a − x;

Это самый тяжелый случай с точки зрения распараллеливания. Результат выполнения оператора S1 используется для выполнения оператора S2. Если операторы непосредственно динамически следуют друг за другом, то их распараллеливание невозможно. Такой тип зависимости называют потоковой зависимостью, или истинной зависимостью. Ее принято записывать S1 δ S2

и изображать так (рис. 8).

Рис. 8. Потоковая, или истинная, зависимость

Используя введенные обозначения, зависимости в программном фрагменте из четырех операторов, приведенном выше, можно графически изобразить следующим образом (рис. 9).

Рис. 9. Графическое представление зависимостей в программном фрагменте

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

S1 : x = cos(z); if (x > 0) { S2 : a = b + c;

КОМПЬЮТЕРНЫЕ ИССЛЕДОВАНИЯ И МОДЕЛИРОВАНИЕ

 

Введение в распараллеливание алгоритмов и программ

253

 

} else {

 

S3 :

a = b − c;

 

 

}

 

выполнение операторов S2 и S3 зависит от результата выполнения оператора S1, но не по данным, а по управлению. Этот вид зависимости записывают как S1 δc S2 и S1 δc S3.

Зависимость по управлению можно свести к зависимости по данным, если ввести новые специфические операторы, изменив вид программы:

S1 :

x = cos(z);

 

S2 :

where (x > 0)

a = b + c;

S3 :

where (x ≤ 0)

a = b − c;

Здесь переменная «x»

становится входной переменной для операторов S2 и S3,

и зависимости по управлению заменяются зависимостями по данным.

Еще один вид зависимости в последовательном фрагменте программного кода может возникнуть при его распараллеливании на вычислительной системе с ограниченными ресурсами. Пусть у нас есть 2 оператора, динамически непосредственно следующих друг за другом и не имеющих зависимостей по данным.

S1 : x = 2 * y / z;

S2 : c = a / b;

Условия Бернстайна выполнены, но если у вас на параллельной вычислительной системе существует единственный исполнитель, умеющий делить одно данное на другое, то параллельное исполнение операторов оказывается невозможным. Это — зависимость по ресурсам, S1 δR S2. Мы далее будем работать в модели неограниченного параллелизма, где зависимости по ресурсам не существует.

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

Зависимости в простых циклах и их анализ на параллельность

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

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

2010, Т. 2, № 3, С. 231–272

254

В. Е. Карпов

do i = b, e, s

. . .

enddo

Здесь i — итерационная переменная, b — ее начальное значение, e — конечное значение, s — шаг изменения итерационной переменной. Заметим, что счетчик может не принимать своего точного конечного значения при выполнении цикла. Множество всех принимаемых значений итерационной переменной назовем итерационным пространством одномерного цикла. Тело цикла лежит между операторами «do» и «enddo».

Путем простой замены итерационной переменной можно привести такой цикл к нормализованному виду

do j = 1, jfin, 1

. . .

enddo

Для краткости записи у нормализованного цикла мы будем опускать шаг счетчика

do j = 1, jfin

. . .

enddo

Пусть тело цикла состоит из двух операторов S1 и S2 , в наборы входных и/или выходных данных которых входит обращение к элементам одного и того же одномерного массива данных A.

do j = 1, jfin

S1 : . . . A[f(j)]. . .

S2 : . . . A[g(j)]. . .

enddo

Здесь f (j) и g(j) — некоторые целочисленные функции целого переменного. Для простоты будем считать, что индекс массива A может принимать любое целое значение.

Нашей основной задачей является выяснение того, можно ли разбить итерационное пространство такого цикла — целочисленный отрезок [1, jfin] — на зоны ответственности для параллельного выполнения. Вспомним, что на самом деле оператор цикла — это просто форма сокращения исходного текста программы. Если убрать это сокращение и развернуть цикл, то получим:

S11: . . . A[f(1)]. . .

S21: . . . A[g(1)]. . .

S12: . . . A[f(2)]. . .

S22: . . . A[g(2)]. . .

. . .

S1jfin: . . . A[f(jfin)]. . .

S2jfin: . . . A[g(jfin)]. . .

КОМПЬЮТЕРНЫЕ ИССЛЕДОВАНИЯ И МОДЕЛИРОВАНИЕ

Введение в распараллеливание алгоритмов и программ

255

Обозначение Ski использовано для «реинкарнации» оператора Sk на i-й итерации цикла.

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

Без ограничения общности получаем цикл:

do j = 1, jfin

S1 : A[f(j)] = . . .

S2 : . . . = . . . A[g(j)]. . .

enddo

Или в развернутом виде:

S11: A[f(1)] = . . .

S21: . . . = . . . A[g(1)]. . .

S12: A[f(2)] = . . .

S22: . . . = . . . A[g(2)]. . .

. . .

jfin

S1 : A[f(jfin)] = . . .

jfin

S2 : . . . = . . . A[g(jfin)]. . .

Легко видеть, что условия Бернстайна могут быть нарушены в том случае, если существуют значения итерационной переменной «j» λ и κ, 1 ≤ λ ≤ jfin, 1 ≤ κ ≤ jfin такие, что f (λ) = g(κ). Чтобы узнать существуют ли такие значения, нужно решить приведенное уравнение при указанных ограничениях в целых числах. Мы приходим к задаче Диофанта. Возможность решения диофантовых уравнений в общем случае составляет суть десятой проблемы Гильберта, алгоритмическую неразрешимость которой в 1970 году доказал Юрий Матиясевич. . .

В простых случаях, например, когда f и g — линейные функции, определить, существует ли решение и каково оно, конечно, возможно, но в общем случае — нет. Если решения не существует, то все операторы развернутого цикла независимы друг от друга и могут быть выполнены одновременно различными исполнителями, скажем, каждая итерация цикла — на своем исполнителе. Пусть решение существует, и мы нашли соответствующие κ и λ. Условия Бернстайна нарушены — между операторами есть зависимость. В этом случае оператор S1κ (где элемент массива A — выходная переменная) называют источником (source) зависимости, а оператор S2λ (где элемент массива A — входная переменная) называют стоком (sink) зависимости. Вычислим величину D = λ − κ (из итерации стока вычитаем итерацию источника). Эту величину принято называть расстоянием зависимости цикла.

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

2010, Т. 2, № 3, С. 231–272

256

В. Е. Карпов

ПРИМЕР 1. Пусть дан цикл

do i = 1, u

S1 : a[i] = d[i] + 5 * i S2 : c[i] = a[i+1] * 2

enddo

Вычислим расстояние зависимости, определим тип зависимости и возможность распараллеливания цикла.

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

S11: a[1] = d[1] + 5 * 1 S21: c[1] = a[2] * 2

S12: a[2] = d[2] + 5 * 2 S22: c[2] = a[3] * 2

. . .

Как видим, операторы S21 и S12 используют один и тот же элемент массива a[2]. При этом S12 является источником зависимости, а S21 — стоком. Соответственно, κ = 2, λ = 1. Расстояние зависимости D = λ−κ = −1. При последовательном выполнении операторов значение a[2] сначала используется, а затем изменяется — это антизависимость. Можно ли выполнить первую итерацию цикла на одном исполнителе, а вторую — на другом? Можно, если первый исполнитель использует старое значение a[2] до того, как второй исполнитель изменит его. Для этого достаточно перед выполнением цикла продублировать необходимые входные данные на исполнителях. Допустимое количество различных исполнителей совпадает с верхней границей цикла u, а возможное ускорение составляет u.

ОБЩЕЕ УТВЕРЖДЕНИЕ 1. Если расстояние зависимости D < 0, то между операторами тела цикла существует антизависимость. Цикл может быть распараллелен так, что каждая итерация будет выполняться отдельным исполнителем, если перед началом выполнения итераций продублировать необходимые входные данные на исполнителях.

ПРИМЕР 2. Пусть дан цикл

do i = 1, u

S1 : a[i] = d[i] + 5 * i S2 : c[i] = a[i-1] * 2

enddo

Вычислим расстояние зависимости, определим тип зависимости и возможность распараллеливания цикла.

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

S11: a[1] = d[1] + 5 * 1 S21: c[1] = a[0] * 2

S12: a[2] = d[2] + 5 * 2 S22: c[2] = a[1] * 2

. . .

КОМПЬЮТЕРНЫЕ ИССЛЕДОВАНИЯ И МОДЕЛИРОВАНИЕ

 

Введение в распараллеливание алгоритмов и программ

257

Как видим, операторы S1

и S2 используют один и тот же элемент массива a[1]. При

этом S1

1

2

 

является источником зависимости, а S2 — стоком. Соответственно, κ = 1, λ = 2. Рассто-

1

 

2

 

яние зависимости D = λ − κ = 1. При последовательном выполнении операторов значение a[1] сначала изменяется, а затем используется — это истинная зависимость. Выполнение итераций параллельно невозможно!

Но всегда ли запрещено распараллеливание при наличии истинной зависимости в цикле? Рассмотрим

ПРИМЕР 3. Пусть дан цикл

do i = 1, u

S1 : a[i] = d[i] + 5 * i S2 : c[i] = a[i-2] * 2

enddo

Вычислим расстояние зависимости, определим тип зависимости и возможность распараллеливания цикла.

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

S11: a[1] = d[1] + 5 * 1

S21: c[1] = a[-1] * 2

S12: a[2] = d[2] + 5 * 2 S22: c[2] = a[0] * 2

S13: a[3] = d[3] + 5 * 3

S23: c[3] = a[1] * 2

. . .

Как видим, операторы S11 и S23 используют один и тот же элемент массива a[1]. При этом S11 является источником зависимости, а S23 — стоком. Соответственно, κ = 1, λ = 3. Расстояние зависимости D = κ − λ = 2. При последовательном выполнении операторов значение a[1] сначала изменяется, а затем используется — это истинная зависимость. Но!!! Значение, вычисленное на 1-й итерации, используется для вычислений только на 3-й итерации. Значение, вычисленное на 3-й итерации, используется затем только на 5-й итерации и т. д. Аналогично все обстоит и для четных итераций. Выполнение итераций параллельно возможно на 2-х исполнителях, один из которых реализует только нечетные итерации, а другой — только четные!

ОБЩЕЕ УТВЕРЖДЕНИЕ 2. Если расстояние зависимости D > 0, то между операторами тела цикла существует потоковая зависимость. При D > 1 цикл может быть распараллелен не более чем на D исполнителях.

Неисследованным остался только случай D = 0.

ПРИМЕР 4. Пусть дан цикл

do i = 1, u

S1 : a[i] = d[i] + 5 * i S2 : c[i] = a[i] * 2

enddo

2010, Т. 2, № 3, С. 231–272

258 В. Е. Карпов

Вычислим расстояние зависимости, определим тип зависимости и возможность распараллеливания цикла.

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

S11: a[1] = d[1] + 5 * 1 S21: c[1] = a[1] * 2

S12: a[2] = d[2] + 5 * 2 S22: c[2] = a[2] * 2

. . .

Как видим, операторы S11 и S21 используют один и тот же элемент массива a[1]. При этом S11 является источником зависимости, а S21 — стоком. Соответственно, κ = 1, λ = 1. Расстояние зависимости D = λ−κ = −1. При последовательном выполнении операторов значение a[1] сначала вычисляется, а затем используется — это потоковая зависимость. Но вся зависимость локализована внутри одной итерации. Поэтому каждую итерацию можно исполнить на своем исполнителе. Допустимое количество различных исполнителей совпадает с верхней границей цикла u, а возможное ускорение составляет u. Если мы поменяем местами операторы S1 и S2 в теле цикла, то тип зависимости сменится на антизависимость, расстояние зависимости цикла останется прежним, как и возможность его распараллеливания.

ОБЩЕЕ УТВЕРЖДЕНИЕ 3. Если расстояние зависимости D = 0, то тип зависимости между операторами тела цикла в общем случае неопределен. Цикл может быть распараллелен так, что каждая итерация будет выполняться отдельным исполнителем.

Случай D = 0 принято называть странно звучащим на русском языке термином «зависимость, не зависящая от цикла» (loop independent dependence). Случай D 0 называют зависимостью, связанной с циклом (loop carried dependence).

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

do i = 1, u

S1 : a[i] = d[i] + 5 * i S2 : c[i] = a[2*i] * 2

enddo

f (i) = i, а g(i) = 2 · i.

Легко видеть, что f(2) = (1), f(4) = g(2), f(6) = g(3) и т. д. Расстояние зависимости точно определить не удается — D = ?. Для первого равенства D = −1, для второго — D = −2, для третьего — D = −3. Однако все эти значения меньше нуля, поэтому все зависимости являются антизависимостями и, следовательно, при необходимом дублировании входных данных возможно распараллеливание цикла по итерациям.

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

КОМПЬЮТЕРНЫЕ ИССЛЕДОВАНИЯ И МОДЕЛИРОВАНИЕ

Введение в распараллеливание алгоритмов и программ

259

Зависимости во вложенных циклах и их анализ на параллельность

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

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

do j1 = 1, u1 do j2 = 1,u2

. . .

do jn = 1, un

. . .

enddo

. . .

enddo enddo

В таких циклах конкретная итерация определяется совокупностью значений всех счетчиков j1, j2, . . . , jn. Будем рассматривать их набор как n-мерный вектор J = (j1, j2 , . . . , jn ) и назовем его итерационным вектором. Множество всех допустимых значений итерационных векторов образует итерационное пространство цикла. В этом пространстве между векторами можно ввести отношения порядка. Будем говорить, что I = J, если k, 1 ≤ k n, ik = jk, и что I < J в том случае, когда s, 1 ≤ s n, такое что k, 1 ≤ k < s, ik = jk, а is < js.

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

do j1 = 1, u1 do j2 = 1, u2

. . .

do jn = 1, un

S1 : A[f1(J), . . . , fn(J)] = . . .

S2 : . . . = . . . A[g1(J), . . . , gn(J)]. . .

enddo

. . .

enddo enddo

Здесь функции fk(J) и gk(J), 1 ≤ k n, есть целочисленные функции от n целых переменных. Задачей является выяснение возможности разбиения итерационного пространства такого цикла на зоны ответственности для параллельного выполнения.

2010, Т. 2, № 3, С. 231–272

260

В. Е. Карпов

Из предыдущей лекции «ежу понятно», что условия Бернстайна нарушаются, и, стало быть, зависимость возникает, если имеет решение система уравнений

F(K) = G(Λ),

где F — вектор-функция (f1, . . . , fn ), а G — вектор-функция (g1, . . . , gn ) при соблюдении условий

(0, . . . , 0) ≤ K ≤ (u1, . . . , un),

(0, . . . , 0) ≤ Λ ≤ (u1, . . . , un).

Понятно, что поиск решения системы диофантовых уравнений — задача несравненно более сложная, чем поиск решения одного диофантова уравнения. Если решения нет, то нет и зависимости операторов при выполнении цикла — каждая итерация может быть выполнена на своем исполнителе, максимальное количество которых — это u1 × . . . × un. Допустим, что с помощью некоторых колдовских приемов нам удалось определить, что решение существует, и найти соответствующие K и Λ. Введем для цикла понятие вектора расстояний зависимости (или просто вектора расстояний) следующим образом:

D = Λ K

(из вектора итераций, соответствующего итерации стока зависимости, вычитаем вектор итерации, соответствующий итерации источника зависимости).

Так, например, определим для двумерного цикла

do i = 1, u1 do j = 1, u2

S1 : a[i, j] = b[i, j] * 2 S2 : c[i,j] = a[i, j-1] + 1

enddo enddo

значение вектора расстояний. Для этого развернем наш цикл в итерационном пространстве:

S1(1,1): a[1, 1] = b[1, 1] * 2

(1,1)

S2 : c[1, 1] = a[1, 0] + 1 S1(1,2): a[1, 2] = b[1, 2] * 2

(1,2)

S2 : c[1, 2] = a[1, 1] + 1

. . .

S1(2,1): a[2, 1] = b[2, 1] * 2

(2,1)

S2 : c[2, 1] = a[2, 0] + 1

. . .

Легко видеть, что операторы S1(1,1) и S2(1,2) используют один и тот же элемент массива

a[1, 1], при этом оператор S1(1,1) является источником зависимости, а оператор S2(1,2) является ее стоком. Поэтому K = (1, 1), Λ = (1, 2), и вектор расстояний есть D = (0, 1).

Определить тип существующей зависимости по данным (а в нашем случае — это истинная зависимость) и возможность распараллеливания цикла по виду вектора расстояний не так просто. Поэтому вводится понятие вектора направлений для цикла. Понятие, с одной стороны, достаточно простое, но с другой стороны — несколько странное. Может быть, при его первом появлении в научной статье возникла опечатка, перекочевавшая в остальные научные работы. Я не

КОМПЬЮТЕРНЫЕ ИССЛЕДОВАНИЯ И МОДЕЛИРОВАНИЕ

Соседние файлы в папке Паралпрог