1 1 Работа с файлами

Важным моментом при написании параллельной программы является работа по сохранению данных в файлы или восcтановлению данных из них. Примерами необходимости таких действий могут служить задачи загрузки начальных данных в массивы при запуске программы и сохранение промежуточных данных и/или результатов счета.

Если говорить о загрузке начальных данных или сохранении результатов счета, то эта процедура происходит в самом начале работы программы или в самом ее конце, и никаких особенностей или отличий о того, как это может быть сделано в последовательной программе, для параллельной программы нет.

Однако, в том случае, когда речь идет о сохранении промежуточных данных, появляются некоторые нюансы, связанные со структурой параллельной программы. Дело в том, что файловые операции - очень медленные операции. Если сохранение данных происходит в каждом итерационном цикле, то такое дейтсвие может существенно снизить быстродействие программы, что кстати верно и в случае обычного последовательного алгоритма. Поэтому при проектировании параллельной программы следует придерживаться двух принципов:

  • Сохранение данных должно происходить не в каждом итерационном цикле, а, скажем, в каждом десятом. В любом случае время между моментами сохранения болжно быть много больше времени, требующемся для записи данных в файл.
  • Сохранение данных должно быть реальзовано с использованием неблокирующих операторов ввода-вывода, позволяющих совместить по времени файловые операции и вычисления.
Рекомендации, данные на этой странице, проверялись на компиляторе Intel Fortran. Проверьте, поддерживает ли ваш компилятор процедуры асинхронной записи. Если нет - ну что ж, вам придется отказаться от распараллеливания процедур вычисления и сохранения данных. Постарайтесь делать запись на диск как можно реже, чтобы это минимальным образом сказалось на быстродействиии вашего кластера.

Посмотрим на конкретном примере, как можно реализовать эти два принципа. Возьмем нашу программу вычисления уравнения теплопроводности, а именно - блок, описывающий итерационный цикл:

 55  c     Подпрограмма вычисления искомой функции
 56        subroutine iter(f0,f1,xmax,ymax,df)
 57        include 'mpif.h'
 58        integer xmax,ymax
 59        real*8 f0(xmax,ymax), f1(xmax,ymax)
 60        real*8 dt,dx,dy
 61        real*8 df
 62        integer x,y,n,myid,np,ierr,status(MPI_STATUS_SIZE)
 63        common myid,np
 64        dt=0.01
 65        dx=0.5
 66        dy=0.5
 67  c     Обмениваемся границами с соседом
 68        if ( myid .gt. 0 )
 69       x call MPI_SENDRECV(
 70       x f0(1,2), xmax, MPI_REAL8, myid-1, 1, 
 71       x f0(1,1), xmax, MPI_REAL8, myid-1, 1, 
 72       x MPI_COMM_WORLD, status, ierr)
 73        if ( myid .lt. np-1 )
 74       x call MPI_SENDRECV(
 75       x f0(1,ymax-1), xmax, MPI_REAL8, myid+1, 1, 
 76       x f0(1,ymax), xmax, MPI_REAL8, myid+1, 1, 
 77       x MPI_COMM_WORLD, status, ierr)
 78  c     Вычисляем функцию в ячейках сетки
 79        do x=2,xmax-1
 80          do y=2,ymax-1
 81          f1(x,y)=f0(x,y)+dt*(
 82       x    (f0(x+1,y)-2*f0(x,y)+f0(x-1,y))/(dx*dx)
 83       x    +
 84       x    (f0(x,y+1)-2*f0(x,y)+f0(x,y-1))/(dy*dy)
 85       x    )
 86          end do
 87        end do
 88  c     Копируем границу для следующей итерации
 89        do x=1,xmax
 90          f1(x,1)=f0(x,1)
 91          f1(x,ymax)=f0(x,ymax)
 92        end do
 93        do y=1,ymax
 94          f1(1,y)=f0(1,y)
 95          f1(xmax,y)=f0(xmax,y)
 96        end do
 97  c     Находим максимальную дельту
 98        df=0.0
 99        do x=1,xmax
100          do y=1,ymax
101            if ( df .lt. abs(f0(x,y)-f1(x,y)) ) then
102                 df = abs(f0(x,y)-f1(x,y))
103            endif
104          end do
105        end do
106        return
107        end

Поскольку мы решили, что сохранение данных будет происходить на каждой 10-й итерации, то есть запись будет происходить в зависимости от номера итерации, то мы должны будем передавать в тело подпрограммы номер текущего цикла. Поэтому вначале изменим описание подпрограммы, добавив дополнительный параметр ее вызова (строка 56).

Как мы знаем, рассматриваема нами подпрограмма по неизменяемым (и это важно!) в течение цикла данным массива f0, вычисляет значения массива f1. Поэтому в ее теле мы должны решить, наступил ли десятый цикл, и если да, то инициировать неблокирующую запись массива f0 в файл. Что мы и делаем в строке 77b. Далее мы открываем файл и инициируем процесс записи в него массива данных (строки 77k и 77q).

Кроме того, надо позаботиться о том, чтобы каждый процесс параллельной программы сохранял свою часть данных в отдельный, принадлежащий только ему файл. Вычислением название файла данных мы в строке 77d.

После инициации записи управление передается циклу, в котором и происходят необходимые нам вычисления, причем процессы вычисления и записи идут параллельно. Перед возвратом управления из подпрограммы мы должны удостовериться, что процедура записи в файл закончена и закрыть файл (строки 105b и 105с).

Теперь посмотрим, как это можно реализовать в программном коде.

 55  c     Подпрограмма вычисления искомой функции
 56        subroutine iter(f0,f1,xmax,ymax,df,n)
 57        include 'mpif.h'
 58        integer xmax,ymax
 59        real*8 f0(xmax,ymax), f1(xmax,ymax)
 60        real*8 dt,dx,dy
 61        real*8 df
 62        integer x,y,n,myid,np,ierr,status(MPI_STATUS_SIZE)
 63        common myid,np
-------------------------------------------------------------------
 63a       character(LEN=20):: st,fname
 63b       logical ex
-------------------------------------------------------------------
 64        dt=0.01
 65        dx=0.5
 66        dy=0.5
 67  c     Обмениваемся границами с соседом
 68        if ( myid .gt. 0 )
 69       x call MPI_SENDRECV(
 70       x f0(1,2), xmax, MPI_REAL8, myid-1, 1, 
 71       x f0(1,1), xmax, MPI_REAL8, myid-1, 1, 
 72       x MPI_COMM_WORLD, status, ierr)
 73        if ( myid .lt. np-1 )
 74       x call MPI_SENDRECV(
 75       x f0(1,ymax-1), xmax, MPI_REAL8, myid+1, 1, 
 76       x f0(1,ymax), xmax, MPI_REAL8, myid+1, 1, 
 77       x MPI_COMM_WORLD, status, ierr)
-------------------------------------------------------------------
 77a c      Если десятый цикл, то сохраняем данные
 77b        if ( ((n/10)*10 .EQ. n ) then
 77c c         Вычисляем название файла данных
 77d           write(fname,'(A,I2.2,A)') "node",myid,".dat"
 77e c         Проверяем существует ли файл для записи
 77f           inquire(file=fname, exist=ex)
 77g c         Если существует, то открываем его для перезаписи,
 77h c         если файл не существует - открываем его как новый
 77i           write(st,'(A)') 'new'
 77j           if ( ex ) write(st,'(A)') 'old'
 77k           open(unit=20, 
 77l      $         file=fname, 
 77m      $         asynchronous='yes', 
 77n      $         status=st,
 77o      $         form='unformatted')
 77p c         Инициируем запись массива f0 в файл
 77q           write(20,ID=idvar,asynchronous='yes') f0
 77r       endif
-------------------------------------------------------------------
 78  c     Вычисляем функцию в ячейках сетки
 79        do x=2,xmax-1
 80          do y=2,ymax-1
 81          f1(x,y)=f0(x,y)+dt*(
 82       x    (f0(x+1,y)-2*f0(x,y)+f0(x-1,y))/(dx*dx)
 83       x    +
 84       x    (f0(x,y+1)-2*f0(x,y)+f0(x,y-1))/(dy*dy)
 85       x    )
 86          end do
 87        end do
 88  c     Копируем границу для следующей итерации
 89        do x=1,xmax
 90          f1(x,1)=f0(x,1)
 91          f1(x,ymax)=f0(x,ymax)
 92        end do
 93        do y=1,ymax
 94          f1(1,y)=f0(1,y)
 95          f1(xmax,y)=f0(xmax,y)
 96        end do
 97  c     Находим максимальную дельту
 98        df=0.0
 99        do x=1,xmax
100          do y=1,ymax
101            if ( df .lt. abs(f0(x,y)-f1(x,y)) ) then
102                 df = abs(f0(x,y)-f1(x,y))
103            endif
104          end do
105        end do
-------------------------------------------------------------------
105a c     Дожидаемся окончания записи и закрываем файл
105b       wait(unit=20,ID=idvar)
105c       close(unit=20)
-------------------------------------------------------------------
106        return
107        end
загрузить исходник программы
 

Процедура записи данных, так, как она представлена на этой странице, не вполне оптимальна. Дело в том, что после выхода из подпрограммы до начала следующего цикла выполняется код, который не модифицирует записываемый массив. И код этот выполняется достаточно долго. Но для простоты восприятия мы всю процедуру сохранения данных уложили внутрь рассматриваемой подпрограммы. Выше дана ссылка на исходный код программы, в котором учтена эта недоработка.

В заключение посмотрим, как изменились временные (скоростные) характеристики нашей программы от добавления процедуры записи. В представленной ниже таблице дана длительность работы нашей программы в трех вариантах. Первый вариант - наша исходная программа без записи данных в файл. Второй вариант - программа с записью данных в синхронном режиме, то есть без распараллеливания процедур вычисления и записи. И третий вариант - программа с записью данных на диск в асинхронном режиме, то есть с распараллеливанием счета и записи в файл.

Точность вычислений в программе немного увеличена по сравнению с исходной программой, которую мы рассматривали в пердыдущих разделах, чтобы потребовалось большее число итераций для нахождения решения уравнения. Все варианты программы запускались для работы на трех процессорах. Запись данных велась на расшаренный в NFS ресурс, кроме первого процесса, где запись велась на локальный диск. Кроме того даны результаты, полученные при записи данных на локальный диск (всеми процессами параллельной задачи).

Вариант програмыФайловая система Время Счета
Записи данных нетне исп.1 мин. 26 сек.
Синхронная записьNFS2 мин. 46 сек.
Синхронная записьлокальная1 мин. 37 сек.
Асинхронная записьNFS2 мин. 37 сек.
Асинхронная записьлокальная1 мин. 28 сек.
Резюме. По результатам тестов можно сделать заключение. Использование NFS сильно замедляет процесс счета. Поэтому стоит подумать об использовании на вычислительных узлах кластера локальных файловых систем для сохранения данных. Однако в этом случае вам придется перед запуском программы и после окончания счета синхронизировать данные на всех узлах, поскольку вообще говоря заранее неизвестно, на каком узле кластера запустится процесс программы с определенным номером. Другими словами файл, созданный процессом 2 на узле node005 в следующий раз может понадобиться процессу 2 на узле node007.

Следующий вывод, который можно сделать - использование асинхронной записи позволяет увеличить общее быстродействие программы, хотя и не существенно. Однако преимущество асинхронной записи будет тем заметнее, чем больше времени у вас будет занимать вычисление одного цикла итерации.


Copyright © 1998-2011 Юрий Сбитнев