Фортран: пишем параллельные программы
Современный Фортран представляет собой специализированный язык программирования, предназначенный в основном для написания вычислительных программ для векторно-конвейерных и параллельных архитектур. Эволюция стандартов языка Фортран была рассмотрена в предыдущих статьях – здесь и здесь.
На данный момент действующим стандартом языка Фортран является стандарт 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 обладает более мощным оптимизатором.
Постановка задачи
Напишем простейшую программу для реализации классического клеточного автомата игры «Жизнь». Не будем сейчас париться с вводом и выводом, исходную конфигурацию зададим в самой программе, а результирующую конфигурацию после заданного числа шагов выведем в файл. Нас будут интересовать сами вычислительные шаги клеточного автомата. Эта задача хороша для нас тем, что она позволяет небольшими усилиями достичь любого наперёд заданного объёма чистых (pure) вычислений с массивами произвольных размеров, не вырождаясь в заведомо излишний код, который оптимизатор мог бы выкинуть, обманув наши метрики производительности.
Для тестов, чтобы далеко не ходить, используется компьютер Mac mini с процессором Intel Core i3 @ 3.6 GHz с 4 физическими ядрами. Компиляторы GNU Fortran 12.2.0 и Intel Classic Fortran 2021.8.0 20221120.
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, 'сек, ', & int(rows*cols,8)*steps/(clock_cnt2-clock_cnt1)*clock_rate, 'ячеек/с' ! выведем результирующую конфигурацию в файл для контроля call output_matrix (field (:, :, thisstep)) ! разместим подпрограммы тут же, чтобы оптимизатору было проще contains ! проинициализируем, просто воткнув одну "мигалку" в чистое поле 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 (' ') + & m(i, j)*(ichar ('*') - ichar (' '))), j=1, cols) end do close (outfile) end subroutine output_matrix ! здесь самое интересное – обработка шага ! для начала простой последовательный алгоритм 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 и cols ! конечно, мы могли бы из просто передать в параметрах, но так культурнее rows = size (m1, dim=1) - 2 cols = size (m1, dim=2) - 2 ! обычные последовательные вложенные циклы ! поскольку в Фортране массивы хранятся по столбцам, то j раньше i 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 end program life
Откомпилируем нашу программу при помощи GNU Fortran и Intel 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.
Попробуем запустить автоматический параллелизатор (спасибо@AlexTmp8за замечание в комментариях):
$ 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:rows+1,0:cols-1) + m1(:,0:cols-1) + m1(0:rows-1,2: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 !$end parallel do m2 (0,:) = m2 (rows, :) m2 (rows+1, :) = m2 (1, :) m2 (:, 0) = m2 (:, cols) m2 (:, cols+1) = m2 (:, 1) end subroutine process_step
Не забудем подключить OpenMP при компиляции:
$ gfortran life_omp.f90 -o life_omp_g -O3 -ftree-vectorize -fopt-info-vec -flto -fopenmp
$ ifort life_omp.f90 -o life_omp -Ofast -qopenmp
$ ./life_omp_g
3 сек, 377022000 ячеек/с
$ ./life_omp
3 сек, 356690000 ячеек/с
Теперь наш цикл выполняется одновременно на 4 ядрах процессора, за счёт чего выполнение ускорилось в 3 с лишним раза. По-прежнему, однако, GNU Fortran чуть впереди Intel Fortran’а.
3. SMP параллелизм через DO CONCURRENT
Попробуем переписать нашу программу стандартными средствами параллельного SMP программирования языка Фортран, без использования внешнего API OpenMP:
! подпрограмма снова может быть чистой, так как она не управляет нитками 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 ! так выглядит параллельный цикл в стандарте Фортрана ! как и в OpenMP,здесь распараллелен только внешний цикл do concurrent (j = 1:cols) local (i, s) 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) + & 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 m2 (0,:) = m2 (rows, :) m2 (rows+1, :) = m2 (1, :) m2 (:, 0) = m2 (:, cols) m2 (:, cols+1) = m2 (:, 1) end subroutine process_step
Что же это нам даёт?
$ ./life_con2
4 сек, 308920000 ячеек/с
Компилятор увлёкся обилием возможностей и ухудшил результат. Так что параллелить всё же надо с умом.
Вывод
Мы рассмотрели компиляцию простейшей программы на современном Фортране с использованием средств векторизации и симметричных параллельных вычислений. В результате тестов Intel Fortran показал преимущество в поддержке возможностей языка и в автопараллелизации последовательного кода, а GNU Fortran – в скорости работы кода с ручным управлением параллелизацией. При этом, однако, не надо забывать, что Intel Fortran поддерживает мощные методы совместной оптимизации раздельно расположенных в исходных файлах единиц компиляции, поэтому для большой программы сравнительный результат мог бы быть другим.
Получается, что компилятор Intel Classic Fortran (ifort) более эффективен тогда, когда нам нужно оттранслировать на SMP много унаследованного последовательного кода, автоматически его распараллелив. GNU Fortran же позволяет генерировать более эффективный код в абсолютном зачёте, но требует для этого некоторой ручной работы по явному указанию параллелизации.
В следующей статье мы рассматриваем средства поддержки массивно-параллельных архитектур, имеющиеся в современном Фортране, и ещё ускоряем нашу программу.
- программирование
- fortran
- фортран
- параллелизация
- параллельное программирование
- Программирование
- Fortran
- Параллельное программирование
Установка GFortran#
GFortran – название проекта GNU Fortran project. Главная wiki-страница предлагает множество полезных ссылок на статьи о GFortran, а также о Fortran в целом. В данном руководстве процесс установки GFortran в Windows, Linux, macOS и OpenBSD представлен в удобном для новичков формате на основе информации с сайта GFortranBinaries.
Windows#
Три источника предлагают быстрый и простой способ установки компилятора GFortran в Windows:
- http://www.equation.com предоставляет 32 и 64-битные x86-совместимые исполняемые файлы для компилятора GCC версии 12.1.
- TDM GCC предоставляет 32 и 64-битные x86-совместимые исполняемые файлы для компилятора GCC версии 10.3.
- MinGW-w64 предоставляет 64-битный x86-совместимый исполняемый файл для GCC версии 12.2.
Во всех вышеперечисленных вариантах процесс установки прост – просто скачайте программу установки и следуйте указаниям мастера установки.
Unix-подобная разработка в Windows#
Для тех кто знаком с unix-подобной средой разработки, в Windows доступно несколько вариантов эмуляции, каждый из которых предоставляет пакеты для компилятора gfortran:
- Cygwin: среда выполнения, которая предоставляет POSIX-совместимое окружение в Windows.
- MSYS2: набор Unix-подобных средств разработки, основанных на современных версиях Cygwin и MinGW-w64.
- Windows Subsystem for Linux (WSL): официальный уровень совместимости для запуска бинарных исполняемых файлов Linux в Windows. С помощью Windows Subsystem for Linux GUI можно запускать текстовые редакторы и другие графические программы.
Всё вышеперечисленные варианты предоставляют доступ к распространённым командным оболочкам, таким как bash, и инструментам разработки, включая GNU coreutils, Make, CMake, autotools, git, grep, sed, awk, ssh и др.
Мы рекомендуем окружение WSL тем, кто ищет Unix-подобную среду разработки.
Linux#
Дистрибутивы на основе Debian (Debian, Ubuntu, Mint и др.)#
Проверьте, установлен ли у вас компилятор gfortran, выполнив в командной строке
which gfortran
Если в результате выполнения команды в окне ввода команд ничего не отобразилось, тогда компилятор gfortran не установлен. Для его установки наберите команду:
sudo apt install gfortran
чтобы проверить, какая версия была установлены, наберите команду:
gfortran --version
Вы можете установить несколько версий вплоть до версии 10 (в дистрибутиве Ubuntu 22.04), указав номер версии после «gfortran», например:
sudo apt install gfortran-8
Чтобы установить новые версии на старые версии выпусков дистрибутива Ubuntu, вам сначала нужно добавить следующий репозиторий, обновить список пакетов, а затем установить компилятор:
sudo add-apt-repository ppa:ubuntu-toolchain-r/test sudo apt update sudo apt install gfortran-10
Наконец, вы можете переключаться между различными версиями компилятора или установить версию по умолчанию с помощью команды update-alternatives (см. manpage). Существует множество онлайн-уроков по использованию этой команды. Хорошо структурированное руководство на примере языков C и C++ можно найти здесь. Вы можете применить ту же логику, заменив названия gcc и g++ на gfortran .
Дистрибутивы на основе RPM (Red Hat Enterprise Linux, CentOS, Fedora, openSUSE)#
sudo yum install gcc-gfortran
Начиная с Fedora 22 и Red Hat Enterprise Linux 8, dnf – менеджер пакетов по умолчанию:
sudo dnf install gcc-gfortran
Дистрибутивы на основе Arch (Arch Linux, EndeavourOS, Manjaro и др.)#
sudo pacman -S gcc-fortran
macOS#
Xcode#
Если у вас установлен Xcode, то откройте окно ввода команд и наберите:
xcode-select --install
Готовые бинарные сборки#
Посетите fxcoudert/gfortran-for-macOS для установки готовых бинарных сборок (исполняемых файлов).
Homebrew#
brew install gcc
Fink#
Ссылка на пакет GNU-gcc
MacPorts#
Поиск доступных версий gcc:
port search gcc
Установка gcc определённой версии:
sudo port install gcc10
OpenBSD#
pkg_add g95
В OpenBSD исполняемый файл компилятора GFortran называется egfortran . Чтобы проверить установлен ли он, выполните команду:
egfortran -v
OpenCoarrays#
OpenCoarrays – проект программного обеспечения с открытым исходным кодом, который предоставляет бинарный интерфейс приложения (ABI), используемый внешним интерфейсом GNU Compiler Collection (GCC) Fortran для сборки исполняемых приложений, которые используют возможности параллельного программирования стандарта Fortran 2018. Поскольку OpenCoarrays не является отдельным компилятором, мы упоминаем его здесь, в разделе о gfortran.
Хотя вы можете скомпилировать в gfortran совершенно корректный код с использованием комассивов (coarrays), но сгенерированные исполняемые файлы будут работать только в одном образе – image (image – термин языка Fortran для параллельного процесса), то есть, в последовательном режиме выполнения. OpenCoarrays позволяет выполнять код параллельно на машинах с общей и распределённой памятью, подобно интерфейсу MPI:
cafrun -n
Процесс его установки описан на официальном сайте в понятной и исчерпывающей форме.
Подчеркнём, что его установка напрямую в Windows невозможна. Она возможна только через WSL.
so the DOM is not blocked —>
© Copyright 2020-2022, Fortran Community.
Фортран: пишем параллельные программы для суперкомпьютера
В первой части статьи мы рассмотрели написание на современном Фортране простой программы, реализующей клеточный автомат «Жизнь», в виде классического последовательного кода (SISD), матричных операций (SIMD) и параллельных конструкций SMP (SIMD с частью функций MIMD). Сейчас мы будем рассматривать использование конструкций Фортрана для программирования массивно-параллельных архитектур (MPP), к которым, в частности, относятся современные суперкомпьютеры. Такие архитектуры реализуют классическую схему MIMD.
Как и в прошлой статье, мы используем для иллюстрации материала компиляторы GNU Fortran и Intel Fortran.
Постановка задачи
Как и в первой части статьи, мы продолжим реализовывать тот же самый клеточный автомат с теми же самыми входными и выходными данными. К сожалению, компилятор Intel Fortran не поддерживает используемую для программирования MPP архитектуру Coarray под операционной системой macOS, поэтому примеры для Intel Fortran мы в этот раз будем выполнять на другой машине с Linux. Это делает невозможным сравнение результатов в абсолютных цифрах, но по относительным оценкам всё по-прежнему будет ясно.
Для GNU Fortran, как и в первой части статьи, будет ипользоваться компилятор версии 12.2.0 и библиотека OpenCoarrays на компьютере Mac mini с 4-ядерным процессором Intel Core i3 @ 3.6 GHz под управлением macOS. Для Intel Fortran будут использоваться компилятор ifort версии 2021.9.0 20230302 и компилятор ifx 2023.1.0 20230320 на компьютере IBM x3250 M4 с 8-ядерным процессором Xeon 4C E3-1270v2 @ 3.9 GHz под управлением Linux. Ядер в этом процессоре больше, чем в Маке, но он постарее и потормознее.
0-4. Повторение темы
Напомним, что в прошлый раз мы достигли следующих результатов на Маке.
Последовательная программа в режиме автопараллелизации, gfortran и ifort:
11 сек, 124773000 ячеек/с
4 сек, 338120000 ячеек/с
Параллельная SMP программа средствами OpenMP, лучший результат gfortran:
3 сек, 377022000 ячеек/с
3 сек, 356690000 ячеек/с
Параллельная SMP программа средствами DO CONCURRENT , лучший результат ifort:
3 сек, 355890000 ячеек/с
Повторим эти результаты на нашем сервере Linux, чтобы можно было в дальнейшем опираться на какие-то сопоставимые значения (с автопараллелизацией ifort; ifx её не поддерживает):
9 сек, 144360000 ячеек/с
19 сек, 71030000 ячеек/с
8 сек, 162910000 ячеек/с
37 сек, 37730000 ячеек/с
Это в целом совпадает с результатами, приведёнными @mobi в комментариях к первой части статьи.
Прежде чем перейти к изложению нового материала, вспомним логику работы нашей программы в архитектуре SMP.
Мы написали классический последовательный алгоритм, а затем применили к нему некоторые небольшие подсказки компилятору о параллелизации, позволившие распараллелить код на несколько SMP ядер, сохраняя при этом его логическую эквивалентность с последовательным алгоритмом. Это очень важное обстоятельство. Вообще в SMP парадигме можно в параллельных процессах делать совершенно разные вещи, но в рассмотренной нами логике мы просто ускоряли последовательный процесс, имеющий один вход и один выход, методом его логически эквивалентных преобразований в частично параллельный.
С другой стороны, архитектура SMP предопределила используемую нами модель оперативной памяти. Все объекты в памяти разделялись между всеми параллельными процессами, одинаково принадлежа им всем, за исключением некоторых отдельных локальных для процесса переменных, которые мы специально описывали как LOCAL или OMP PRIVATE .
5. MPP параллелизм через комассивы
Модель массивно-параллельных вычислений совершенно другая. Она просто излагается, но не всегда легко укладывается в голове для практического применения, поэтому тут нужно некоторое внимание.
Для начала, давайте разберёмся, чем вообще плохо SMP? Не особо парясь, парой подсказок компилятору мы можем ускорять свои последовательные программы в несколько раз – казалось бы, хватило бы только ядер, как пирату в мультфильме.

К сожалению, пулемётную ленту из ядер мы организовать не можем, так как очень быстро узким местом становится оперативная память. Разделение доступа к памяти между несколькими арифметико-логическими устройствами сложно чисто технически, и даже если мы это всё нужным образом припаяем и электрически нагрузим, то всё равно наши ядра будут конкурировать за доступ к одним и тем же страницам памяти (она же разделяемая) на более высоком уровне. Поэтому больше десятка-другого ядер в архитектуре SMP эффективно не подключить.
Что же нам делать, если мы хотим построить суперкомпьютер, включающий десятки тысяч вычислительных ядер (то есть собственно массивно-параллельную архитектуру)? В таком случае используются вычислительные узлы, каждый из которых содержит собственную оперативную память. Узлы объединены специальной высокоскоростной вычислительной сетью, позволяющей синхронизировать отдельные области памяти при помощи сообщений между узлами. Сам узел при этом чаще всего внутри себя, то есть на более низком уровне иерархии, использует несколько ядер, объединённых архитектурой SMP. Архитектура MPP не обязательно ограничивается двумя уровнями иерархии, как тут упрощённо описано; уровней может быть и больше (например, стойки суперкомпьютера взаимодействуют между собой с меньшей скоростью, чем узлы внутри стойки).
Программная архитектура MPP систем восходит к транспьютерам и ранним разработкам массивно-параллельного кода на языке Оккам.
Среда выполнения организована таким образом, что программа запускается одновременно в нескольких экземплярах, по одному экземпляру на узел (или ядро узла, что пока что для нас логически неважно). Текст и машинный код программы при этом на каждом используемом узле один и тот же, но динамика выполнения этого кода разная.
Так как код одинаков, то получается, что динамика его выполнения должна зависеть от каких-то внешних обстоятельств. И самым главным из таких обстоятельств является просто-напросто номер узла. Этот номер узла передаётся программе системной утилитой, запускающей массивно-параллельный код на массиве узлов. В Фортране номер узла, на котором в данный момент выполняется код, выдаётся функцией THIS_IMAGE() и всегда принимает значения от 1 до общего количества выполняющих программу узлов, которое, в свою очередь, выдаётся функцией NUM_IMAGES() .
Как ясно из вышесказанного, каждый параллельно выполняющийся процесс в архитектуре MPP имеет свою собственную оперативную память. По этой причине все переменные, описанные в фортрановской программе, хранят собственные значения для каждого номера узла. В качестве исключения из этого правила, в коде могут быть специальным образом описаны комассивы, позволяющие программе получить доступ к оперативной памяти другого узла. Такой доступ к элементам комассивов имеет специальный синтаксис с квадратными скобками и не может применяться везде, где допустимо использование переменных в локальной оперативной памяти узла. Например, по очевидным причинам нельзя создать указатель на объект в чужой оперативной памяти.
Теперь, как говорится, со всей этой фигнёй мы попытаемся взлететь. Ясно, что в такой модели уже одной-двумя подсказками компилятору не обойдёшься, надо менять логику кода.
Наш общий подход будет типичным для этого стиля программирования. Игровое поле мы объявим комассивом, к которому потенциально имеют доступ все узлы. Но фактически каждый узел будет заниматься только своей частью поля. Для этого разделим все столбцы матрицы field на полосы по числу узлов. Каждый узел рассчитывает значения элементов массива в своей полосе и заполняет полосу массива в своей оперативной памяти. Кроме того, по краям полосы узла находятся две единичные полоски, интересные его соседям. Значения в этих полосках узел копирует в оперативную память своих соседей.
Важно понять, что комассив field, как целое, вообще физически нигде не существует до момента окончательной сборки при записи результата в память. Он раскидан по своим отображениям на узлах, в которых каждый узел физически работает только со своей полосой из этого массива. По этой причине мы поменяли в программе также инициализацию матрицы, чтобы не занимать физическими нулями оперативную память из ненужных частей массива, которая фактически никогда не будет использоваться узлом.
Например, если у нас массив имеет 1002 столбца (1000 основных и 2 по краям для свёртки), а программа исполняется на 10 узлах, то узел номер 1 вычисляет столбцы 0:100, узел номер 2 – 101:200 и т.д. При этом столбец 100, рассчитанный узлом 1, помещается им не только в свою оперативную память, но и в память узла 2, так как этот столбец понадобится узлу 2 для расчёта столбца 101. Такие перекрывающиеся части комассивов называются “гало”.
Итак, вот новый код:
program life_mpp implicit none integer, parameter :: matrix_kind = 4 integer, parameter :: generations = 2 integer, parameter :: rows = 1000, cols = 1000 integer, parameter :: steps = 10000 integer (kind=matrix_kind) :: field (0:rows+1, 0:cols+1, generations) [*] integer :: thisstep = 1, nextstep =2 integer :: i integer :: clock_cnt1, clock_cnt2, clock_rate integer, allocatable :: cols_lo (:), cols_hi (:) ! диапазоны столбцов для узлов integer :: me ! номер текущего узла, чтобы обращаться покороче me = this_image() print *, "it's me:", me ! заполним таблицы верхних и нижних границ полос для узлов allocate (cols_lo (num_images()), cols_hi (0:num_images())) cols_hi (0) = 0 do i = 1, num_images() cols_lo (i) = cols_hi (i-1) + 1 cols_hi (i) = cols * i / num_images() end do ! проинициализируем матрицу (инициализация тоже изменилась) call init_matrix (field (:, :, thisstep)) sync all ! первый узел займётся хронометрированием if (me == 1) call system_clock (count=clock_cnt1) ! цикл выглядит как и раньше, но после каждого шага синхронизируемся, ! чтобы не возникло разнобоя с сечениями источника и приёмника do i = 1, steps call process_step (field (:, :, thisstep), field (:, :, nextstep)) thisstep = nextstep nextstep = 3 - thisstep sync all end do ! первый узел заканчивает хронометрирование, печатает результат, ! собирает матрицу и пишет в файл if (me == 1) then call system_clock (count=clock_cnt2, count_rate=clock_rate) print *, (clock_cnt2-clock_cnt1)/clock_rate, 'сек, ', & int(rows*cols,8)*steps/(clock_cnt2-clock_cnt1)*clock_rate, 'ячеек/с' ! мы хотим вывести матрицу в файл, а разные её столбцы хранятся ! на разных узлах. надо собрать всю матрицу на пишущем узле do i = 2, num_images() field (:, cols_lo(i):cols_hi(i), thisstep) = & field (:, cols_lo(i):cols_hi(i), thisstep) [i] end do call output_matrix (field (:, :, thisstep)) end if ! надо синхронизироваться в конце, а то освободится память ! завершившегося процесса, пока её не скопировали sync all ! уходим всей бригадой contains pure subroutine init_matrix (m) integer (kind=matrix_kind), intent (out) :: m (0:,0:) integer j ! обнулим поле в своей полосе и гало ! не лезем далеко в чужие полосы, чтобы не распределять ! ненужную узлу память do j = cols_lo(me)-1, cols_hi(me)+1 m (:, j) = 0 end do ! первый и последний узлы обнуляют кромки поля if (me == 1) m (:, 0) = 0 if (me == num_images()) m (:, cols+1) = 0 ! нарисуем "мигалку" на имеющих отношение к ней узлах if (cols_lo(me) = 49) m (50, 50) = 1 if (cols_lo(me) = 50) m (50, 51) = 1 if (cols_lo(me) = 51) m (50, 52) = 1 end subroutine init_matrix ! gfortran считает эту подпрограмму impure из-за доступа в чужую память ! ifort и ifx считают её 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 do j = cols_lo (me), cols_hi (me) 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 ! обмениваемся гало с соседями снизу и сверху if (me > 1) then m2 (:, cols_lo (me)) [me-1] = m2 (:, cols_lo (me)) end if if (me < num_images()) then m2 (:, cols_hi (me)) [me+1] = m2 (:, cols_hi (me)) end if ! заворачивание краёв здесь будет посложнее, так как выполняется разными узлами ! в пределах своей компетенции if (me == num_images()) m2 (:, 0) [1] = m2 (:, cols) if (me == 1) m2 (:, cols+1) [num_images()] = m2 (:, 1) m2 (0,cols_lo(me):cols_hi(me)) = m2 (rows, cols_lo(me):cols_hi(me)) m2 (rows+1, cols_lo(me):cols_hi(me)) = m2 (1, cols_lo(me):cols_hi(me)) end subroutine process_step subroutine output_matrix (m) integer (kind=matrix_kind), intent (in) :: m (0:,0:) integer :: rows, cols integer :: i, j, n integer :: outfile rows = size (m, dim=1) - 2 cols = size (m, dim=2) - 2 open (file = 'life.txt', newunit=outfile) do i = 1, rows write (outfile, '(*(A1))') (char (ichar (' ') + & m(i, j)*(ichar ('*') - ichar (' '))), j=1, cols) end do close (outfile) end subroutine output_matrix end program life_mpp
Для трансляции и запуска программ в mpp архитектуре зачастую используются специальные скрипты:
$ caf life_mpp.f90 -o life_mpp -O3 -ftree-vectorize -fopt-info-vec -flto -fopenmp
$ mpiexec -n 4 ./life_mpp
2 сек, 480267000 ячеек/с
Как видим, gfortran ускорил нашу программу ещё на 30% по сравнению с лучшим результатом SMP, достигнув на 4 ядрах производительности 3.84 от последовательной версии. Очевидно, это связано с тем, что даже в SMP системе расшивка параллельных процессов по адресам памяти ускоряет работу с оперативной памятью.
У Intel Fortran:
$ ifort life_mpp.f90 -o life_mpp_ifort -Ofast -coarray
$ ifx life_mpp.f90 -o life_mpp_ifx -Ofast -coarray
Лучшие Альтернативные варианты для Simply Fortran для Windows
Simply Fortran — это интегрированная среда разработки, поддерживаемая Approximatrix, LLC. Это программное обеспечение для разработки и ИТ помогает пользователям работать с языком программирования FORTRAN. Он позволяет проводить численные.
GTK+ 2 Runtime Environment
Мультиплатформенный инструментарий для создания графических пользовательских интерфейсов.
Скачать
GTK+ 2 Runtime Environment — отличное бесплатное (gpl) программное обеспечение для Windows, относящееся к категории Программное обеспечение для разработки с.
Является ли это приложение хорошей альтернативой приложению Simply Fortran?
Спасибо за оценку!
Beerwin´s PlainHTML
A free app for Windows, by Beerwin.
Скачать
Если вы относитесь к числу программистов старой школы и предпочитаете создавать веб-страницы традиционным способом, то этот инструмент, вероятно, принесет.
Является ли это приложение хорошей альтернативой приложению Simply Fortran?
Спасибо за оценку!
RAD Developer IDE
RAD Developer — это интегрированная среда разработки (включая.
Скачать
Является ли это приложение хорошей альтернативой приложению Simply Fortran?