Главная страница
Навигация по странице:

  • 3.3 Эквивалентные преобразования алгоритмов, избыточность вычислений и обменов данными

  • Параллельные вычисления


    Скачать 1.75 Mb.
    НазваниеПараллельные вычисления
    Дата06.05.2019
    Размер1.75 Mb.
    Формат файлаpdf
    Имя файла[Bakanov_V.M.]_Parallelnuee_vuechisleniya__Uchebno(z-lib.org).pdf
    ТипУчебное пособие
    #76301
    страница12 из 16
    1   ...   8   9   10   11   12   13   14   15   16
    а циклов for (i = 1; i < N; i++) for (j = 1; j < M; j++)
    (7)
    A[i][j] = A[i-1][i] + A[i][j-1]; метод координат неприменим, т.к. любое разбиение (и по измерению I и по измерению J) перпендикулярными осям координат плоскостями приводит к разрыву информационных зависимостей. Несмотря на это можно заметить, что существуют удовлетво- ряющие условию i+j=const гиперплоско- сти (пунктир на рис.22, проходящие через не со- держащие информаци- онных зависимостей вер- шины; при этом па- раллельно работающим процессорам следует на- значить вычисления тела цикла для каждой из ги- перплоскостей i+j=const
    Часто удается добить- ся повышения эффек- тивности распараллели- вания программы при помощи эквивалентных
    преобразований - таких преобразований кода программы, при которых полностью сохраняется результат ее выполнения при изменении последовательности действий.
    Рисунок 21 — Пространство итераций для фрагмента (6)

    - 83 -
    Первым этапом рабо- ты по распараллелива- нию является выявление зависимостей; второй шаг - распараллелива- ние циклов с использо- ванием результатов первого этапа. Примеры автоматических (выпол- няемых распараллели- вающим компилятором) эквивалентных преобра- зований в применении к циклам приведены ниже в табл.6.
    Если последователь- ная программа содер- жит два оператора S
    1
    и S
    2
    , причем S
    1
    находится перед S
    2
    , то говорят, что ме- жду этими двумя операторами существует зависимость по данным, если они считывают или записывают данные в общей области памяти таким образом, что порядок их выполнения нельзя изменять без изменения конечного ре- зультата вычислений. Имеются три основных типа зависимости по данным:

    Потоковая зависимость - оператор S
    2
    потоково зависит от S
    1
    , если S
    2
    счи- тывает из ячейки, в которую записывает S
    1
    (такая зависимость называет- ся истинной).

    Антизависимость - оператор S
    2
    является антизависимым относительно S
    1
    , если S
    2
    записывает в ячейку, из которой S
    1
    считывает.

    Зависимость по выходу - оператор S
    2
    зависит по выходу от S
    1
    , если опе- ратор S
    2
    записывает данные в ту же ячейку памяти, что и S
    1
    Часто просто говорят ‘S
    2
    зависит от S
    1
    ’, если между ними существует за- висимость по данным (тип зависимости не важен). Зависимости по данным легко определяется в последовательном коде, содержащем ссылки только на скаляры. Намного труднее определить зависимости в циклах и при ссылках на массивы (что обычно встречается вместе), поскольку ссылки в массивы имеют индексы, а в индексах часто используются параметры циклов, т.е. ин- дексы массива имеют различные значения на разных итерациях цикла. Общая проблема вычисления всех зависимостей по данным в программе неразре- шима из-за синонимичности имен массивов, которая возникает при исполь- зовании указателей или вызовов функций внутри индексных выражений. Да-
    Рисунок 22 — Пространство итераций для фрагмента (7)

    - 84 - же если указатели не разрешены (как в Fortran’e) и индексные выражения яв- ляются линейными функциями, проблема является NP-трудной, т.е. эффек- тивного алгоритма решения ее скорее всего не существует.
    Ниже рассматриваются несколько полезных преобразований для циклов:
    локализация, расширение скаляра, распределение цикла, слияние циклов, раз-
    вертка и сжатие, развертка цикла, разделение на полосы, разделение на
    блоки, перекос цикла, которые помогают выявлять параллельность, устранять зависимости и оптимизировать использование памяти:

    Перестановка циклов - внешний и внутренний циклы меняются местами.

    Локализация - каждому процессу дается копия переменной.

    Расширение скаляра - скаляр заменяется элементом массива.

    Распределение цикла - один цикл расщепляется на два отдельных цикла.

    Слияние циклов - два цикла объединяются в единый.

    Развертка и сжатие - комбинируются перестановка циклов, разделение на полосы и развертка.

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

    Разделение на полосы - итерации одного цикла разделяются на два вло- женных цикла.

    Разделение на блоки - область итераций разбивается на прямоугольные блоки.
    • Перекос цикла - границы цикла изменяются таким образом, чтобы вы- делить параллельность фронта волны.
    Распределение циклов используется для устранения зависимости в цикле (фак- тически для выявления параллелизма). Если исходный цикл имеет вид (здесь и далее используется нотация языка программирования, равноудаленная и от For- tran’а и от C) for [i=1 to n] { a[i] = ;
    = + a[i-1];
    }
    Здесь второй оператор тела цикла зависит от результата, вычисленного пер- вым на предыдущей итерации. При условии отсутствия иных зависимостей
    (кроме указанной) цикл распределяется следующим образом (каждый из двух циклов легко распараллеливается, хотя сами циклы должны выполняться после- довательно): for [i=1 to n] a[i] = ;

    - 85 - for [i=1 to n]
    = + a[i-1];
    При слиянии циклы, имеющие одинаковые заголовки и независимые тела, объединяются (сливаются) в единый цикл; слияние приводит к уменьшению числа процессоров и их укрупнению (более крупнозернистый параллелизм).
    Развертка и сжатие приводит к сближению обращений к ячейкам памяти, при этом производительность повышается за счет кэширования. В примере ум- ножения матриц: for [i=1 to n] for [j=1 to n] for [k=1 to n c[i, j] = c[i, j] + a[i, k] * b[k, j];
    сначала разворачивается внешний цикл, затем полученные циклы сжима-
    ются (сливаются) таким образом, чтобы операторы сложения оказались в цикле максимальной вложенности. После развертки внешнего цикла и сжатия выше- расположенного фрагмента получим (считая n
    четным): for [i=1 to n by 2] // половина всех итераций (только нечетные i) for [j=1 to n] for [k=1 to n]
    (8)
    { c[i, j] = c[i, j] + a[i, k] + b[k, j]; c[i+1, j] = c[i+1, j] + a[i, k] + b[k, j];
    }
    Здесь в каждом внешнем цикле вычисляются два скаляра c[i, j]
    и c[i+1, j]
    , рас- полагающиеся в смежных ячейках памяти (при условии, что матрицы в ОП хра-
    нятся по столбцам – что характерно для Fortran’а и отнюдь не для C/C++). А что мешает в каждом внешнем цикле вычислять не 2, а 3,4,5.. скаляров (конечно, если n
    не делится нацело на это число, понадобится некоторое дополнение алго- ритма)?
    В случае разделения на полосы цикл делится надвое, причем во внешнем цик- ле перебираются полосы, а внутренний итерирует все элементы данной полосы.
    Например, для умножения матриц единократное развертывание внешнего цикла равноценно разделению полосы шириной 2 итерации (дополнительные итера- ции производятся по индексу i1
    ): for [i=1 to n by 2] // половина всех итераций (только нечетные i) for [i1=i to i+1] // полоса из двух итераций for [j=1 to n] for [k=1 to n] c[i1, j] = c[i1, j] + a[i1, k] + b[k, j];

    - 86 -
    Преобразование развертки и сжатия равноценны разделению на полосы внешнего цикла, перемещению разделенного цикла на место внутреннего и его развертки (цикл максимальной вложенности использует индекс i1
    ): for [i=1 to n by 2] // половина всех итераций (только нечетные i) for [j=1 to n] for [k=1 to n] for [i1=1 to i+1] // полоса из двух итераций c[i1, j] = c[i1, j] + a[i1, k] + b[k, j];
    После развертки внутреннего цикла (т.е. замены i1
    двумя значениями – i
    и i+1
    ) получается программа, равноценная полученной преобразованием развертки и сжатия (8).
    При разделении на блоки распределение данных в памяти компактируется и, следовательно, повышается эффективность кэширования на МВС с общей памя- тью или размещение данных в распределенной памяти. В качестве примера рас- сматривается фрагмент транспонирования матрицы: for [i=1 to n] for [j=1 to n] b[i, j] = a[j, i];
    При условии хранения в памяти массивов по строкам (характерно для C/C++) д
    о ступ к элементам матрицы b
    осуществляется с шагом 1 (каждый элемент b
    размещается рядом с предыдущим вычисленным элементом b
    в соответствии с внутренним циклом по j
    ). Однако доступ к элементам матрицы a
    осуществляется с шагом n
    (длина всей строки). Т.о. при ссылках на a
    кэш используется неэф- фективно (в кэш загружается излишне много значений элементов a
    , из которых реально нужна всего
    1/n
    ).
    Разбиение матрицы на блоки делит область итераций на прямоугольники
    (квадраты в частном случае), размер данных в которых не превышают размера
    кэша. При желании полностью поместить в кэш m
    ×m
    (само собой, m) чисел
    (значений матриц a
    или b
    ) следует изменить алгоритм транспонирования таким образом: for [i=1 to n by m] // для каждой m-й строки for [j=1 to n by m] // и m-ного столбца… for [i1=i to min(i+m-1, n)] // блок размером m строк for [j1=j to min(j+m-1, n)] // блок размером m столбцов b[i1, j1] = a[j1, i1];

    - 87 -
    Теперь в двух внутренних циклах доступен квадрат из m
    ×m элементов мат- риц a
    и b
    , который полностью помещается в кэш. Преобразование разделения на блоки является комбинацией разделения на полосы и перестановки циклов.
    Преобразование перекос циклов используется с целью выделения скрытой па- раллельности в основном вдоль волновых фронтов (в этом случае обновления величин в массивах распространяется подобно волне). Типичный пример – ре- шение дифуравнений в частных производных методом Гаусса-Зейделя при 5- точечном шаблоне на равномерной квадратной сетке размером n
    ×n
    [3]:
    Рисунок 23 — Зависимости по данным в методе итераций Гаусса-Зейделя: a) – исход- ное пространство итераций, б) – пространство итераций после сдвига цикла. for [i=1 to n] for [j=1 to n]
    a[i, j] = 0.25
    × (a[i-1, j]) + a[i, j-1] + a[i+1, j] + a[i, j+1]);
    При перекосе цикла смещаются границы цикла таким образом, чтобы волно- вые фронты устанавливались не по диагонали, а по столбцам (что иллюстриро- вано схемой рис.23). Перекос осуществляется методом прибавления к гранич- ным значениям индекса внутреннего цикла числа, кратного величине индекса внешнего цикла, затем в теле внутреннего цикла из значения индекса это число вычитается. Образующий кратное сомножитель называется сомножителем пе-
    рекоса. Преобразованный т.о. вышеприведенный фрагмент при равном 1 со- множителе перекоса приведен ниже: for [i=1 to n] for [j=i+1 to i+n] // прибавить i к имеющимся границам
    (9)
    a[i, j-i] = 0.25
    × (a[i-1, j-i]) + a[i, j-1-i] + a[i+1, j-i] + a[i, j+1-i]);
    Перекос цикла позволил выявить параллельность, ибо после преобразований зависимостей в каждом столбце области итераций больше нет, однако зависи- мости остались во внешнем цикле.
    Из фрагмента (9) видно, что границы внутреннего цикла зависят от значений индекса внешнего. Однако границы исходных циклов независимы, поэтому вы-

    - 88 - полнить перестановку циклов возможно. После перестановки циклов получаем для общего случая (
    Li, Ui
    – нижняя и верхняя границы исходного внешнего цик- ла,
    Lj и
    Uj
    – то же для внутреннего цикла, k
    – сомножитель перекоса): for [j = (k
    ×LI + Lj) to (k×Ui + Uj)] for [i = max(Li, ceil((j - Uj) / k)) to min(Ui, ceil(j - Lj) / k))]

    Конкретное применение этого преобразования для метода Гаусса-Зейделя приводит к такому коду: for [j=2 to n+n] for [i=max(1, j-n) to min(n, j-1)] a[i, j-i] = 0.25
    × (a[i-1, j-i]) + a[i, j-1-i] + a[i+1, j-i] + a[i, j+1-i]);
    После этого внутренний цикл несложно распараллелить, напр., путем созда- ния процессов для каждой итерации внешнего цикла.
    Несмотря на наличие аппарата выявления информационных зависимостей в циклах, претендующий на опыт распараллеливания алгоритмов программист должен приучаться видеть параллелизм в простых конструкциях, например for (i = 1; i < N; i++) {
    A[i] = A[i] + B[i];
    (8)
    C[i] = C[i-1] + B[i];
    }
    В приведенном фрагменте существует зависимость по информации между i- той и (i-1)-й итерациями в операторе
    C[i] = C[i-1] + B[i]
    , поэтому просто распре- делить итерации между процессорами нельзя. Однако фрагмент (8) легко разбить (операция распределение циклов) на два одномерных цикла (т.к. опе- раторы
    A[i] = A[i] + B[i]
    и
    C[i] = C[i-1] + B[i]
    информационно независимы), причем первый эффективно распараллеливается: for (i = 1; i < N; i++)
    A[i] = A[i] + B[i]; for (i = 1; i < N; i++)
    C[i] = C[i-1] + B[i];
    3.3 Эквивалентные преобразования алгоритмов,
    избыточность вычислений и обменов данными
    Эквивалентным преобразованием (ЭкП) алгоритма является замена данно- го алгоритма (или его части) на алгоритм, гарантирующий получение такого же конечного результата на всех наборах входных данных в точной арифме-

    - 89 - тике (учет ограниченной машинной точности представления данных может накладывать ограничения на ЭкП).
    Одним из ЭкП, достаточно часто используемое в распараллеливающих компиляторах, является редукция высоты дерева. Один из методов реализа- ции такого преобразования – использование ассоциативных, коммутативных и дистрибутивных свойств арифметики, таким путем компиляторы могут об- наруживать неявный параллелизм в алгебраических выражениях и генериро- вать объектный (а затем и исполняемый) код для многопроцессорных систем с указанием операций, которые можно выполнять одновременно.
    На рис.24 показано (в виде графа алгоритма), как от- транслировал бы алгебраиче- ское выражение обычный компилятор (слева) и как оно может быть обработано рас- параллеливающим компиля- тором (справа); при этом не принимается во внимание возможное изменение точно- сти вычислений [2]. Как вид- но, во многих случаях воз- можно уменьшить число яру- сов (редуцировать высоту де- рева) путем выявления (ранее скрытого) параллелизма в вы- числениях.
    Не следует забывать, что подобная оптимизация про- грамм совсем не бесплатна – необходимы увеличенные за- траты времени и вычисли- тельных ресурсов в период анализа/компиляции.
    Очень эффективны эквива- лентные преобразования при оптимизации циклов (гнезд циклов).
    В табл.6 приведены некото- рые преобразования циклов, автоматически осуществляе- мые распараллеливающим
    Fortran-компилятором
    Рисунок 24 — Уменьшение высоты дерева: a) - за счет ассоциативности, б) - за счет коммутатив- ности и в) - за счет дистрибутивности (слева – обычный компилятор, справа - распараллели- вающий)

    - 90 -
    Lahey/Fujitsu Fortran’95 (LF95, http://www.lahey.com) для Linux (LF95’Pro включает возможность автоматической и OpenMP-параллелизации). Распа- раллеливающие компиляторы постоянно совершенствуются и в настоящее время пригодны для создания эффективных параллельных программ с разде- ляемыми переменными; особенно это касается научных программ, содержа- щих много циклов и длительных вычислений.
    В табл.6 приведены некоторые преобразования циклов, автоматически осуществляемые распараллеливающим Fortran-компилятором Lahey/Fujitsu
    Fortran’95 (LF95, http://www.lahey.com
    ) для Linux (LF95’Pro включает возмож- ность автоматической и OpenMP-параллелизации). Распараллеливающие компиляторы постоянно совершенствуются и в настоящее время пригодны для создания эффективных параллельных программ с разделяемыми пере- менными; особенно это касается научных программ, содержащих много цик- лов и длительных вычислений.
    Таблица 6 — Операции над массивами и автоматическая параллелиза- ция (на примере Fortran-базированной распараллеливающей систе- мы LF95’Pro).


    Преобразо-
    вание
    До
    преобразо-
    вания
    После
    преобразо-
    вания
    После
    распараллеливания
    1
    Сечения (slic- ing) циклов do i=1, 50000 a(i)=b(i)+c(i) end do
    Процессор 1:
    do i1=1,25000 a(i1)=b(i1)+c(i1) end do
    Процессор 2:
    do i2=25001,50000 a(i2)=b(i2)+c(i2) end do
    2
    Перестановка вложенных
    (nested) циклов do i= 2,10000 do j=1,10 a(i,j)=a(i-1,j)
    +b(i,j) end do end do do j=1,10 do i=2,10000 a(i,j)=a(i-1,j)
    +b(i,j) end do end do
    Процессор 1: do j=1,5 do i=210000 a(i,j)=a(i-1,j)
    +b(i,j) end do end do
    Процессор 2: do j=6,10 do i=2,10000 a(i,j)=a(i-1,j)
    +b(i,j) end do end do
    3
    Слияние (fu- sion) циклов do i=1,10000 a(i)=b(i)+c(i) end do do i=1,10000 d(i)=e(i)+f(i) end do do i=1,10000 a(i)=b(i)+c(i) d(i)=e(i)+f(i) end do
    Процессор 1: do i=1,5000 a(i)= b(i)+c(i) d(i)=e(i)+f(i) end do
    Процессор 2: do i=5001,0000 a(i)=b(i)+c(i) d(i)=e(i)+f(i) end do
    4
    Приведение
    (reduction) циклов.
    Приведение циклов может sum=0 do i=1,10000 sum=sum+a(i) end do
    Процессор 1: sum1=0 do i=1,5000 sum1=sum1+a(i) end do
    Процессор 2: sum2=0 do i=5001,0000 sum2=sum2+a(i) end do

    - 91 - быть причиной изменений в результатах (в пределах оши- бок округле- ния).
    Далее частичные суммы складываются: sum=sum1+ sum2
    Во многих случаях приведенные (лежащие, впрочем, на поверхности) пре- образования помогают и разработчику при создании новых программ. Одна- ко перед реальным программированием в любом случае полезно обдумать последовательность вычислений (алгоритмизацию). Классический пример – параллельная сортировка (по аналогии с лежащим на поверхности распарал- леливанием алгоритма суммирования элементов массива складывается убеж- дение в возможности эффективного крупноблочного распараллеливания ме- тодом сортировки на каждом процессоре части общего массива; однако по- нимание проблем, связанных с пересечением частично отсортированных множеств приходит обычно позднее); последовательные и мелкозернистые параллельные алгоритмы сортировки описаны, напр., в работе [3].
    Распараллеливающие компиляторы хорош
    1   ...   8   9   10   11   12   13   14   15   16


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