Глава 4. Мультипроцессорная обработка (только версия PRO)
В начало страницы
В этой главе описываются методы параллельного исполнения программы на Fortran. program in
Параллельная обработка Fortran-программ носит название мультипроцессорной
обработки или мультипроцессинга.
В начало страницы
В настоящем документе мультипроцессинг означает, что одна программа
выполняется на двух или более процессорах, которые могут работать независимо и
одновременно. Здесь это не означает одновременного выполнения двух или более
программ. Рассмотрим следующий код:
do i = 1, 50000
a(i) = b(i) + c(i)
end do
Различные итерации цикла DO выполняются на разных процессорах в одно и то же
время.
CPU 1:
do i1 = 1, 25000
a(i1) = b(i1) + c(i1)
end do
CPU 2:
do i2 = 25001, 50000
a(i2) = b(i2) + c(i2)
end do
В начало страницы
Эффект мультипроцессинга состоит в экономии времени исполнения за счет
использования двух или более процессоров одновременно. Например, если цикл DO
может исполняться параллельно разделением его, как показано выше, то
теоретически время исполнения этого цикла DO может быть сокращено в два раза.
На практике увеличение производительности требует осторожности и труда со
стороны программиста, как это объясняется в следующих разделах.
Хотя время исполнения при мультипроцессинге обычно уменьшается, общее
процессорное время, требуемое на исполнение программы, может возрасти. Это
происходит потому что общее процессорное время, по меньшей мере равно времени
исполнения программы на одном процессоре, и дополнительное время на
мультипроцессинг может увеличить общее процессорное время.
В начало страницы
Увеличение скорости при использовании мультипроцессинга с помощью LF95 PRO
происходит от расщепления циклов по доступным процессорам. В число
препятствий увеличения производительности входят:
* Накладные расходы на инициализацию и управление подпроцессами с
вспомогательными процессорами.
* Отсутствие больших массивов и циклов, их обрабатывающих.
* Программы, интенсивно использующие не вычисления, а ввод/вывод.
* Возможность получения неправильных результатов.
* Циклы, которые нельзя параллелизовать.
Эти препятствия рассматриваются в следующих ниже разделах.
В начало страницы
Трата времени на запуск и прекращение подпроцессов (отдельных исполнительных
потоков) на вспомогательном процессоре. Это время может перевесить время,
выигранное посредством выполнения части кода на вспомогательном процессоре,
если доля работы, исполняемая на этом процессоре, незначительна.
В начало страницы
Если ваша программа не тратит большую часть своего времени на интенсивные
вычислительные циклы, то нет и адекватной возможности для разделения его между
процессорами. Она, вероятно, будет столь же быстро работать и без
парраллелизации. Например, если половину времени ваша программа тратит на
циклы, которые можно параллелизовать, то максимальная экономия времени за счет
использования двух процессоров составит 25%. И если программа при
последовательном исполнении берет две минуты и половина этого времени тратится
на циклы, которыые можно параллелизовать, то оптимальное параллельное
исполнительное время будет одна минута и 30 секунд.
В начало страницы
Если ваша программа тратит много времени на чтение или запись файлов или
на ожидание пользовательского ввода, то, вероятно, всякое ускорение ее за счет
параллелизации будет очень небольшим на фоне трат на ввод/вывод.
В начало страницы
Возможность парралелизации некоторых циклов может быть установлена
компилятором без помощи программиста. Однако, многие циклы связаны
зависимостью данных, которая препятствует автоматической параллелизации
из-за возможности ошибок в результатах. По этой причине LF95 PRO содержит
строки контроля за оптимизацией (см. "Строка контроля оптимизации" на стр.73)
и директивы OpenMP (см. "OpenMP" на стр. 85), с которыми программист может
передать информацию, необходимую компилятору для параллелизации циклов, без
которой правильная параллелизация невозможна.
В начало страницы
Некоторые циклы не могут быть параллелизованы по причинам, которые будут
рассмотрены позже в этой главе. Иногда перекодирование цикла с вынесением
оператора или нескольких операторов из тела цикла позволяют сделать его
параллелизуемым.
В начало страницы
Компьютерная конфигурация с двумя процессорами, которые работают независимо
и одновременно, необходима для экономии времени за счет мультипроцессинга.
Мултипроцесорная программа может исполняться и на машине с одним единственным
процессором; однако в этом случае истраченное время не будет меньше чем время
исполнения сравнимой программы, написанной без мультипроцессинга.
В начало страницы
При автоматической параллелизации циклы DO и операции с массивами
параллелизуются без участия программиста в модификации программы. Это упрощает
передачу исходных программ на другие обрабатывающие системы, если эти
программы удовлетворяют стандартам Fortran.
LF95 PRO имеет средство, называемое Строка управления оптимизацией (сокращенно
OCL), которое помогает проводить автоматическую параллелизацию.
OCL используются программистом для идентификации конструктов, которые могут
исполняться параллельно (см."Строка управления оптимизацией" на стр. 73, где
есть примеры). Поскольку OCL имеет форму комментариев Fortran, программы с
ними соответствуют стандартам и могут компилироваться компиляторами, не
использующими OCL.
В начало страницы
Имеются четыре таких параметра. Это --parallel, --threads, --threadstack и
--ocl. Они описаны в "Параметры компилятора и компоновщика" на стр. 14.
В начало страницы
В следующих ниже разделах описываются свойств различных переменных окружения,
которые могут влиять на пути параллельного исполнения программ.
В начало страницы
Когда эта переменная установлена, ее значение должно быть меньше или равно
количеству процессоров, функционирующих во время исполнения. (Оно называется
количество активных CPU.)
В начало страницы
Если --threads указан во время компиляции, значение PARALLEL должно быть
равно аргументу параметра --threads и количество активных CPU должно быть
больше или равно аргументу параметра --threads. Если переменная окружения
PARALLEL не установлена, то аргумент параметра --threads должен быть таким же,
как количество активных CPU.
В начало страницы
Когда переменная THREAD_STACK_SIZE установлена, она определяет размер стека в
килобайтах для каждого стека подпроцесса (thread stack). Локальные переменные
в циклах DO и операциях с массивами кладутся на стек. Может оказаться
необходимым увеличить размер стека, если этих переменных много.
По умолчанию размер стека для каждого подпроцесса такой же, как размер
исполнительного. Параметр компилятора --threadstack и переменная окружения
THREAD_STACK_SIZE могут менять размер стека для каждого подпроцесса. Параметр
компилятора --threadstack подавляет переменную окружения THREAD_STACK_SIZE.
Примеры компиляции и исполнения
% lf95 --parallel --ocl test1.f
% a.out
В приведенном примере автоматической параллелизации и оптимизации
управляющие строки (OCL) действуют во время компиляции. Эта программа
выполняется с использованием всех активных CPU в конфигурации.
% lf95 --parallel test2.f
5001-i: "test2.f", line 2: параллелизуется цикл DO с индексом i.
% setenv PARALLEL 2
% a.out
% setenv PARALLEL 4
% a.out
Во втором примере переменная окружения PARALLEL устанавливается на 2 и
программа исполняется с двумя CPU. Затем переменная окружения PARALLEL
устанавливается на 4 и программа исполняется с четырьмя CPU.
В начало страницы
В этом разделе мультипроцессинг описан более подробно.
В начало страницы
Операторами, подвергаемыми автоматической параллелизации, служат циклы DO
(включая вложенные циклы DO) и операторы над массивами (выражения-массивы и
присваивания массивам).
Сечения циклов (Loop Slicing)
Автоматическая параллелизация может разрезать циклы DO на несколько частей.
Требуемое для исполнения время уменьшается за счет исполнения частей DO
параллельно.
do i = 1, 50000
a(i) = b(i) + c(i)
end do
Разные итерации цикла DO могут исполняться на разных CPU одновременно.
CPU 1:
do i1 = 1, 25000
a(i1) = b(i1) + c(i1)
end do
CPU 2:
do i2 = 25001, 50000
a(i2) = b(i2) + c(i2)
end do
В начало страницы
Автоматическая параллелизация применяется также к операторам с массивами
(выражениям с массивами и присваиванием массивам).
integer a(1000), b(1000)
a = a + b
Половина операций делается на одном CPU и половина -- на другом.
CPU 1:
a(1:500) = a(1:500) + b(1:500)
CPU 2:
a(501:1000) = a(501:1000) + b(501:1000)
В начало страницы
LF95 параллелизует цикл DO, если порядок ссылок на данные останется таким же
как при последовательном исполнении. LF95 обеспечивает тот же результат
работы мультипроцессорной программы, что и при последовательном ее исполнении.
Следующий пример содержит цикл DO, к которому нельзя применить разрезание на
части. В этом цикле DO , когда параметр цикла I равен 5001, необходимо
иметь значение элемента массива A(5000).
do i = 2,10000
a(i) = a(i-1) + b(i)
end do
Следующее разрезание нельзя применить к предыдущему коду:
CPU 1:
do i = 2,5000
a(i) = a(i-1) + b(i)
end do
CPU 2:
do i = 5001, 10000
a(i) = a(i-1) + b(i)
end do
A(5000) недоступно для CPU2, и цикл так разрезать нельзя.
В начало страницы
Если разрезается вложенный (nested) цикл DO, LF95 пытается параллелизовать,
если возможно, самый внешний цикл. LF95 выбирает цикл DO, который можно
разрезать и переставляет его с возможно более внешним циклом. Целью этого
является уменьшение накладных расходов мультипроцессинга и увеличение
исполнительной производительности.
Следующая картина показывает пример перестановки во вложенном цикле.
Можно разрезать внутренний цикл с параметром J. Частота мультипроцессорного
управления может быть уменьшена перестановкой его с внешним циклом.
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
После параллелизации получим:
CPU 1:
do j = 1, 5
do i = 2, 10000
a(i,j) = a(i-1,j) + b(i,j)
end do
end do
CPU 2:
do j = 6, 10
do i = 2, 10000
a(i,j) = a(i-1,j) + b(i,j)
end do
end do
В начало страницы
В следующем примере ссылки на массив A не могут быть разрезаны, потому что
порядок ссылок на данные будет отличаться от порядка ссылок при
последовательном исполнении. Массив B можно разрезать, потомку что порядок
ссылки на данные остается тот же, что и при последовательном исполнении. В
этом случае оператор , в котором определяется массив A и оператор, где
определяется массив B , разделены в двух циклах DO, и тот цикл DO, где
определяется массив B, параллелизуется.
do i = 1, 10000
a(i) = a(i-1) + c(i)
b(i) = b(i) + c(i)
end do
С распределением циклов это превращается в:
do i = 1, 10000
a(i) = a(i-1) + c(i)
end do
do i = 1, 10000
b(i) = b(i) + c(i)
end do
Затем второй цикл параллелизуется:
CPU 1:
do i = 1, 5000
b(i) = b(i) + c(i)
end do
CPU 2:
do i = 5001, 10000
b(i) = b(i) + c(i)
end do
В начало страницы
В следующем примере имеются два последовательных цикла DO с одинаковым
управлением. В этом случае дополнительное управление циклом и частоту
управления мультипроцессингом можно сократить за счет слияния двух циклов в
один.
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
После параллелизации это превращается в:
CPU 1:
do i = 1, 5000
a(i) = b(i) + c(i)
d(i) = e(i) + f(i)
end do
CPU 2:
do i = 5001, 10000
a(i) = b(i) + c(i)
d(i) = e(i) + f(i)
end do
В начало страницы
Приведение циклов разрезает циклы DO, изменяя порядок операций (сложения,
умножения и т.д.). Заметим, что приведение циклов может быть причиной
небольших изменений в результатах.
Оптимизация приведением циклов применяется при наличии в цикле одной из
следующих операций:
* SUM: S=S+A(I)
* PRODUCT: P=P*A(I)
* DOT PRODUCT: P=P+A(I)*B(I)
* MIN: X=MIN(X,A(I))
* MAX: Y=MAX(Y,A(I))
* OR: N=N.OR. A(I)
* AND: M=M.AND.A(I)
Следующий пример иллюстрирует приведение циклов и их автоматическое
разрезание.
sum = 0
do i = 1, 10000
sum = sum + a(i)
end do
Параллелизация происходит так:
CPU 1:
sum1 = 0
do i = 1, 5000
sum1 = sum1 + a(i)
end do
CPU 2:
sum2 = 0
do i = 5001, 10000
sum2 = sum2 + a(i)
end do
Частичные суммы складываются:
sum = sum + sum1 + sum2
В начало страницы
Следующие типы циклов не могут подвергаться разрезанию.
1. Циклы, в которых не прогнозируется уменьшение временных затрат.
2. Циклы, содержащие операции типа, не подходящего для разрезания циклов.
3. Циклы, содержащие ссылки на процедуры.
4. Слишком сложные циклы.
5. Циклы, содержащие операторы ввода/вывода.
6. Циклы, в которых порядок ссылки на данные может измениться относительно
порядка при последовательном исполнении.
В начало страницы
Мульти-подпроцессные программы не могут отлаживаться с помощью fdb.
В начало страницы
LF95 допускает употребление строк управления оптимизацией (OCL) для
ориентировки автоматической параллелизации. Такие строки дают эффект, когда
указаны оба параметра --parallel и --ocl.
В начало страницы
Строки OCL имеют несколько функций, зависящих от спецификатора управления
оптимизацией.
В начало страницы
Колонки 1-5 строки управления оптимизацией (OCL) должны быть такими:
"!OCL ". Затем следуют один или более спецификаторов управления оптимизацией:
!OCL i [,i] ....
где каждое i есть один из спецификаторов управления оптимизацией: SERIAL,
PARALLEL, DISJOINT, TEMP, или INDEPENDENT (см. "Спецификатор управления
оптимизацией" на стр. 73).
В начало страницы
Позиция OCL зависит от спецификатора контроля оптимизации.
OCL для автоматической параллелизации может располагаться в тотальной (total)
позиции или в позиции цикла. Названные позиции определяются так:
* Тотальная позиция: начало любой программной единицы.
* Позиция цикла: непосредственно перед циклом DO. Вместе с тем, более одной
OCL можно указывать в позиции цикла и строки комментария можно помещать
между OCL и циклом DO.
!ocl serial <------------------ тотальная позиция
subroutine sub(b, c, n)
integer a(n), b(n), c(n)
do i = 1, n
a(i) = b(i) + c(i)
end do
print*, fun(a)
!ocl parallel <---------------- позиция цикла
do i = 1, n
a(i) = b(i) * c(i)
end do
print*, fun(a)
end
Автоматическая параллелизация и спецификаторы управления оптимизацией
Спецификатор управления оптимизацией становится бесполезным для цикла DO
который не годится (is not a target) для разрезания цикла, даже если указан
спецификатор управления оптимизацией для автоматической параллелизации.
В начало страницы
Для облегчения автоматической параллелизации можно использовать следующие
спецификаторы управления оптимизацией:
* SERIAL
* PARALLEL
* DISJOINT
* TEMP
* INDEPENDENT
SERIAL
Этот спецификатор используется для запрещения разрезания цикла.
Например, если программист знает, что последовательное исполнение цикла DO
происходит быстрее чем параллельное исполнение, возможно, потому, что счетчик
цикла всегда будет небольшим, он может указать спецификатор SERIAL для
этого цикла.
Syntax:
!OCL SERIAL
Спецификатор SERIAL может быть указан в позиции цикла или в тотальной позиции.
Эффект от SERIAL зависит от его позиции.
* В позиции цикла
SERIAL запрещает разрезание цикла DO, соответствующего OCL (и любого
вложенного цикла).
* В тотальной позиции
SERIAL запрещает разрезание всех циклов в программной единице, содержащей OCL.
В следующей программе, если цикл 2 не должен разрезаться, разрезание циклов
можно предотвратить указанием SERIAL. Буквы p слева в исходной программе
отмечают параллелизуемые операторы.
p do j = 1, 10
p do i = 1, l ! <----------- loop 1
p a1(i,j) = a1(i,j) + b1(i,j)
p c1(i,j) = c1(i,j) + d1(i,j)
p e1(i,j) = e1(i,j) + f1(i,j)
p g1(i,j) = g1(i,j) + h1(i,j)
p end do
p end do
p do j=1, 10
p do i=1, m ! <------------ loop 2
p a2(i,j) = a2(i,j) + b2(i,j)
p c2(i,j) = c2(i,j) + d2(i,j)
p e2(i,j) = e2(i,j) + f2(i,j)
p g2(i,j) = g2(i,j) + h2(i,j)
p end do
p end do
p do j=1, 10
p do i=1, n ! <------------ loop 3
p a3(i,j) = a3(i,j) + b3(i,j)
p c3(i,j) = c3(i,j) + d3(i,j)
p e3(i,j) = e3(i,j) + f3(i,j)
p g3(i,j) = g3(i,j) + h3(i,j)
p end do
p end do
p do j = 1, 10
p do i = 1, l ! <------------ loop 1
p a1(i,j) = a1(i,j) + b1(i,j)
p c1(i,j) = c1(i,j) + d1(i,j)
p e1(i,j) = e1(i,j) + f1(i,j)
p g1(i,j) = g1(i,j) + h1(i,j)
p end do
p end do
!ocl serial
do j = 1, 10
do i = 1, m ! <------------ loop 2
a2(i,j) = a2(i,j) + b2(i,j)
c2(i,j) = c2(i,j) + d2(i,j)
e2(i,j) = e2(i,j) + f2(i,j)
g2(i,j) = g2(i,j) + h2(i,j)
end do
end do
p do j = 1, 10
p do i = 1, n <-------------- loop 3
p a3(i,j) = a3(i,j) + b3(i,j)
p c3(i,j) = c3(i,j) + d3(i,j)
p e3(i,j) = e3(i,j) + f3(i,j)
p g3(i,j) = g3(i,j) + h3(i,j)
p end do
p end do
PARALLEL
Спецификатор PARALLEL используется для обращения (reverse) эффекта от SERIAL
и разрешения нарезания циклов.
Syntax: !OCL PARALLEL
Спецификатор PARALLEL может размещаться в позиции цикла или тотальной позиции.
Действие PARALLEL зависит от его позиции.
* Случай позиции цикла
PARALLEL разрешает нарезание циклов для цикла DO, соответствующего OCL (и
любых вложенных в него циклов),
* Случай тотальной позиции
PARALLEL допускает нарезание циклов для всех циклов в программе, содержащей
OCL. В следующем примере, если только loop 2 должен разрезаться, он может
нарезаться при указании PARALLEL вместе с SERIAL, как это показано.
Буква P слева в исходной программе отмечает параллелизуемые операторы.
!ocl serial <------------ total position
...
do j = 1, 10
do i = 1, l ! <----------- loop 1
a1(i,j) = a1(i,j) + b1(i,j)
c1(i,j) = c1(i,j) + d1(i,j)
e1(i,j) = e1(i,j) + f1(i,j)
g1(i,j) = g1(i,j) + h1(i,j)
end do
end do
!ocl parallel
p do j = 1, 10
p do i = 1, m ! <----------- loop 2
p a2(i,j) = a2(i,j) + b2(i,j)
p c2(i,j) = c2(i,j) + d2(i,j)
p e2(i,j) = e2(i,j) + f2(i,j)
p g2(i,j) = g2(i,j) + h2(i,j)
p end do
p end do
do j = 1, 10
do i = 1, n ! <----------- loop 3
a3(i,j) = a3(i,j) + b3(i,j)
c3(i,j) = c3(i,j) + d3(i,j)
e3(i,j) = e3(i,j) + f3(i,j)
g3(i,j) = g3(i,j) + h3(i,j)
end do
end do
DISJOINT
Спецификатор DISJOINT указывает, что порядок ссылок на данные (ссылок на
массивы в цикле DO) остается неизменным при выполнении последовательно и
параллельно. В результате можно нарезать цикл DO, который в противном случае
не нарезался бы, так как компилятор был бы не в состоянии определить
порядок ссылок на данные.
Syntax: !OCL DISJOINT [ (a [,a]...) ]
Здесь "a" есть имя массива, для которого нарезание цикла возможно.
Символ обобщения (wild-card) можно использовать в "a". Если имя массива
опущено, DISJOINT действует только в области цикла DO. См. синтаксис
Wild Card в "Спецификация Wild Card" на стр. 81.
Спецификатор DISJOINT может размещаться в позиции цикла или в тотальной
позиции. Действие DISJOINT зависит от его позиции.
* В позиции цикла
DISJOINT поощряет нарезание цикла DO, соответствующего OCL (и всех вложенных
циклов).
* В тотальной позиции
DISJOINT поощряет нарезание циклов для всех циклов в программной единице.
Рассмотрим следующий код:
do j = 1, 1000
do i = 1, 1000
a(i,l(j)) = a(i,l(j)) + b(i,j)
end do
end do
Так как индексное выражение массива A есть элемент другого массива L(J), то
система не может определить, возникнут ли конфликты при разрезке A.
Поэтому система не будет нарезать внешний цикл DO. Если программист знает,
что при нарезке A конфликтов не будет, внешний цикл DO будет нарезан, если
DISJOINT будет использован, как показано в примере ниже. Буква P показывает
в левой части исходного кода параллелезуемые операторы.
!ocl disjoint(a)
p do j = 1, 1000
p do i = 1, 1000
p a(i,l(j)) = a(i,l(j)) + b(i,j)
p end do
p end do
Замечание:
Если массив, который не может нарезаться, отмечен по ошибке спецификатором
DISJOINT, LF95 может проделать неправильное разрезание цикла и результат
программы может оказаться неверным.
TEMP
Указатель TEMP используется для извещения системы, что перечисленные
переменные используют временно в цикле DO. В результате исполнительная
производительность параллелизованного цикла DO может быть улучшена.
Синтаксис:
!OCL TEMP [ (s [,s]...) ]
Здесь "s" есть имя переменной, используемой временно в цикле DO. Указание
символа обобщения (wild card) возможно в "s". Если имя переменной опущено,
TEMP становится эффективным для всех скалярных переменных в области цикла DO.
См. "Спецификация символа обобщения" на стр. 81 по поводу синтаксиса символа
обобщения.
Спецификатор TEMP может размещаться в позиции цикла или в тотальной позиции.
Действие TEMP зависит от его позиции.
* В позиции цикла.
TEMP указывает, что переменные в цикле DO, соответствующего OCL, есть
временные переменные.
* В тотальной позиции
TEMP указывает, что переменные всех циклов в программной единице, содержащей
OCL, есть временные переменные.
В примере, приведенном ниже, вследствие того, что T есть переменная common,
LF95 должен предполагать, что переменная T используется в подрутине SUB даже
если T используется только в цикле DO. LF95 добавляет код для гарантии, что
T имеет правильное значение в конце параллелизации цикла DO. Буква P слева
в исходной программе отмечает параллелизуемые операторы.
common t
.
.
.
p do j = 1, 50
p do i = 1, 1000
p t = a(i,j) + b(i,j)
p c(i,j) = t + d(i,j)
p end do
p end do
.
.
.
call sub
Если программист знает, что значение T в конце цикла DO не нужно в подрутине
SUB, программист может поставить спецификатор TEMP с T, как это показано в
следующем коде. В результате улучшается исполнительная производительность
так как инструкция, которая поправляет значение T, становится лишней в конце
цикла DO
common t
.
.
.
!ocl temp(t)
p do j = 1, 50
p do i = 1, 1000
p t = a(i,j) + b(i,j)
p c(i,j) = t + d(i,j)
p end do
p end do
.
.
.
call sub
Замечание:
Если переменная, которая не используется как временная, описана в
спецификаторе TEMP по ошибке, LF95 может сделать некорректную нарезку цикла и
результат программы может быть неверным.
INDEPENDENT
Спецификатор INDEPENDENT используется для указания компилятору LF95, что
параллельное исполнение совпадает с последовательным даже если процедура
вызывается из цикла DO. Поэтому цикл DO, содержащий процедуру, пригоден
для нарезания.
Синтаксис:
!OCL INDEPENDENT [ (e [,e]...) ]
Здесь "e" есть имя процедуры, которая не препятствует разрезанию цикла.
В "e" можно использовать символ обобщения. Если имя процедуры опущено,
INDEPENDENT действует для всех процедур в пределах цикла DO.
См. "Спецификация символа обобщения" на стр. 81.
Заметим, что процедура e должна компилироваться с параметром --parallel.
Спецификатор INDEPENDENT может размещаться в позиции цикла и в тотальной
позиции.
Действие INDEPENDENT зависит от его позиции.
* В позиции цикла INDEPENDENT разрешает нарезание соответствующего цикла DO
(и всех вложенных циклов).
* В тотальной позиции INDEPENDENT разрешает нарезание всех циклов в программе,
содержащей OCL. Рассмотрим следующий код:
do i = 1, 10000
j = i
a(i) = fun(j)
end do
..
end
function fun(j)
fun = sqrt(real(j**2+3*j+6))
end
В приведенной программе, так как процедура "FUN" вызывается в цикле DO,
система не может определить, параллелизуем ли цикл DO. Если программист знает,
что никаких проблем не будет при нарезании цикла, вызывающего процедуру "FUN",
он может сообщить системе о возможности нарезки спецификатором INDEPENDENT,
как это показано в следующем ниже коде. Буква P в левой части исходной
программы отмечает параллелизуемые операторы.
!ocl independent(fun)
p do i = 1,1000
p j = i
p a(i) = fun(j)
p end do
..
end
function fun(j)
fun = sqrt(real(j**2+3*j+6))
end
Замечание:
Если процедура, которая не может быть нарезана, описана спецификатором
INDEPENDENT по ошибке, LF95 может проделать неправильную нарезку цикла, что
приведет к ошибке в результатах.
Спецификация с символам обобщения (Wild Card Specification)
В операнде следующих спецификаторов управления оптимизацией можно указывать
имена переменных или процедур с помощью символа обобщения:
* DISJOINT
* TEMP
* INDEPENDENT
Указание с символом обобщения есть комбинация специальных символов с
алфавитно-цифровыми символами. Эффект будет тот же самый, как и при указании
всех имен процедур и переменных, которые соответствуют выражению с wild card.
Имеются два символа wild card, "*" и "?", и они соответствуют следующим
цепочкам символов.
"*" соответствуют всякой символьной цепочке из одной или более букв
и цифр.
"?" соответствуют любой отдельной букве или цифре. Спецификация с wild card
не может содержать более одного символа wild card
!ocl temp(w*)
В этом примере w* соответствует любой переменной, начинающейся с w и имеющей
длину два или более символов. Например, переменные с именами work1, w2,
и work3 включены в эту спецификацию.
!ocl disjoint(a?)
В этом примере a? соответствует любому двух символьному имени массива,
имеющему на первом месте букву a. Например, имена массивов a1, a2 и aa
включены в эту спецификацию. А имя abc в нее не входит, так как его длина
не равна двум.
!ocl independent(sub?)
В этом примере sub? соответствует имени процедуры с длиной четыре и началом
sub. Например, имена процедур sub1, sub2, sub9 включены в эту спецификацию.
В начало страницы
В этом разделе поясняются некоторые особенности средств параллелизации.
--threads
Когда количество CPU, работающих параллельно, указывается в параметре
компилятора --threads, аргумент параметра --threads должен иметь то же самое
значение, что и переменная окружения PARALLEL. Если переменная окружения
PARALLEL не установлена, значение аргумента параметра --threads должно быть
тем же, что и количество CPU, активных во время исполнения. Приведенный ниже
пример иллюстрирует неправильное применение параметра компилятора --threads,
когда число активных CPU есть четыре. Если для --threads указано неверное
значение, результаты исполнения могут оказаться неверными. В приведенном
примере с ошибкой значение N и значение PARALLEL различны.
% setenv PARALLEL 2
% lf95 --parallel --threads 4 a.f
В следующем примере результаты исполнения могут быть неверными, если число
активных CPU не равно двум.
% lf95 --parallel --threads 2 a.f
В начало страницы
Если имеется параллелизованный цикл DO в процедуре, которая вызывается из
другого параллелизованного цикла DO, возникает гнездо параллелизованных
циклов DO. Программа, которая содержит такие циклы DO, не должна
компилироваться с параметром --threads.
Следующий пример -- такой, в котором параллелизованный цикл DO должен
выполняться последовательно. Если исходная программа, которая содержит такие
циклы DO, компилируется с параметром компилятора --threads, результат может
быть неверным.
file: a.f
!ocl independent(sub)
do i = 1,100 ! <------ выполняется параллельно
j = i
call sub(j)
end do
:
end
subroutine sub(n)
:
do i = 1, 10000 ! < ----- должен исполняться последовательно
a(i) = 1 / b(i)**n
end do
:
end
Результат может быть неправильным, если исходная программа a.f компилируется
следующим образом:
% lf95 --parallel --threads 4 a.f (неверное применение)
Для предотвращения такой ошибки указывайте !OCL SERIAL в процедуре, которая
вызывается из параллелизованного цикла DO:
!ocl serial
subroutine sub(n)
:
do i = 1,10000
a(i) = 1 / b(i)**n
end do
:
end
В начало страницы
Когда --parallel указано как параметр компиляции, результат исполнения может
отличаться от результата последовательного исполнения. Причина в том, что
в результате приведения цикла порядок операций при параллельном исполнении
может отличаться от порядка при серийном исполнении. Следующий пример
иллюстрирует оптимизацию при приведении циклов.
sum = 0
do i = 1, 10000
sum = sum + a(i)
end do
При параллелизации имеем:
CPU 1:
sum1 = 0
do i = 1, 5000
sum1 = sum1 + a(i)
end do
CPU 2:
sum2 = 0
do i = 5001, 10000
sum2 = sum2 + a(i)
end do
Затем частные суммы складываются:
sum = sum + sum1 + sum2
Переменная SUM аккумулирует значения от A(1) до A(10000) в порядке
последовательного исполнения.
При параллельном вычислении SUM1 аккумулирует значения от A(1) до A(5000)
и SUM2 аккумулирует значения от A(5001) до A(10000) в то же самое время.
После этого сумма SUM1 и SUM2 добавляется к SUM. Оптимизация приведения
циклов может внести побочный эффект (в силу округления) в результат
исполнения, потому что порядок сложения элементов массива отличается от
последовательного сложения.
В начало страницы
В следующей программе указывается по ошибке DISJOINT для массива A. Результат
исполнения будет неверным, если массив A разрезается, потому что порядок
ссылок на массив A отличается от порядка при последовательном исполнении.
!ocl disjoint(a)
do i = 2,10000
a(i) = a(i-1) + b(i)
end do
В следующей программе по ошибке указано TEMP для переменной T. Правильное
значение не будет присвоено переменной last, потому что LF95 не гарантирует
правильное значение переменной T в конце цикла DO.
!ocl temp(t)
do i = 1, 1000
t = a(i) + b(i)
c(i) = t + d(i)
end do
last = t
Следующая программа указывает по ошибке INDEPENDENT для процедуры SUB.
Результат выполнения может оказаться неверным, если массив A будет нарезаться,
так как порядок ссылок на данные в массиве A отличен от ссылок при
последовательном исполнении.
common a(1000), b(1000)
!ocl independent(sub)
do i = 2, 1000
a(i) = b(i) + 1.0
call sub(i-1)
end do
...
end
subroutine sub(j)
common a(1000)
a(j) = a(j) + 1.0
end
В начало страницы
Если имеется оператор I/O, то встроенная процедура или ссылка на функцию,
которые не подходят для нарезки цикла в процедуре, вызываемой в
параллелизуемом цикле DO, могут привести к неправильным результатам.
Исполнительная производительность программы с мультипроцессингом может
уменьшиться вследствие избытка параллельных действий. В итоге результат
оператора I/O может отличаться от результата при последовательном исполнении.
Приведем пример, в котором оператор I/O находится в процедуре, которая
вызывается в параллелизованном цикле DO.
file: a.f
!ocl independent(sub)
do i = 1, 100
j = i
call sub(j)
end do
:
end
recursive subroutine sub(n)
:
print*, n
:
end
OpenMP
В этом разделе описывается параллелизация с помощью OpenMP. По поводу
специальной не относящейся к реализации (non-implementation-specific)
информации см. OpenMP Fortran specification, содержащею ее и LF95 в формате
PDF. Подробную информацию об OpenMP см. в http://www.openmp.org/
Предполагается, что читатель имеет общее понятие об OpenMP. Ее использование в
реализации LF95 описано ниже.
В начало страницы
Имеются четыре параметра компилятора для OpenMP-параллелизации. Это --openmp,
--cpus, --threadstack и --threadheap. Эти параметры описаны в "Параметры
компилятора и компоновщика" на стр. 14.
В начало страницы
OpenMP имеет несколько специальных переменных окружения, описанных в
документации OpenMP в http://www.openmp.org. Вместе со своими переменными
окружения эта реализация имеет:
THREAD_STACK_SIZE 16 THREAD_STACK_SIZE 2048
Пользователь может указывать размер стека для каждого подпроцесса (thread),
используя переменную окружения THREAD_STACK_SIZE. Максимальный размер стека
для подпроцесса в Linux есть 2048 Kbytes. Параметр компилятора --threadstack
подавляет эту переменную.
В начало страницы
В этом разделе приведены детали, которые остаются зависимыми от процессора
посредством спецификаций OpenMP вместе с другими спецификациями и
ограничениями.
Вложенность параллельных областей
Вложенность параллельных областей поддерживается.
Свойства устанавливаемых динамических подпроцессов
Такие свойства поддерживаются по умолчанию.
В начало страницы
Если нет указаний о количестве их в служебных рутинах OMP_SET_NUM_THREADS
или в переменных окружения OMP_NUM_THREADS, то количество совпадает с числом
доступных CPU.
В начало страницы
Если предложение SCHEDULE опущено, по умолчанию действует SCHEDULE(STATIC).
В начало страницы
Если опущена переменная окружения OMP_SCHEDULE, то директивы DO или
PARALLEL DO, имеющие по расписанию тип RUNTIME, по умолчанию относятся
к SCHEDULE(STATIC).
В начало страницы
Оператор ASSIGN в пределах блока OpenMP не могут ссылаться на метку
оператора, расположенного вне блока директив OpenMP. Таким образом, на метку
оператора в директивном блоке OpenMP не может ссылаться оператор ASSIGN,
расположенный вне директивного блока OpenMP. Переход внутрь или из области
директивного блока с помощью присвающего
оператора GO TO не поддерживается.
В начало страницы
Следующие встроенные функции и операторы могут указываться в директиве ATOMIC
или предложении REDUCTION.
Встроенные функции : AND, OR
Операторы: .XOR., .EOR.
В начало страницы
В конструкте FORALL директивы OpenMP употреблять нельзя.
THREADPRIVATE
При использовании директивы THREADPRIVATE заданный блок common должен быть
определен одинаково во всех программных единицах. Блок common указывает, что
THREADPRIVATE не может увеличивать свой размер.
IF Clause для директивы PARALLEL
Когда значение IF clause для директивы PARALLEL ложно, директива PARALLEL
игнорируется. Поэтому никакой группы подпроцессов не создается. Однако,
директива PARALLEL продолжает действовать.
Подставляемые (Inline) расширения
Следующие процедуры не расширяются при подстановке:
* Определенные пользователем процедуры, которые содержат OpenMP директивы
Fortran.
* Определенные пользователем процедуры, на которые ссылаются в директивах
OpenMP.
Встроенные процедуры, вызываемые из параллельной области.
Переменная в главной процедуре, на которую ссылается внутренняя процедура,
вызванная в параллельной области, рассматривается как SHARED даже если она
приватизирована в параллельной области.
:i = 1 ! это совместное i
!$omp parallel private(i)
i = 2 ! частное i
print*, i ! частное i
call proc ! частное i
!$omp end parallel
contains
subroutine proc()
: ! совместное i
print*, i ! i is shared
: ! совместное i
end subroutine
:
В начало страницы
Когда переменная DO последовательного цикла DO в параллельной области
помечается как "SHARED", она приватизируется в области действия цикла DO.
!$omp parallel shared(i)
i = 1 ! i совместное
do i = 1, n ! i частное
: ! i частное
end do ! i частное
print*, i ! i совместное
!$omp end parallel
!$omp parallel private(i)
i = 1 ! i частное
do i = 1, n ! i частное
: ! i частное
end do ! i частное
print*, i ! i частное
!$omp end parallel
В начало страницы
Переменная, которая фигурирует в оператор-функции, не может иметь атрибутов
PRIVATE, FIRSTPRIVATE, LASTPRIVATE, REDUCTION или THREADPRIVATE.
В начало страницы
Переменная, объявленная как объект группы namelist, не может иметь атрибуты
PRIVATE, FIRSTPRIVATE, LASTPRIVATE, REDUCTION или THREADPRIVATE.
В начало страницы
Внутренние процедуры есть SCHEDULE(STATIC). Порожденная внутренняя процедура
имеет имя "_n_", где n есть порядковый номер.
В начало страницы
Параметры --openmp и --parallel могут быть указаны одновременно. Параметр
--parallel игнорируется в каждой программной единице, которая содержит
директивы OpenMP.
В начало страницы
Много-подпроцессные программы нельзя отлаживать с помощью fdb.