Современный Фортран представляет собой специализированный язык программирования, предназначенный в основном для написания вычислительных программ для векторно-конвейерных и параллельных архитектур. Эволюция стандартов языка Фортран была рассмотрена в предыдущих статьях – здесь и здесь.
На данный момент действующим стандартом языка Фортран является стандарт ISO 2018 года «Fortran 2018», готовится к принятию стандарт 2023 года. К сожалению, различные компиляторы Фортрана поддерживают требования стандартов в различной степени.
В этой статье мы попробуем написать простейшую параллелизуемую программу на языке Фортран, используя для этого методы конвейеризации и симметричной параллелизации и сравним их между собой, применив наиболее популярные компиляторы GNU Fortran и Intel Fortran.
В целом, компилятор Intel Fortran гораздо более полно реализует стандарт Fortran 2018. В частности, он поддерживает все имеющиеся в стандарте средства параллельных вычислений, в то время как GNU Fortran реализует только самые базовые из них (чего, впрочем, в ряде случаев более чем достаточно). С другой стороны, Intel Fortran, в отличие от GNU Fortran, не обеспечивает реализацию символьного типа CHARACTER (KIND=4) с поддержкой кодировки UCS-4, что может затруднить обработку не-ASCII текстов. Бытует мнение, что Intel Fortran обладает более мощным оптимизатором.
Установка компилятора Fortran в разных ос
Постановка задачи
Напишем простейшую программу для реализации классического клеточного автомата игры «Жизнь». Не будем сейчас париться с вводом и выводом, исходную конфигурацию зададим в самой программе, а результирующую конфигурацию после заданного числа шагов выведем в файл. Нас будут интересовать сами вычислительные шаги клеточного автомата. Эта задача хороша для нас тем, что она позволяет небольшими усилиями достичь любого наперёд заданного объёма чистых (pure) вычислений с массивами произвольных размеров, не вырождаясь в заведомо излишний код, который оптимизатор мог бы выкинуть, обманув наши метрики производительности.
0. Последовательная программа
Для начала напишем программу в чисто последовательном стиле. Напишем всё в одном файле, чтобы оптимизатору было легче работать.
program life ! чисто последовательный вариант программы implicit none ! здесь мы задаём количество байтов ! для каждой ячейки — вдруг операции над ! целыми словами окажутся эффективными? (нет) integer, parameter :: matrix_kind = 1 integer, parameter :: generations = 2 ! автомат рассматривает 2 поколения integer, parameter :: rows = 1000, cols = 1000 ! размеры поля integer, parameter :: steps = 10000 ! количество шагов ! описываем игровое поле. значения элементов могут быть целыми 0 или 1 integer (kind=matrix_kind) :: field (0:rows+1, 0:cols+1, generations) integer :: thisstep = 1, nextstep =2 ! индексы массива для шагов ! при желании это можно легко обобщить на автомат с памятью больше 1 шага integer :: i ! счётчик шагов integer :: clock_cnt1, clock_cnt2, clock_rate ! для работы с таймером ! инициализируем поле на шаге thisstep начальной конфигурацией call init_matrix (field (:, :, thisstep)) ! засечём время call system_clock (count=clock_cnt1) ! вызовем процедуру выполнения шага в цикле для заданного числа шагов do i = 1, steps ! тут мы берём сечение массива по thisstep и преобразовываем в nextstep call process_step (field (:, :, thisstep), field (:, :, nextstep)) ! следующий шаг становится текущим thisstep = nextstep ! а для следующего шага снова возвращаемся к другому сечению nextstep = 3 — thisstep end do ! узнаем новое значение таймера и его частоту call system_clock (count=clock_cnt2, count_rate=clock_rate) ! напечатаем затраченное время и оценку производительности print *, (clock_cnt2-clock_cnt1)/clock_rate, ‘сек, ‘, мигалку» в чистое поле pure subroutine init_matrix (m) integer (kind=matrix_kind), intent (out) :: m (0:,0:) m = 0 m (50, 50) = 1 m (50, 51) = 1 m (50, 52) = 1 end subroutine init_matrix ! выведем матрицу в файл при помощи пробелов, звёздочек и грязного хака subroutine output_matrix (m) integer (kind=matrix_kind), intent (in) :: m (0:,0:) integer :: rows, cols integer :: i, j integer :: outfile rows = size (m, dim=1) — 2 cols = size (m, dim=2) — 2 open (file = ‘life.txt’, newunit=outfile) do i = 1, rows ! выводим в каждой позиции строки символ, код которого является ! суммой кода пробела и значения ячейки (0 или 1), умноженного ! на разность между звёздочкой и пробелом write (outfile, ‘(*(A1))’) (char (ichar (‘ ‘) + m1 (i, j-1) + m1 (i-1, j+1) + m1 (i, j+1) + m1 (i+1, j+1) ! присваиваем значение выходной клетке select case (s) case (3) m2 (i, j) = 1 case (2) m2 (i, j) = m1 (i, j) case default m2 (i, j) = 0 end select end do end do ! закольцуем игровое поле, используя гало в массиве, ! дублирующее крайние элементы с другой стороны массива m2 (0,:) = m2 (rows, 🙂 m2 (rows+1, 🙂 = m2 (1, 🙂 m2 (:, 0) = m2 (:, cols) m2 (:, cols+1) = m2 (:, 1) end subroutine process_step end program life
Откомпилируем нашу программу при помощи GNU Fortran и Intel Fortran:
Циклы в Fortran
$ gfortran life_seq.f90 -o life_seq_g -O3 -ftree-vectorize -fopt-info-vec -flto
$ ifort life_seq.f90 -o life_seq -Ofast
$ ./life_seq_g
11 сек, 125172000 ячеек/с
$ ./life_seq
14 сек, 94120000 ячеек/с
125 лямов в секунду у GNU Fortran против 94 лямов у Intel Fortran.
$ gfortran life_seq.f90 -o life_seq_g -O3 -ftree-vectorize -fopt-info-vec -flto -floop-parallelize-all -fopenmp
$ ifort life_seq.f90 ‑o life_seq ‑Ofast ‑parallel
11 сек, 124773000 ячеек/с
4 сек, 340690000 ячеек/с
Intel Fortran очень серьёзно прибавил в производительности, в три с половиной раза. GNU Fortran добавил самую малость. Это единственный из наших тестов, где ifort показал преимущество перед gfortran, причём весьма заметное.
Давайте, может, попробуем 32-разрядные целые вместо байтов (с автопараллелизатором)?
integer, parameter :: matrix_kind = 4
$ ./life_seq_g
10 сек, 131818000 ячеек/с
$ ./life_seq
6 сек, 212080000 ячеек/с
Как видим, ничего хорошего нам это не дало.
1. Матричная программа
Некоторые люди думают, что, если заменить циклы неявными вычислениями с матрицами, то это невероятно оптимизирует код. Посмотрим, так ли это. Поменяем нашу любимую подпрограмму process_step:
! обработка шага операциями с матрицами pure subroutine process_step (m1, m2) integer (kind=matrix_kind), intent (in) :: m1 (0:,0:) integer (kind=matrix_kind), intent (out) :: m2 (0:,0:) integer :: rows, cols integer s (0:size(m1,dim=1)-1, 0:size (m1,dim=2)) rows = size (m1, dim=1) — 2 cols = size (m1, dim=2) — 2 ! вычислим матрицу s, которая повторяет по форме и размерам матрицу m1 ! и содержит в каждом элементе количество живых соседей клетки s = m1(0:rows-1,:) + m1(2:rows+1,:) + m1(0:rows-1,0:cols-1) + m1(:,2:cols+1) + m1(2:rows+1,2:cols+1) ! завернём края ещё до вычислений s (0,:) = s (rows, 🙂 s (rows+1, 🙂 = s (1, 🙂 s (:, 0) = s (:, cols) s (:, cols+1) = s (:, 1) ! и применим оператор матричной обработки where where (s==3 .or. s==2 .and. m1 == 1) m2 = 1 elsewhere m2 = 0 end where end subroutine process_step
Вернёмся к matrix_kind = 1 и проверим мощь матричных операторов (с автопараллелизатором):
$ ./life_mat_g
12 сек, 115730000 ячеек/с
$ ./life_mat
7 сек, 184630000 ячеек/с
Как видим, результат чуть-чуть хуже чисто последовательного алгоритма. Причём если выключить автопараллелизатор, то Intel Fortran почему-то сильно расстраивается:
25 сек, 55580000 ячеек/с
При этом надо ещё отметить, что Intel Fortran по умолчанию размещает очень мало памяти для стека, и увеличение размеров игрового поля (а вместе с ним и размещаемой на стеке переменной s в матричном варианте) приводит к выпадению программы в кору. GNU Fortran свободно работает при настройках по умолчанию с огромным размером поля.
С другой стороны, складывается впечатление, что здесь можно серьёзно соптимизировать матричный алгоритм, чтобы не перебирать одни и те же элементы массива трижды при движении по матрице. Возможно, кто-то из читателей предложит своё решение.
2. SMP параллелизм через OpenMP
Обе предыдущие программы были чисто последовательными, хотя компиляторы немножко векторизовали операции. Это неинтересно. Давайте извлечём пользу из наличия нескольких ядер в процессоре, причём сделаем это самым простым и грубым способом – через OpenMP:
! обратите внимание, что подпрограмма, управляющая внутри себя ! параллелизмом с помощью директив omp, не может быть объявлена чистой, ! так как это очевидный побочный эффект. декларация pure привела бы ! к ошибке компиляции impure subroutine process_step (m1, m2) integer (kind=matrix_kind), intent (in) :: m1 (0:,0:) integer (kind=matrix_kind), intent (out) :: m2 (0:,0:) integer :: rows, cols integer :: i, j, s rows = size (m1, dim=1) — 2 cols = size (m1, dim=2) — 2 ! внешний цикл исполняется параллельно на ядрах SMP. ! переменные i и s свои в каждой параллельной ветке кода !$omp parallel do private (i, s) do j = 1, cols do i = 1, rows s = m1 (i-1, j) + m1 (i+1, j) + m1 (i-1, j-1) + m1 (i+1, j-1) + m1 (i, j-1) + m1 (i-1, j+1) + m1 (i, j+1)+ m1 (i+1, j+1) select case (s) case (3) m2 (i, j) = 1 case (2) m2 (i, j) = m1 (i, j) case default m2 (i, j) = 0 end select end do end do m2 (0,:) = m2 (rows, 🙂 m2 (rows+1, 🙂 = m2 (1, 🙂 m2 (:, 0) = m2 (:, cols) m2 (:, cols+1) = m2 (:, 1) end subroutine process_step
Здесь нас ждёт некоторое разочарование, потому что конструкция DO CONCURRENT в GNU Fortran реализована мало и плохо. Предложение LOCAL не может быть оттранслировано этим компилятором. И даже если бы мы как-то вывернулись из этого положения, то GNU Fortran всё равно преобразует DO CONCURRENT в обычный последовательный цикл DO (в интернете встречаются утверждения, что иногда GNU Fortran способен распараллелить DO CONCURRENT , но автору не удалось достичь такого эффекта).
Поэтому трансляцию этого примера мы можем выполнить только в Intel Fortran (обратите внимание, что компилятору всё равно нужна многонитевая библиотека OpenMP для параллелизации, без неё цикл будет откомпилирован в последовательный код):
$ ifort life_con2.f90 -o life_con -Ofast -qopenmp
$ ./life_con
3 сек, 355890000 ячеек/с
Этот результат лучше всего, что мы видели в Intel Fortran, хотя немного не дотягивает до результата GNU Fortran с OpenMP.
4. Больше SMP параллелизма
Синтаксис оператора DO CONCURRENT как бы намекает, что мы можем объединить внутренний и внешний циклы в один параллельный цикл по двум параметрам. Посмотрим, что это даст:
! подпрограмма снова может быть чистой, так как она не управляет нитками ! объединяем циклы do в общий do concurrent pure subroutine process_step (m1, m2) integer (kind=matrix_kind), intent (in) :: m1 (0:,0:) integer (kind=matrix_kind), intent (out) :: m2 (0:,0:) integer :: rows, cols integer :: i, j, s rows = size (m1, dim=1) — 2 cols = size (m1, dim=2) — 2 ! так выглядит параллельный цикл в стандарте Фортрана ! здесь распараллелен как внешний, так и внутренний цикл ! в единую параллельную конструкцию, параметризованную по j и i do concurrent (j = 1:cols, i = 1:rows) local (s) s = m1 (i-1, j) + m1 (i+1, j) + m1 (i-1, j-1) + m1 (i+1, j-1) + https://habr.com/ru/articles/726654/» target=»_blank»]habr.com[/mask_link]
Hello world#
В этой части руководства мы напишем нашу первую программу на Fortran: повсеместно распространённый пример первой программы «Hello, World!».
Однако, прежде чем мы сможем написать нашу программу, нам необходимо убедиться, что у нас установлен компилятор Fortran.
Фортран является компилируемым языком, что означает, что после написания текста исходного кода программы, он должен быть передан компилятору для создания исполняемого файла программы, который можно будет запустить.
Установка компилятора#
В этом руководстве мы будем работать со свободным и открытым компилятором GNU Fortran compiler (gfortran), который является частью GNU Compiler Collection (GCC).
Чтобы установить компилятор gfortran в операционной системе Linux, используйте пакетный менеджер вашей системы. На macOS, вы можете установить gfortran используя Homebrew или MacPorts. Для Windows, вы можете получить установочные файлы здесь.
Чтобы проверить, что компилятор gfortran установлен провильно, откройте терминал и выполните команду:
$> gfortran —version
вывод на экран в результате её выполнения должен быть похож на следующий:
GNU Fortran 7.5.0 Copyright (C) 2017 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Hello world#
После установки компилятора, создайте новый файл в вашем любимом редакторе кода и наберите в нём следующий пример:
program hello ! This is a comment line; it is ignored by the compiler print *, ‘Hello, World!’ end program hello
Сохраните файл вашей программы в файл hello.f90 и соберите её, набрав и выполнив в командной строке:
$> gfortran hello.f90 -o hello
.f90 – стандартное расширение файла для исходного текста современного Fortran. Число 90 является отсылкой к первому современному стандарту Fortran 1990 года.
Для запуска вашей скомпилированной программы выполните команду:
$> ./hello Hello, World!
Поздравляем, вы написали, скомпилировали и запустили свою первую программу на Fortran! В следующей части этого руководства мы расскажем о переменных для хранения данных.
so the DOM is not blocked —>
Источник: fortran-lang.org
Программа на фортране пример
Сегодня, как и обещал вам в первом выпуске, я расскажу о Фортране.
Как вы знаете, это первый «настоящий» язык программирования. Название Fortran является аббревиатурой от FORmula TRANslator, то есть, переводчик формул. Фортран широко используется в первую очередь для научных и инженерных вычислений. Одно из преимуществ современного Фортрана — большое количество написанных на нём программ и библиотек подпрограмм. Современный Фортран (Fortran 95 и Fortran 2003) приобрёл черты, необходимые для эффективного программирования для новых вычислительных архитектур; позволяет применять современные технологии программирования, в частности, ООП.
Фортран — жёстко стандартизированный язык, именно поэтому он легко переносится на различные платформы. Существует несколько международных стандартов языка:
- FORTRAN IV (он же — FORTRAN 66) (1966)
- FORTRAN 77 (1978)
Множество улучшений: строковый тип данных и функции для его обработки, блочные операторы IF, ELSE IF, ELSE, END IF, оператор включения фрагмента программы INCLUDE и т. д. - Fortran 90 (1991)
Значительно переработан стандарт языка. Введён свободный формат написания кода. Появились дополнтельные описания IMPLICIT NONE, TYPE, ALLOCATABLE, POINTER, TARGET, NAMELIST; управляющие конструкции DO … END DO, DO WHILE, CYCLE, SELECT CASE, WHERE; работа с динамической памятью (ALLOCATE, DEALLOCATE, NULLIFY); программные компоненты MODULE, PRIVATE, PUBLIC, CONTAINS, INTERFACE, USE, INTENT. Появились новые встроенные функции, в первую очередь, для работы с массивами.
В языке появились элементы ООП.
Отдельно объявлен список устаревших черт языка, предназначенных для удаления в будущем. - Fortran 95 (1997)
Коррекция предыдущего стандарта. - Fortran 2003 (2004)
Дальнейшее развитие поддержки ООП в языке. Взаимодействие с операционной системой.
Фонд свободного программного обеспечения GNU выпускает открытый компилятор Фортрана-77 g77 , доступный практически для любой платформы и полностью совместимый с GCC , но не поддерживающий всех языковых конструкций современных стандартов Фортрана. Также существует проект g95 по созданию на основе GCC компилятора Fortran-95.
Структура программ изначально была ориентирована на ввод с перфокарт и имела ряд удобных именно для этого случая свойств. Так, 1-я колонка служила для маркировки текста как комментария (символом C), с 1-й по 5-ю располагалась область меток, а с 7-й по 72-ю располагался собственно текст оператора или комментария.
Колонки с 73-й по 80-ю могли служить для нумерации карт (чтобы восстановить случайно рассыпавшуюся колоду) или для краткого комментария, транслятором они игнорировались. Если текст оператора не вписывался в отведённое пространство (с 7-й по 72-ю колонку), в 6-ой колонке следующей карты ставился признак продолжения, и затем оператор продолжался на ней. Расположить два или более оператора в одной строке (карте) было нельзя. Когда перфокарты ушли в историю, эти достоинства превратились в серьёзные неудобства.
Именно поэтому в стандарт Фортрана, начиная с Fortran 90, в добавление к фиксированному формату исходного текста появился свободный формат, который не регламентирует позиции строки, а также позволяет записывать более одного оператора на строку. Введение свободного формата позволило создавать код, читабельность и ясность которого не уступает коду, созданному при помощи других современных языков программирования, таких как C или Java.
Своего рода «визитной карточкой» старого Фортрана является огромное количество меток, которые использовались как в операторах безусловного перехода GOTO , так и в операторах циклов, и в операторах описания форматного ввода/вывода FORMAT. Большое количества меток и операторов GOTO часто делало программы на Фортране трудными для понимания.
Именно этот негативный опыт стал причиной, по которой в ряде современных языков программирования (например, Java) метки и связанные с ними операторы безусловного перехода вообще отсутствуют.
Однако современный Фортран избавлен от избытка меток за счет введения таких операторов, как DO … END DO, DO WHILE, SELECT CASE
Также к положительным чертам современного Фортрана стоит отнести большое количество встроенных операций с массивами и гибкую поддержку массивов с необычной индексацией. Пример:
real,dimension(. ) :: V
.
allocate(V(-2:2,0:10)) ! Выделить память под массив, индексы которого могут
! меняться в пределах от -2 до 2 (первый индекс)
! и от 0 до 10 — второй
.
V(2,2:3)=V(-1:0,1) ! Повернуть кусочек массива
write(*,*)V(1,:) ! Напечатать все элементы массива V, первый индекс которых равен 1.
deallocate(V)
Пример программы
Программа «Hello, World!»
Фиксированный формат (символами «ˆ» выделены пробелы в позициях строки с 1 по 6):
^^^^^^PROGRAM hello
^^^^^^PRINT*, ‘Hello, World!’
^^^^^^END
Свободный формат:
program hello
print *, «Hello, World!»
end
Оператор PROGRAM не является обязательным. Строго говоря, единственный обязательный оператор Фортран-программы — оператор END.
Выбор прописных или строчных букв для написания операторов программы произволен. С точки зрения современных стандартов языка Фортран, множество прописных букв и множество строчных букв при написании операторов языка совпадают.
Пример простой программы на фортране (со свободной формой записи).
В примере демонстрируется использование модулей и внешних функций.
Функция CALC_AVERAGE содержится в отдельном файле и зависит от модуля ARRAY_CALCULATOR как объявленная в нем.
Оператор USE обеспечивает доступ к ARRAY_CALCULATOR. Этот модуль содержит объявление функции CALC_AVERAGE.
Массив из 5-ти элементов передается функции CALC_AVERAGE, которая возвращает значение AVERAGE для вывода на экран.
Листинг основной программы:
! Файл: main.f90
! Эта программа вычисляет среднее арифметическое 5ти чисел
PROGRAM MAIN
USE ARRAY_CALCULATOR
REAL, DIMENSION(5) :: A = 0
REAL :: AVERAGE
PRINT *, ‘Введите 5 чисел: ‘
READ (*,'(F10.3)’) A
AVERAGE = CALC_AVERAGE(A)
PRINT *, ‘Среднее арифметическое 5ти чисел: ‘, AVERAGE
END PROGRAM MAIN
Ниже приведен листинг модуля, на который ссылается основная программа. Он демонстрирует некоторые возможности Фортрана 90/95(интерфейсный блок и объявление массива).
! Файл: array_calc.f90.
! Модуль содержит различные вычисления над массивами.
MODULE ARRAY_CALCULATOR
INTERFACE
FUNCTION CALC_AVERAGE(D)
REAL :: CALC_AVERAGE
REAL, INTENT(IN) :: D(:)
END FUNCTION CALC_AVERAGE
END INTERFACE
! другие интерфейсы подпрограмм.
END MODULE ARRAY_CALCULATOR
Ниже листинг объявленной функции, вызываемой из основной программы
! Файл: calc_aver.f90.
! Внешняя функция, возвращающая среднее арифметическое значений массива.
FUNCTION CALC_AVERAGE(D)
REAL :: CALC_AVERAGE
REAL, INTENT(IN) :: D(:)
CALC_AVERAGE = SUM(D) / UBOUND(D, DIM = 1)
END FUNCTION CALC_AVERAGE
Файлы, содержащие приведенные выше строки, можно отдельно скомпилировать а затем слинковать друг с другом используя следующие команды:
ifort -c array_calc.f90
ifort -c calc_aver.f90
ifort -c main.f90
ifort -o calc main.o array_calc.o calc_aver.o
В этом наборе команд:
Опция -c предотвращает сборку и оставляет .o файлы.
Первая команда создает файлы array_calculator.mod и
array_calc.o (имя в операторе MODULE определяет название файла модуля array_calculator.mod). Файл модуля записывается в текущую директорию.
Вторая команда создает файл calc_aver.o.
Третья команда создает файл main.o и использует модульный файл array_calculator.mod.
Последняя команда собирает все объектные файлы в исполняемую программу с названием calc. Для линковки используется ifort вместо команды ld.
Порядок, в котором приведены файлы, важен.В данной команде ifort:
Компилируем array_calc.f90, который содержит описание модуля, и создаем его объектный файл и файл array_calculator.mod.
Компилируем calc_aver.f90, в котором содержится внешняя функция
CALC_AVERAGE.
Компилируем main.f90 (основная/главная программа). Оператор USE
ссылается на файл array_calculator.mod.
Используем ld для сборки основной программы и всех объектных файлов в исполняемый файл с именем calc.
Запуск программы
Если прописан путь в директорию, содержащую прграмму calc, то для запуски достаточно ввести ее имя:
calc
При запуске операторы PRINT и READ из основной программы приведут в результате к следующему диалогу между пользователем и программой:
Введите 5 чисел:
55.5
4.5
3.9
9.0
5.6
Среднее арифметическое 5ти чисел: 15.70000
Ссылки на полезные материалы по языку:
На этом мой выпуск окончен. Надеюсь он вам понравился. В следующий раз мы поговорим с вами о языке программирования D.
До встречи через неделю!
Источник: subscribe.ru