company_banner

Знакомство с Coarray Fortran: будем параллельны?

    Очень давно хотел написать о том, на какой стадии сейчас находится один из «прародителей» популярных сегодня языков программирования. Да, я говорю о Фортране. Попытаюсь разрушить стереотип, который существует в умах многих разработчиков – что Фортран древний язык без каких либо перспектив, и уж точно никто на нём уже не пишет. Наоборот, он очень активно эволюционирует и уже давно предлагает богатый набор различных функциональностей, закреплённых в стандартах, вполне, кстати, сопоставимых с тем же С/С++.
    От «старого» 77-го Фортрана осталось уже не так и много… в 95 стандарте мы могли вполне себе создавать свои типы данных, динамически выделять и очищать память, работать с указателями, перегружать функции и операторы и много чего ещё. По сути, от С своим набором средств он отличается несильно. Тем не менее, я не хочу пытаться сравнивать языки – это вопрос философии. Скажу только, что фортрановский компилятор Intel пользуется очень большим спросом, и, собственно, приобретается даже активнее, чем тот же С++.
    Цель же данного поста – рассказать о текущем состоянии дел. Собственно, Фортран на сегодняшний день — параллельный язык, и стал он таковым после принятия стандарта Fortran 2008, в котором появились Coarray’и.

    Итак, обо всё по порядку. За основу была взята модель программирования SPMD (Single Program MultipleData). Если вы знакомы с MPI, то суть та же – мы пишем наше приложение, копии которого будут выполняться определенное количество раз параллельно. При этом у каждой копии имеются свои локальные данные. Те данные, к которым необходим доступ из разных копий, описываются с помощью специального синтаксиса, именуемого как Coarray.

    Для понимания достаточно привести простой Hello World пример:

    program hello
    write(*,*) "Hello world"
    end program hello
    


    Собственно, самый обычный код. Вот только скомпилировав его с ключиком –coarray (компилятором Intel), мы увидим «приветы» из нескольких разных копий программы, или, в терминах Coarray, из разных Image’ей (образов). Причём их число можно контролировать, например, через ключ –coarray-num-images=x, или переменную окружения FOR_COARRAY_NUM_IMAGES. Понятно, что существует способ определять, в каком образе происходит выполнение. Усложним наш пример:

    program hello_image
    write(*,*) "Hello from image ", this_image(), "out of ", num_images()," total images“
    end program hello_image


    После запуска мы увидим что-то похожее на это:

    Hello from image            1 out of            4  total images
    Hello from image            4 out of            4  total images
    Hello from image            2 out of            4  total images
    Hello from image            3 out of            4  total images
    


    Очевидно, что наше приложение было выполнено 4 раза (4 копии/образа). Имея эти данные о Coarray, мы в принципе, уже способны создавать параллельные приложения.

    Вот только весьма бестолковые, потому что нет ответа на главный вопрос – как же быть с данными? Для этого вводят очень простой и понятный синтаксис:

    real, codimension[*] :: x
    real :: y[*]


    Квадратные скобки говорят нам о том, что мы используем Сoarray.
    В данном примере это просто скаляры, которые всё так же доступны в каждой копии программы. Но теперь мы можем обратиться к значению этого скаляра в нужной нам копии (образе).

    Например, написав y[2] мы обратимся к значению y в образе 2. Это открывает нам возможности для «настоящей» параллельной работы с данными.

    Естественно, существует ряд логичный ограничений, накладываемых на Сoarray’и, как, например, любая попытка связать объект Сoarray с другим объектом через указатели, или передача объектов Сoarray в С код.
    Давайте рассмотрим ещё несколько примеров, считая, что мы уже объявили ранее переменную x как Сoarray:

    x = 42.0


    В данном случае, мы оперируем с локальной для образа переменной х.
    Как только в нашем коде появляются квадратные скобки – это явный указатель на то, что происходит доступ с переменной в другом образе программы:

    x[3] = 42.0  ! задаёт значение х равное 42 в образе 3
    x = x[1]  ! присваиваем локальной для текущего образа переменной х значение из образа 1
    x[i] = x[j]  !присваиваем переменной х в образе I значение переменной х из образа j


    Что хорошо в Coarray, так это то, что в отличие от чистого MPI, мы не заботимся о посылке или приеме сообщений. Всё это на плечах реализации (которая уже и использует тот же MPI). Но мы «над» этим. Кроме того, код будет работать как на системах с распределённой памятью, так и на системах с общей. Достаточно только поменять ключ на coarray=shared или coarray=distributed.

    Раз уж у нас появились данные в разных копиях нашей программы, логично предположить, что должны быть и средства для их синхронизации. Конечно, они имеются. Это, например, конструкция SYNC ALL, синхронизирующая все образы. Есть ещё и SYNC IMAGES(), позволяющая синхронизировать только определенные образы.

    Ещё один пример:

    integer, codimension[*] :: fact
    integer :: i, factorial
    fact = this_image() 
    SYNC ALL           
    if ( this_image() == 1 ) then
       factorial = 1
       do i = 1, num_images()
         factorial = factorial * fact[i]
       end do
       write(*, *) num_images(), 'factorial is ', factorial
    end if


    Естественно, не самый быстрый способ посчитать факториал, зато хорошо иллюстрирующий суть работы с Сoarray.

    Для начала, объявляем fact как Сoarray, и далее в каждом образе мы присваиваем значение, равное номеру образа. Перед тем, как мы перемножим все значения, нужно убедиться, что они уже присвоены, поэтому используем SYNC ALL. И в образе с номером 1, таком якобы «мастер-образе», вычисляем факториал.

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

    Я же заканчиваю своё краткое описание относительно «новых плюшек» из последнего принятого стандарта. Надеюсь, было не очень скучно смотреть Фортрановский код. Если же отзывы покажут обратное и проснётся живой интерес по данной теме, то я не откажу вам в удовольствии и продолжу тему. А сейчас всем большое спасибо за внимание.
    • +29
    • 10,6k
    • 2

    Intel

    147,00

    Компания

    Поделиться публикацией
    Комментарии 2
      +6
      Обеспечивается ли атомарность записи? Например, если выполнить код для четырех image'ей:
      integer, codimension[*] :: fact
      do x = 1, 1000
          fact[1] = fact[1] + 1
      end do
      


      Будет ли в результате fact[1] = 4000? Или же это будет случайное число меньше 4000, зависящее от приоритетов потоков?
        +4
        Спасибо за отличный вопрос! Нет, результат будет рандомный, атомарности нет. Кстати, приоритетов потоков тоже нет. Здесь речь о MPI процессах — копиях приложения. Для получения 4000, придётся самому вводить критическую секцию (тоже часть Coarray), примерно так:

        critical
            do x = 1, 1000
              fact[1] = fact[1] + 1
            end do
        end critical
        sync all
        

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое