Руководство [1] описывает эффективные методы программирования с разделяемой памятью (SM) для компьютеров SPP серий (Exemplar). Соответствует Convex Fortran Ver. 9.5, Convex C Ver. 6.5. Первые 4 главы охватывают основные концепции, включая оптимизацию и программирование с минимальными усилиями. Последующие главы рассчитаны на более опытных программистов.
Руководство [2] по сравнению с 10-ым изданием для Convex C серий содержит следующие добавления:
В руководстве [3] по сравнению с 10-ым изданием опции, директивы компилятора, соглашения о вызовах подпрограмм выделены отдельно для SPP и С серий.
Процессоры объединяются в функциональные блоки, каждый из которых содержит 2 процессора, 128--512 Мбайт памяти и управляющие устройства. Функциональные блоки внутри гиперузла связаны между собой, с памятью и с устройствами ввода-вывода через 5 -ти портовый неблокируемый кроссбар (все порты могут обмениваться информацией одновременно). Все процессоры объединяются в 1--8 гиперузлов (hypernodes), каждый из которых содержит 2--8 процессоров. Каждый процессор имеет 1 Мбайт кэш команд и 1 Мбайт кэш данных.
Все гиперузлы связаны между собой, с памятью и устройствами ввода-вывода через систему когерентной тороидальной связи (CTI - Coherent Torroidal Interconnect). CTI включает неблокируемый кроссбар на каждом гиперузле для связи внутри гиперузла и высокоскоростное кольцо (ring), которое связывает гиперузлы между собой. CTI обеспечивает широкополосный, с низкой задержкой, доступ к данным других узлов.
Согласованность данных обеспечивается при помощи системы глобально разделяемой распределенной виртуальной памяти (Globally Shared Distributed Virtual Memory). В такой модели физическая память разделяется между всеми гиперузлами и каждому процессору доступно полное виртуальное адресное пространство.
Администратор системы может разделить процессоры на один или несколько подкомплексов (subcomplexes). Самый большой подкомплекс может включать всю систему, самый маленький --- два процессора. Процесс загружается в систему для выполнения на процессорах некоторого подкомплекса. Общая физическая память одного гиперузла может достигать 2 Гбайт, в максимальной конфигурации системы (8 гиперузлов) 16 Гбайт. Каждый процесс может иметь доступ к 4 Гбайтам виртуального адресного пространства. В случае необходимости программы могут быть написаны таким образом, чтобы иметь доступ к более чем 4 Гбайтам памяти.
Компьютеры CONVEX SPP серий повышают производительность за счет большей скорости скалярных операций, конвейеризации, использования высокоскоростных кэшей команд и данных, широкого набора регистров. Для мультиузловых комплексов может быть реализована двумерная модель параллелизма: нижний уровень --- параллелизация внутри гиперузла, верхний уровень --- внутри подкомплекса. Поскольку число процессоров для SPP серий может быть достаточно большим, доступ к памяти только через шину кроссбар неэффективен. Поэтому используется система связи CTI.
Часть памяти гиперузла используется в качестве сетевого кэша, в котором хранятся копии совместно используемых данных. Строка сетевого кэша занимает 64 байта. Кэши процессоров имеют 32-байтные строки. Каждая строка сетевого кэша содержит две смежные строки процессорного кэша, каждая строка процессорного кэша располагается в одном банке памяти и хранит часто используемые данные независимо от их физического расположения в памяти. Сетевые и процессорные кэши способствуют лучшей распределенности данных.
Память, ``приватная для нити" --- присваивание уникальной виртуальной адресной последовательности нити процесса внутри узла. Каждая нить процесса имеет доступ к собственному 4 Гбайтному адресному пространству. Из них примерно 3.7 Гбайта доступно для программ, данных и стека, остальное используется OC SPP-UX. Размер стека можно настраивать. Процессы не могут получить доступ к виртуальному адресному пространству других процессов. Это виртуальное адресное пространство отображается в физическую память подкомплекса, на котором процесс запускается.
Память, ``приватная для узла" --- отображение виртуальных адресов на физические для всех нитей процесса внутри узла.
Память, ``глобальная для подкомплекса" --- отображение виртуальных адресов на физические для всех нитей процесса, выполняющихся в узлах подкомплекса. Подкомплексы не могут иметь доступ к физической памяти других подкомплексов (также как и процессы к виртуальной памяти других процессов).
Различие между ``близко" и ``далеко" разделяемой памятью следующее. Пусть имеется массив двойной точности длиной 65536 элементов, который описан таким образом, чтобы быть размещенным в far_shared памяти на подкомплексе из 4-х гиперузлов. Предположим, что массив попадет на границу 4-байтной страницы в 1-ом узле. Массив будет занимать 8x65536/4096=128 страниц. Его 1-ая страница будет в 1-ом узле, 2-ая во 2-ом, ... , 5-ая в 1-ом узле, ... , 128-ая в 4-ом узле. Если данный массив будет описан в классе near_shared памяти, все его виртуальные страницы будут отображены в один узел.
По умолчанию, все данные программы располагаются в разделяемой памяти ( близко или далеко разделяемой по выбору компилятора для обеспечения лучшей производительности). Программист может специфицировать распололжение переменных в памяти.
real a(100),b(100),c(100)
common /stuff/ big(10000),little(10)
c$dir thread_private a,b,c
c$dir near_shared /stuff/
Если используются только разделяемые переменные, то процесс имеет 4 Гбайта общего виртуального пространства. Распределение приватных данных может быть использовано для увеличения общего адресного пространства. Например, если каждая нить разделяет 2 Гбайта данных, то процесс имеет: 2 Гбайта (разделяемой памяти) + 2*[число нитей] Гбайт (приватной памяти). Связь приватных данных между нитями процесса можно осуществить путем их копирования в разделяемые переменные.
Если SM-программа компилируется с использованием опции -О3, программный код автоматически генерируется для параллельного выполнения. Программа загружается на множество процессоров как единый процесс с одной нитью на процессор. Нить 0 начинает выполняться, в то время как все другие нити остаются пассивными. Каждая ожидает, когда будет активизирована нитью 0. Основываясь на анализе графа ссылок на элементы массивы, компилятор пытается сгенерировать вычисления для нитей таким образом, чтобы они гарантировали ``близость" данных, т.е. чтобы повторные ссылки к тем же самым данным использовали эти данные из кэшей процессоров или связанного с ним процессорного кэша.
Поскольку разделения памяти между процессами нет, мало резона описывать или распределять переменные с использованием специфических классов памяти.
При выполнении данной опции создается объектный код, полностью использующий скалярные особенности PA-RISC архитектуры процессора.
Базовый блок --- сегмент кода, имеющий одну точку входа и одну выхода.
Производится исключение неиспользуемых операторов присваивания, сокращение лишних операций присваивания и вычисления общих подвыражений за счет использования регистров, замена значений часто используемых функций константами, алгебраические и тригонометрические упрощения.
Компилятор также осуществляет оптимизацию уровня -no.
Глобальное распределение регистров --- хранение совместно используемых скалярных переменных в регистрах (а не в основной памяти).
Производится замена используемых переменных константами, исключение лишних присваиваний, вынос из тела цикла независимых выражений (code motion), замена арифметических операций более быстрыми неарифметическими (strength reduction). Компилятор осуществляет также оптимизацию уровней -O0 , -no ,то есть оптимизацию на уровне базовых блоков и машинных команд.
с использование глобального распределения регистров
do i=1,n
a(i)=x
.
.
enddo
x вычисляется на каждой итерации в цикле. Используя механизм GRA, компилятор генерирует код, эквивалентный следующему псевдокоду:
reg=x ! reg обозначает регистр
do i=1,n
a(i)=reg ! компилятор исключает запись и хранние
. ! х в основной памяти
.
enddo
x=reg
Компилятор автоматически определяет, какие скалярные переменные наиболее подходят для GRA и соответственно распределяет регистры.
Применение GRA может иногда приводить к неправильным результатам.
1. При нарушении стандартных соглашений о передаче параметров подпрограммой.
Используя в качестве фактического параметра i константу, GRA размещает ее в регистре, не зная что это константа, и при переписи обратно из регистра возникает ошибка исполнения (runtime error).
call ass(0,10,a)
.
.
subroutine ass(iv,i,a)
integer a(100,100)
reg=i ! i - помещается в регистр
do j=1,10
if(iv.eq.1) then
.
.
i= ...
.
.
else
. ! нет присваивания i
.
endif
enddo
i=reg ! содержимое регистра помещается
! обратно в i - невозможно !!
! если i константа
return
end
Избежать данной ситуации возможно путем использования опции -nga, запрещающей размещение в регистре переменных, передающихся по ссылке.
2. При использовании разделяемых (shared) переменных в мультинитевом процессе.
Проблема возникает, если параллельные нити процесса обновляют разделяемую переменную, размещенную в регистре. В этом случае каждая параллельная нить записывает значение разделяемой переменной в регистр. Такие параллельные присваивания мультинитевого процесса имеют смысл только если они осуществляются внутри критических или упорядоченных секций программного кода. В этом случае GRA не размещает переменные в регистре. Для исключения данной возможности необходимо применять опцию -ngs, запрещающую размещение в регистре разделяемых переменных мультинитевого процесса. GRA выполняется по умолчанию на уровне -O1 и выше.
Данный уровень оптимизации устанавливается по умолчанию на SPP серии.
do i=1,10000 ! исходный код
a(i)=a(i)*b(i)
enddo
do iout=1,10000,1000 ! трансформированный код
do istrip=iout,iout+999
a(istrip)=a(istrip)+b(istrip)
enddo
enddo
do i=1,n ! исходный код
b(i,1)=0
do j=1,m
a(i)=a(i)+b(i,j)*c(i,j)
enddo
d(i)=e(i)+a(i)
enddo
do i=1,n ! трансформированный код
b(i,1)=0
enddo
do i=1,n
do j=1,m
a(i)=a(i)+b(i,j)*c(i,j)
enddo
enddo
do i=1,n
d(i)=e(i)+a(i)
enddo
Распределение цикла может повысить эффективность программного кода за счет сокращения числа обращений к памяти во время итераций и уменьшения перезаписей кэша (cashe thrashing).
do i=1,n ! исходный код
do j=1,m
a(i,j)=b(i,j)+c(i,j)
enddo
enddo
do j=1,m ! трансформированный код
do i=1,n
a(i,j)=b(i,j)+c(i,j)
enddo
enddo
В исходном коде доступ к массивам a, b, c осуществляется по строкам, что неэффективно для Fortran. Трансформированный код осуществляет доступ по столбцам.
do i=1,n ! исходный код
a(i)=b(i)+c(i)
enddo
do j=1,n
if(a(j).LT.0) a(j)=b(j)*b(j)
enddo
do i=1,n ! после объединения циклов
a(i)=b(i)+c(i)
if(a(i).LT.0) a(i)=b(i)*b(i)
enddo
do i=1,n ! после замены элемента массива
temp1=b(i) ! скалярной переменной
temp2=temp1+c(i)
if(temp2.LT.0) temp2=temp1*temp1
a(i)=temp2
enddo
Раскрутка уменьшает потери времени, связанные с инициализацией и приращением переменной цикла. По умолчанию полная раскрутка осуществляется при числе итераций цикла меньше пяти, в остальных случаях частичная (на уровне -О2 и выше). При числе итераций больше 5 раскрутка осуществляется для самых внутренних циклов. Раскрутка цикла сокращает время доступа к основной памяти и увеличивает количество используемых регистров. Осуществляется по умолчанию и может быть отменена опцией -nur.
do i=1,100 ! исходный код
a(i)=b(i)+c(i)
enddo
do i=1,100,4 ! после частичной раскрутки
a(i)=b(i)+c(i)
a(i+1)=b(i+1)+c(i+1)
a(i+2)=b(i+2)+c(i+2)
a(i+3)=b(i+3)+c(i+3)
enddo
do i=1,n ! исходный код
if(i.GT.0) then
do j=1,i
a(i,j)=0
enddo
endif
enddo
do i=1,n ! после исключения лишних
do j=1,i ! проверок
a(i,j)=0
enddo
enddo
do i=1,100 ! исходный код
if (i.EQ.1) then
a(i)=b(i)
else if (i.EQ.100) then
a(i)=c(i)
else
a(i)=-a(i)
endif
enddo
a(1)=b(1) ! после выноса граничных итераций
do i=2,99
a(i)=-a(i)
enddo
a(100)=c(100)
Использование выноса граничных итераций увеличивает длину программного кода и время компиляции. По умолчанию выполнение peeling ограничено заданным пределом длины кода. Этот предел можно расширить, используя опцию -peel или отменить совсем при помощи -peelall.
Peeling осуществляется по умолчанию и может быть отменен опцией -nopeel или директивой NO_PEEL для отдельных циклов.
do i=1,n ! исходный код
if (foo.EQ.bar) then
a(i)=b(i)
else
a(i)=0
endif
enddo
if (foo.EQ.bar) then ! после выноса условного оператора
do i=1,n
a(i)=b(i)
enddo
else
do i=1,n
a(i)=0
enddo
endif
Вынос условного оператора за тело цикла ускоряет его выполнение, однако может сильно увеличить размер кода программы. По умолчанию увеличение размера кода компилятором ограничено заданным пределом. Данный предел можно увеличить опцией -ptst или отменить полностью опцией -ptstall. Test promotion осуществляется по умолчанию и может быть отменен опцией -noptst.
Увеличивает эффективность цикла эа счет размещения элементов массива в регистрах (а не в основной памяти или кэше).
do i=1,n ! исходный код
do j=1,m
a(i)=a(i)+b(j)
enddo
enddo
do i=1,n ! преобразованный код
REG=a(i) ! a(i) записывается в регистр
do j=1,m
REG=REG+b(j)
enddo
a(i)=REG ! содержимоое регистра
enddo ! записывается в a(i)
Scalar replacement осуществляется по умолчанию и может быть отменена опцией -nsr.
Автоматической локализации данных на уровне -О2 препятствуют:
Параллелизация может быть реализована на уровне циклов, задач и блоков.
Задача (task) --- сегмент кода программы, который может выполняться параллельно с другими задачами. Каждая задача запускается на отдельной (выделенной) нити процесса.
Параллельный блок (parallel region) --- сегмент кода программы, выполняющийся на нескольких процессорах. Параллельный блок запускается на нескольких нитях процесса.
На уровне циклов параллелизация осуществляется автоматически, на уровне задач и параллельных блоков --- при помощи директив и прагм компилятора.
На уровне задач параллелизация осуществляется при помощи следующих директив :
begin_tasks [(атрибуты)] - начало параллельного выполнения задач
next_task - конец задачи и начало следующей
end_tasks - конец параллельного выполнения задач
Список атрибутов может включать: nodes, threads, ordered и др.
Параллелизация на уровне задач относится к функциональному параллелизму. Функциональный параллелизм появляется в алгоритме, когда выполняются различные потоки команд над различными данными. Этот тип параллелизма наиболее часто связывается с MIMD-машинами с распределенной (distributed) памятью, но может эффективно использоваться и для модели SM.
При использовании директив begin_tasks ...end_tasks необходимо соблюдать осторожность с целью предотвращения присваивания разделяемым переменным значений одной задачей, которые будут использоваться затем другой задачей. Если необходимо, следует использовать средства синхронизации и при этом учитывать накладные расходы на ее осуществление.
c$dir begin_tasks ! используется thread - параллелизм
do i=1,n-1 ! по умолчанию
a(i)=a(i+1)+b(i)
enddo
c$dir next_task
call tsub(x,y)
c$dir next_task
c(1:1000:2)=d(1:500)
c$dir end_tasks
c$dir begin_tasks (nodes) ! используется двумерная модель
c$dir loop_parallel (threads) ! параллелизма
do i=1,n
if(b(i).NE.0) then
a(i)=b(i)*c(i)
else
a(i)=c(i)*d(i)
endif
enddo
c$dir next_task
c$dir begin_tasks (threads)
call t1sub()
c$dir next_task
call t2sub()
c$dir next_task
call t3sub()
c$dir end_tasks ! (threads)
c$dir next_task
x(1:1000)=y(1:1000)
c$dir end_tasks ! (nodes)
На уровне параллельных блоков параллелизация осуществляется при помощи следующих директив :
parallel [(атрибуты)] - начало параллельного блока
end_parallel - конец параллельного блока
Список атрибутов может включать : nodes, threads (по умолчанию).
На уровне циклов распараллеливание заключается в разбиении цикла на отдельные части, запускаемые параллельно на нескольких процессорах.
program paraxpl
.
.
do i=1,1024
a(i)=b(i)+c(i)
.
.
enddo
Допустим, что внутри цикла нет участков, препятствующих параллелизации. Данная программа может быть распараллелена на 8 процессоров по 128 итераций на каждый. Первый процессор выполняет цикл для i=1, ..., 128; второй --- для i=129, ..., 256 и так далее. По умолчанию компилятор генерирует код для запуска на имеющемся количестве процессоров, однако динамическая оптимизация (dynamic selection) обеспечивает генерацию параллельного кода только в случае выигрыша в производительности по сравнению с последовательным кодом. Динамическая оптимизация включает анализ времен активизации параллельных нитей, объявления приватных переменных, используемых в цикле, объединения параллельных нитей после завершения их работы. На каждом процессоре запускается отдельная нить процесса, которая идентифицируется операционной системой SPP-UX уникальным ID. Все нити, кроме 0-ой, являются неактивными (idle) . Нить 0 выполняет все последовательные участки кода программы. Остальные нити одновременно активизируются 0-ой нитью при прохождении параллельного участка кода. После прохождения параллельного участка они снова становятся пассивными.
Явно распараллеливаемые при помощи директив или прагм итерации циклов, с зависимостями от данных, присваиваются нитям в карусельном режиме и инициируются в порядке, определяемом циклом так, чтобы можно было использовать соответствующие участки синхронизации.
При использовании директивы prefer_parallel (nodes), компилятор осуществляет анализ возможной параллелизации циклов. Так, например, автоматически параллелизуются двойные циклы во вложенных циклах: самый внешний цикл после перестановки будет распараллелен в рамках модели node-параллелизма, самый внутренний --- в рамках модели thread-параллелизма и будет запускаться на процессорах каждого гиперузла. Автоматическая параллелизация циклов не осуществляется при использовании следующих директив:
loop_parallel (nodes);
parallel (nodes);
begin_tasks (nodes), next_task, end_tasks.
Необходимо явно указать threads-parallel циклы, задачи или блоки, содержащиеся внутри node-parallel участков кода.
Выбор уровня параллелизма влияет только на размещение активизированных нитей в физической памяти и не влияет на их допустимое количество. Если задан node-параллелизм, на каждом гиперузле активизируется одна нить процесса. Совокупность нитей составляет множество активизированных (spawn) нитей, пронумерованных от 0 до [число гиперузлов-1]. Если затем thread-параллелизм осуществляется внутри node-параллелизма, то активизируется новая совокупность нитей, пронумерованных от 0 до [количество нитей гиперузла-1]. Это означает, что активизированные нити дублируются на каждом гиперузле, однако внутри него они уникальны.
program 2dxpl
.
.
c$dir prefer parallel (nodes)
do j=1,1024
c$dir prefer parallel (threads)
do i=1,1024
a(i,j)=b(i,j)+c(i,j)
.
.
enddo
enddo
В данном примере компилятор распараллеливает j-цикл по гиперузлам и i-цикл по нитям (внутри гиперузла). Допустим, что программа запускается на субкомплексе, состоящем из 2-х гиперузлов, каждый из которых содержит по 4 процессора. Тогда цикл по j распараллеливается на 2 нити, по одной на каждом гиперузле. Нить 0 запускается на последовательном участке кода и является первоначально активной, нить 1 активизируется при выполнении на параллельном участке кода :
нить 0 (j=1,512)
нить 1 (j=513,1024)
Цикл по i распараллеливается на каждом гиперузле. На 1-ом гиперузле нить 0 остается активной, нити 1-3 активизируются на параллельном участке :
нить 0 (i=1,256)
нить 1 (i=257,512)
нить 2 (i=513,768)
нить 3 (i=769,1024)
На 2-ом гиперузле нить 1 активизируется как нить 0, нити 1--3 активизируются на параллельном участке кода (внутри цикла по i ). После завершения цикла по i нить 0 на 2-ом гиперузле опять становится нитью 1 (внутри цикла по j ).
Элементные операции присваивания с массивами (FORTRAN 90) преобразуются компилятором в циклы, которые автоматически распараллеливаются.
x(1:M:2,1:N)=y(2:M+1:2,2:N+1)
Данная строка преобразуется следующим образом :
do i=1,N
do j=1,M,2
x(j,i)=y(j+1,i+1)
enddo
enddo
Маскированные операции присваивания массивов с помощью оператора WHERE (FORTRAN 90) также преобразуются компилятором в циклы, которые автоматически распараллеливаются.
real data(1000),limit
logical normal(1000)
.
.
where(data.LE.limit) normal=.TRUE.
.
Данная строка преобразуется следующим образом:
do i=1,1000
if(data(i).LE.limit) normal(i)=.TRUE.
enddo
Простые циклы могут быть распараллелены без предварительных преобразований (опция -О2), однако часто такие преобразования повышают эффективность параллелизации. Например, перестановка циклов способствует более производительному использованию процессорного кэша самым внутренним циклом и параллелизации самого внешнего цикла; расщепление цикла оптимизирует повторное использование кэш-данных (reuse data).
Динамическая оптимизация (dynamic selection) осуществляется по умолчанию при задании -О3. Компилятор генерирует одновременно последовательные и параллельные коды для циклов, подлежащих распараллеливанию, и затем оценивает их эффективность с учетом издержек времени активизации параллельных нитей, объявления приватных переменных, объединения параллельных нитей. В исполняемый модуль записывается наиболее эффективный код. Задание опции -nds отменяет динамическую оптимизацию. В этом случае все допускающие параллелизацию циклы распараллеливаются.
Препятствуют автоматической параллелизации циклов :
do i=1,n-1
a(i)=a(i+1)+b(i)
enddo
.
.
do i=1,n
a(J(i))=b(i)
enddo
do i=1,n
a(i)=b(i)+c(i)
.
.
asum=asum+a(i)
enddo
При распараллеливании цикла для каждой нити создается временная копия переменной asum. После завершения параллельного цикла каждая нить обновляет значение глобальной переменной asum. Большинство конструкций, препятствующих локализации данных, препятствуют и их параллелизации (операторы ввода-вывола, вызовы подпрограмм в теле цикла, альясные скалярные переменные или массивы).
На Фортране можно повысить производительность, описывая первую размерность массивов (соответственно последнюю на С) так, чтобы занять целое число строк сетевого кэша (64 байта). Для этого необходимо изменить первую размерность так, чтобы она стала кратной 64. Например массивы integer*4, real*4 должны иметь первую размерность кратной 16, real*8, complex кратной 8 . После изменения размерности массивов необходимо упорядочить COMMON блоки так, чтобы массивы появлялись раньше скаляров. Требуется также упорядочить скаляры от наибольшего к наименьшему (по формату данных), чтобы они были выравнены до границы наиболее эффективно. Это рекомендуется делать везде, где возможно, и описывать все переменные независимо от того, входят ли они в COMMON блоки или нет.
Оптимизация для SPP серии может вызвать падение эффективности для С серии. Следует использовать препроцессор с опцией -fpp для выбора нужных машинно-зависимых размерностей.
p - препроцесор;
c - компилятор;
o - компилятор;
a - ассемблер;
l - загрузчик;
Пример: -Wl, -a, archive предписывает загрузчику компоновать с архивными библиотеками.
C$DIR [SPP | CSERIES] directive [, directive ... ]
Строка директивы начинается с 1-ой позиции. Если задан один из атрибутов, определяющих тип рабочей машины (SPP или CSERIES), строка с директивой будет применяться только при компиляции для заданного типа рабочей машины. Если заданы две или более директивы, они разделяются запятыми. Директива должна помещаться на одной строке, она не может быть продолжена.
C$DIR BARRIER(barr-name [,barr-name ...])
где barr-name --- имя объявляемой переменной типа BARRIER; Применение директивы полезно только на уровне -O3.
C$DIR BEGIN_TASKS [ (attribute-list) ]
где возможный параметр attribute-list (т.е. список атрибутов) классифицирует способ, которым задачи запускаются параллельно. Только один атрибут ORDERED доступен на машинах С-серии. На машинах SPP-серии attribute-list может быть одной из следующих комбинаций атрибутов:
- ORDERED
- NODES
- THREADS
- MAX_THREADS=m
- ORDERED, NODES
- ORDERED, THREADS
- ORDERED, MAX_THREADS=m
- NODES, MAX_THREADS=m
- THREADS, MAX_THREADS=m
- ORDERED, NODES, MAX_THREADS=m
- ORDERED, THREADS, MAX_THREADS=m
C$DIR BLOCK_LOOP [ (BLOCK_FACTOR = n) ]
где n - заданный коэффициент для разбиения цикла. Если n не задан, компилятор выбирает соответствующий коэффициент. BLOCK_LOOP эффективна только на уровнях -O2 или -O3.
Блочно-разделяемые массивы распределяются поровну среди всех гиперузлов, на которых выполняется программа; для этого страницы массива распределяются блоками одинакового размера по всем гиперузлам комплекса.
Директива имеет синтаксис:
C$DIR BLOCK_SHARED(alloc-arr [, alloc-arr ...])
где alloc-arr --- размещаемый массив, описываемый до этой директивы в операторе ALLOCATABLE.
C$DIR CRITICAL_SECTION [ (gate-var) ]
где возможный параметр gate-var --- это ранее объявленная GATE-переменная, которая будет использоваться для контроля выполнения помеченного сегмента кода.
C$DIR END_CRITICAL_SECTION
C$DIR FAR_SHARED (namelist)
где namelist --- список имен COMMON-блоков, массивов и скалярных переменных.
Директива использует следующий формат:
C$DIR FAR_SHARED_POINTER (alloc-var)
где alloc-var --- имя переменной, описанной ранее в операторе ALLOCATABLE. Дополнительные директивы THREAD_PRIVATE_POINTER, NODE_PRIVATE_POINTER и \break NEAR_SHARED_POINTER помещают указатели в другие классы памяти.
C$DIR GATE(gate-name [, gate-name ...])
где gate-name --- это имя объявляемой переменной типа GATE. Использование GATE полезно только на уровне -O3.
Используя GATE переменные и вспомогательные встроенные функции можно ограничить выполнение блока программы одной нитью процесса. Более подробная информация в [2].
Директива LOOP_PARALLEL отличается от директивы PREFER_PARALLEL в том, что распараллеливает выполнение цикла, следующего за директивой, невзирая на имеющиеся в цикле зависимости.
Формат директивы LOOP_PARALLEL такой:
C$DIR LOOP_PARALLEL [ (attribute-list) ]
где опция attribute-list может содержать любую из следующих комбинаций атрибутов:
Включение директивы LOOP_PARALLEL предписывает компилятору игнорировать любые зависимости между итерациями. При использовании данной директивы полученные результаты могут быть некорректными. Необходимо сравнивать их с результатами, полученными без параллелизации.
C$DIR LOOP_PRIVATE(varlist)
где varlist --- список скалярных переменных или массивов, разделенных запятыми, которые должны использоваться только для следующего за директивой цикла.
C$DIR NEAR_SHARED (namelist)
где namelist --- список имен COMMON-блоков, массивов и скалярных переменных, которые должны храниться в near-shared памяти.
C$DIR NEAR_SHARED_POINTER (alloc-var)
где alloc-var --- имя переменной, описанной ранее в операторе ALLOCATABLE.
C$DIR NODE_PRIVATE (namelist)
где namelist --- список имен COMMON-блоков, массивов и скалярных переменных.
C$DIR NODE_PRIVATE_POINTER (alloc-var)
где alloc-var --- имя переменной, описанной ранее в операторе ALLOCATABLE.
C$DIR NO_LOOP_DEPENDENCE (namelist)
где namelist --- список массивов, потенциальные LCD-зависимости по которым будут игнорироваться компилятором. Если namelist не задан, то компилятор предполагает, что не существует никаких LCD-зависимостей в любом из массивов цикла.
Используйте эту директиву только для массивов. Используйте LOOP_PRIVATE директиву для задания скалярных переменных, свободных от зависимостей.
Директива отменяет задаваемый предел увеличения длины кода на всех уровнях: установленный по умолчанию, заданный при помощи -peel или -peelall опций.
Использование директивы эффективно только на уровне -О2 и выше.
Использование директивы эффективно только на уровне -О2 и выше.
Формат директивы следующий :
C$DIR NO_SIDE_EFFECTS ( func [, func ...] )
где func --- имена определенных пользователем функций. Хотя директива может находиться в любом месте программы, эффективней ее размещать перед вызовом указанных функций. В случае отсутствия аргументов директива распространяется на все вызываемые после нее в программе функции.
Использование директивы эффективно на всех уровнях оптимизации, за исключением -no.
Данная директива позволяет убрать вызовы функций, не имеющих побочных эффектов, из выражений, присваиваемых скалярной переменной, нигде не используемой. Чаще такая возможность появляется после проведенных преобразований оптимизации и реже в исходном коде. Рекомендуется использование директивы после следующего сообщения компилятора:
More optimization is possible if this function call has no
side effects.
Вызываемые в цикле функции без побочных эффектов инвариантны в следующих аспектах:
Использование директивы эффективно только на уровне -О2 и выше.
Формат директивы следующий :
C$DIR ORDERED_SECTION (gate)
где gate задает ранее объявленную переменную GATE, которая будет использоваться для управления входом в упорядоченную (ordered) секцию. Более подробная информацию см. в [2].
Директива PREFER_PARALLEL отличается от директивы LOOP_PARALLEL и директивы FORCE_PARALLEL тем, что не распараллеливает цикла, следующего за директивой, в случае существования в нем зависимостей.
Использование директивы эффективно только на уровне -О3. Директива имеет следующий формат :
C$DIR PREFER_PARALLEL [(attribute-list)]
где возможный параметр attribute-list определяет способ, которым цикл, следующий за директивой, распараллеливается. На машинах C-серии доступны только атрибуты CHUNK_SIZE и ORDERED.
На машинах SPP-серии attribute-list может содержать одну из следующих комбинаций атрибутов:
C$DIR PREFER_PARALLEL ( CHUNK_SIZE = 8 )
do i=1,750
a(i)=c(i)*sin(b(i))
enddo
Распределяет цикл среди всех нитей порциями по 8 итераций.
Использование директивы эффективно только на уровне -О2 и выше.
Использование директивы эффективно на всех уровнях оптимизации, включая -no.
Формат директивы следующий:
C$DIR ROW_WISE (array_name [, array_name ...])
dimension a(4,1000)
do i=1,4
do j=1,1000 ! доступ к массиву замедляется
a(i,j)=0 ! из-за разрывного размещения
enddo ! элементов массива в памяти
enddo ! ( noncontiguous memory )
C$DIR ROW_WISE (a)
dimension a(4,1000)
do i=1,4 ! доступ к массиву ускоряется
do j=1,1000 ! за счет смежного размещения
a(i,j)=0 ! элементов массива в памяти
enddo ! ( contiguous memory )
enddo
Данная директива может применяться только к безусловно присваиваемым на последней итерации цикла переменным. Переменные, значения которых присваиваются при помощи условного оператора, не должны быть private, они должны присваиваться внутри упорядоченных (ordered) секций.
Использование директивы эффективно на всех уровнях оптимизации.
Использование директивы полезно когда число итераций цикла слишком мало для выигрыша при векторизации или параллелизации, когда вычисляемые результаты должны быть такими же как и для скалярного варианта, а также для предотвращения перестановки цикла (loop interchange).
do 10 i=1,n ! ( n = 2 )
do 10 j=1,m ! ( m = 1000 )
10 a(i,j) = b(i,j) + c(i,j)
В данном примере компилятор осуществит перестановку местами i и j цикла для смежного размещения в памяти элементов массивов a, b, c.
C$DIR SCALAR
do 10 i=1,n ! ( n = 2 )
do 10 j=1,m ! ( m = 1000 )
10 a(i,j) = b(i,j) + c(i,j)
Применение директивы SCALAR обеспечит, чтобы цикл с наибольшим числом итераций оставался самым внутренним.
C$DIR SCALAR
do 10 i=1,n ! ( n = 2 )
C$DIR SCALAR
do 10 j=1,m ! ( m = 2 )
10 a(i,j) = b(i,j) + c(i,j)
В данном примере предотвращается векторизация и параллелизация обоих циклов, т.к. число итераций мало.
Действие task_private переменных ограничено блоком от BEGIN_TASKS до \break END_TASKS.
Как TASK_PRIVATE могут быть описаны только статические переменные и массивы. Динамические, размещаемые и автоматические массивы не допускаются.
Использование директивы эффективно только на уровне -О3.
Директива TASK_PRIVATE имеет формат:
C$DIR TASK_PRIVATE (varlist)
где varlist --- список переменных или массивов, разделенных запятыми, которые должны быть приватными для каждой задачи, следующей за этой директивой.
C$DIR THREAD_PRIVATE (namelist)
где namelist --- список имен COMMON-блоков, массивов и скалярных переменных.
Директива имеет следующий формат :
C$DIR THREAD_PRIVATE_POINTER (alloc-var)
где alloc-var --- имя переменной, описанной ранее в операторе ALLOCATABLE.
Директива имеет следующий формат:
C$DIR UNROLL [ (UNROLL_FACTOR=n) ]
где необязательный параметр UNROLL_FACTOR определяет число повторений тела цикла n.
Использование директивы эффективно только на уровне -О2 и выше.
Директива имеет следующий формат:
C$DIR UNROLL_AND_JAM [ (UNROLL_FACTOR=n) ]
где необязательный параметр UNROLL_FACTOR определяет число повторений тела цикла n.
Использование директивы эффективно только на уровне -О2 и выше.
1. Exemplar Programing Guide, Third Edition, June 1995
2. Fortran Language Reference Eleventh Edition, October 1994
3. Fortran User's Guide, Eleventh Edition, October 1994
4. Convex MPICH User's Guide for Exemplar Systems.