DVMH-Indirect (1158460), страница 3
Текст из файла (страница 3)
i0000m(rows4 + 1 * rows3 + i * rows2) = (i — 1) * 2 + 1
i0000m(rows4 + 2 * rows3 + i * rows2) = i * 2 + 1
Enddo
для первого варианта и
Do i = begIdx, endIdx
i0000m(calc_idx(rows, i0000m, i, 1)) = (i — 1) * 2 + 1
i0000m(calc_idx(rows, i0000m, i, 2)) = i * 2 + 1
Enddo
для второго варианта, т. е. РТС выдает диапазон глобальных индексов, и компилятор ведет счет в глобальных. Такая реализация довольно медленная, далее будет цикл с указанием REFERENCE, там будет улучшенный вариант. Также возникает вопрос, как быть с тем, что множество индексов цикла исходного может переходить в не непрерывное множество индексов локальной части процесса, это, думаю, лучше решать на уровне РТС, т. е. будет несколько вызовов обработчика с непрерывными участками. Другой вариант — делаем вызов на объемлющий участок, а компилятор вставит условие проверки на включение витка, есть опасение, что будет слишком неэффективно на каждом витке иметь if.
Идем дальше – как скомпилировать такой REALIGN:
!DVM$ REALIGN cols(rows(1, i):rows(2, i) - 1) WITH rows(*, i)?
Вариант первый: конвертер, имея более широкие возможности (может сразу подставлять указанные пользователем выражения) вычисляет “поэлементное” выравнивание, прогоняя цикл по локальной части массива rows и записывая начало и конец в массив, который затем передается РТС под видом объединения интервалов. (этот вариант должен быть самым быстрым) – вполне годится.
Вариант второй: конвертер, опять же имя более широкие возможности, готовит небольшую функцию, которая будет возвращать концы интервала для каждого из индексов rows, передает ссылку на нее в РТС, а прогон осуществляет уже РТС. (помедленнее, т.к. много вызовов)
Вариант третий: придумываем как можно описать заданные пользователем формулы, передаем их в РТС, она их вычисляет. (самый медленный вариант, и сложный).
Идем дальше. Идет внешне похожий на предыдущий цикл, но с особенностями:
!DVM$ PARALLEL(i) ON rows(*, i)
Do i = 2, n - 1
cols(2 * i - 1) = i - 1
cols(2 * i) = i + 1
Enddo
Плох он тем, что выравнен на одно, а работа внутри идет совсем с другим.
При этом тут такая неприятность: rows распределен блочно, а cols, который на него выравнен, - нет. Тут нарушается наше предположение, что в параллельных циклах, отображенных на блочно-распределенный массив, все используемые массивы блочно-распределенные. Надеемся, что компилятор в силах для цикла определить все используемые в нем массивы, тогда пусть используется старая технология только если все (а не только тот, на который отображен цикл) используемые массивы распределены блочно. Тогда ничего фатального в том, что cols будет считаться распределенным поэлементно, не будет.
Лучше бы его переписать, но и в таком виде можно его скомпилировать по общему правилу: компилятор готовит два варианта выполнения и проверяет, все ли массивы распределены блочно (rows и cols). Зарисуем соответствующие обработчики:
Do i = begIdx, endIdx
i0000m(cols3 + (2 * i - 1) * cols2) = i — 1
i0000m(cols3 + (2 * i) * cols2) = i + 1
Enddo
для первого варианта и
Do i = begIdx, endIdx
i0000m(calc_idx(cols, i0000m, 2 * i - 1)) = i — 1
i0000m(calc_idx(cols, i0000m, 2 * i)) = i + 1
Enddo
для второго варианта, т. е. РТС выдает диапазон глобальных индексов, а компилятор строит обращения как к поэлементно распределенным массивам, индексируемым глобальными индексами. Этот цикл вряд ли удастся улучшить в рамках текущего предложения по расширению, так как значения индексных переменных цикла используются как значения, а значит нельзя переходить к локальной индексации.
Идем далее. Применяется полученное извне поэлементное распределение, производятся нужные выравнивания.
Теперь новые теневые грани. Теневая грань — это набор элементов, не принадлежащих текущему процессу (требование принадлежности соседнему процессу снимается), для которых
-
возможен доступ без специальных указаний откуда угодно
-
обновление производится указанием SHADOW_RENEW
Теневые грани для поэлементно распределенных массивов по-умолчанию пустые и их можно наращивать, т. е. директива SHADOW становится исполняемой, в ней задается множество элементов теневой грани и имя (у массива бывает несколько теневых граней). При этом множество элементов задается примерно как при выравнивании одного массива на другой. Например:
!DVM$ SHADOW x(cols(rows(1, i):rows(2, i) - 1)) NAME=own
означает создать теневую грань для массива x под названием own и отнести к ней элементы с индексами от cols(j), где j пробегает от rows(1,i) до rows(2,i)-1, перебирая всю локальную часть rows (естественно, за вычетом локальной части массива x, это РТС отбросит автоматически). Предлагаю делать так же — компилятор осуществляет проход по rows, затем проход от rows(1,i) до rows(2,i)-1 и заполняет массив ссылок, потом отдает его РТС, она вычитает локальную часть массива x и получается теневая грань с именем own. Проблема в размере этого массива — с одной стороны в том, что он потенциально довольно большой (стоит рассмотреть передачу в РТС теневой грани частями), а с другой стороны его размер и вовсе неизвестен, пока не перебрать все указанные элементы. Вероятно, следует передавать порциями, тогда обе проблемы решатся.
Далее следует довольно интересный вид параллельного цикла:
!DVM$ PARALLEL(i) ON a(i), REFERENCE(a(:) <= i)
Do i = 1, 2 * n
a(i) = 0.5
Enddo
Это — цикл с локальной индексацией. В нем сказано, что всякое обращение к массиву a есть обращение по переменной цикла i, а также, что любое использование переменной цикла i есть индексация массива a. Для такого цикла делается один вариант реализации — только с поддержкой поэлементных распределений (который, кстати говоря, в этом конкретном примере совпадает с реализацией для циклов с только блочно-распределенными массивами). В вызове loop_create делается указание на то, что цикл надо вести по локальным индексам, а чьим локальным индексам определяется правилом выравнивания. Параллельный цикл с указанием REFERENCE может быть только одномерным, в указании REFERENCE в правой части может стоять только переменная цикла, а в левой части обязан стоять массив, на который цикл отображен, а также могут находится иные массивы, но тогда действует правило связывания, т. е. компилятор, видя, что в REFERENCE в левой части не один массив, делает дополнительный вызов связывания массивов с тем, чтобы их можно было бы индексировать одним и тем же локальным индексом.
Обработчик выглядит так:
Do i = begIdx, endIdx
r0000m(a3 + i * a2) = 0.5
Enddo
Далее имеется цикл, который использует значение глобального индекса, а значит ситуация полностью аналогична второму циклу — будет сгенерировано 2 варианта и будет выбран вариант 2, в котором будет переход от глобальных индексов к локальным. Схематично обработчик:
Do i = begIdx, endIdx
r0000m(calc_idx(y, r0000m, i)) = i
Enddo
Этот цикл можно было бы вывернуть наизнанку, сделав прогон по локальным индексам, но присваивая глобальное значение i, но как это описать на языке по-хорошему - не понятно.
Далее идет новая мощная конструкция:
!DVM$ STRICT REFERENCING REGION, REFERENCE(x(:), y(:), rows(*, :) <= cols(i)), REFERENCE(a(:), cols(:) <= rows(1, i):rows(2, i) - 1)
Эта конструкция, по сути, - указание компилятору какие массивы можно будет индексировать локальными индексами, а также место пересчета индексных массивов в локальные индексы. Конструкция предполагается статической, так как активно используется компилятором и значительно влияет на генерируемый код. Ее суть пояснена в тексте программы. Остается вопрос как это скомпилировать. Можно разбить ее компиляцию на следующие этапы — определяем количество групп связанных массивов, вызываем процедуры связывания для каждой группы; затем для каждого массива из правых частей делаем вызов, который регистрирует ссылку (для не являющихся диапазонами); для диапазонов делаем свой вызов, который регистрирует ссылку на диапазон (т. е. сразу для пары индексных выражений). Так как в правой части запрещены каскадные индексации и арифметические операции с i (но разрешены линейные преобразования над значениями индексных массивов), то не будет неподъемной задачей выразить то, что написал там пользователь. Так как требования довольно жесткие по индексации в ней выражаются, то дополнительно надо будет включать ее действие в циклах указанием BYREF, дабы не было нужды разрывать такой регион (что может быть очень дорого), тем более, что он никак не мешает обрабатывать циклы так же, как и вне такого региона (слово регион вроде как уже занято, но другого не придумалось). Это указание BYREF и есть пометка цикла, как цикла по графу, но без слова граф и специфических для него понятий вроде вершин и ребер.
Про оставшиеся два цикла пояснения в программе довольно развернутые. Здесь упомяну, что компилятор для них делает только 1 вариант — с поддержкой поэлементных распределений и действует с выполнением указаний REFERENCE, т. е. всякая ссылка на массив, появляющийся в левой части указания REFERENCE делается по локальным индексам, а всякая ссылка на массив, появляющийся в правой части указания REFERENCE заменяется на ссылку на другой массив, который подготовит РТС при входе в этот регион (в нем значения будут локальными индексами). При этом если массив встречается и в левой части и в правой, то применяются оба правила — ссылка заменяется на ссылку на другой массив, а индексируется он локальными индексами.
Рассмотрим следующий цикл:
!DVM$ PARALLEL(i) ON x(i), BYREF
Do i = 1, n
x(i) = y(i)
Enddo
Его обработчик будет выглядеть так:
Do i = begIdx, endIdx
r0000m(x3 + i * x2) = r0000m(y3 + i * y2)
Enddo
То есть прямо как для блочно-распределенных массивов.
Следующий цикл уже немного интереснее:
!DVM$ PARALLEL(i) ON y(i), BYREF, SHADOW_RENEW(x(own))
Do i = 1, n
s = 0
Do j = rows(1, i), rows(2, i) - 1
s = s + a(j) * x(cols(j))
Enddo
y(i) = s
Enddo
Его обработчик будет выглядеть так:
Do i = begIdx, endIdx
s = 0
Do j = i0000m(rows_localized4 + 1 * rows_localized3 + i * rows_localized2), i0000m(rows_localized4 + 2 * rows_localized3 + i * rows_localized2) - 1
s = s + r0000m(a3 + j * a2) * r0000m(x3 + i0000m(cols_localized3 + j * cols_localized2) * x2)
Enddo
r0000m(y3 + i * y2) = s
Enddo
Пример программы. Неструктурные сетки.
Используем имеющиеся конструкции, но расширяем их возможности.
Рассмотрим пока только узловые функции. При этом, как следствие, неважно какова размерность пространства – геометрические свойства ячеек и прочее, имеется только набор «соседей».
Пример – алгоритм Якоби на кольце.
Исходная программа:
Program test2
Parameter (nnode=1000, maxneigh=10)
Real fi(nnode), psi(nnode)
Integer neighbor(nnode, maxneigh)
! Кол-во соседей
Integer neighbors(nnode)
! Init
! Grid
Do I = 1, nnode
! одномерное кольцо
Neighbors(i) = 2
Neighbor(I, 1) = modulo(I, nnode) + 1
Neighbor(I, 2) = modulo(I – 2, nnode) + 1
Enddo
! Functions
Do I = 1, nnode
Fi(i) = 1
Psi(i) = 2
enddo
! Calculate
Do it = 1, itcount
! Jacobi iteration
Do I = 1, nnode
Psi(i) = 0
Do j = 1, neighbors(i)
Psi(i) = psi(i) + fi(neighbor(I,j)) / neighbors(i)
enddo
Enddo
Do I = 1, nnode
fi(i) = 0
Do j = 1, neighbors(i)
fi(i) = fi(i) + psi(neighbor(I,j)) / neighbors(i)
enddo
Enddo
enddo
end
DVM-программа:
Program test2dvm
Parameter (nnode=1000, maxneigh=10)
Real fi(nnode), psi(nnode)
Integer neighbor(nnode, maxneigh)
! Кол-во соседей
Integer neighbors(nnode)
!DVM$ ALIGN :: fi, psi, neighbor, neighbors
!DVM$ DYNAMIC :: fi, psi, neighbor, neighbors
!DVM$ SHADOW neighbor(0:0, 0:0)
!DVM$ SHADOW neighbors(0:0)
! Init
!DVM$ REDISTRIBUTE(BLOCK) :: neighbors
!DVM$ REALIGN neighbor(I, *) with neighbors(i)
! Grid
!DVM$ PARALLEL(i) on neighbors(i)
Do I = 1, nnode