В данной заметке я расскажу о своем опыте распараллеливания программы с использованием OpenMP, написанной одновременно и на С++, и на Fortran 90, причем вызов фортрановской части кода осуществляется параллельно в цикле из C++ части.  Остановлюсь в основном на тех деталях и тонкостях, которые мне показались настоящими сюрпризами. Суть программы достаточно проста: есть некая цилиндрическая структура, которая модельно разбивается на аксиальные ячейки, каждая аксиальная ячейка с использованием методов математического моделирования обсчитывается независимо. Основная часть кода написана на C++, но вот то, что нужно рассчитать для каждой аксиальной ячейки, написано на Fortran 90, и надо сказать, что эта фортрановская часть достаточно внушительная. Код испокон веков обсчитывал последовательно каждую аксиальную ячейку, и в виду того, что фортрановская часть делает объемные вычисления, код считал долго. И тут была поставлена задача – распараллелить код, т.е. считать каждую аксиальную ячейку параллельно, дабы ускорить время расчета всей программы. Была принята следующая идея: выделение памяти под массивы и т.п., необходимые для фортрановских расчетов, оставить как и прежде, т.е. там же в фортране, удобно перегруппировав их в массив объектов структуры, описывающей аксиальную ячейку, и плюсом расширив данные, а вот вызов главной функции, делающей вычисления в фортрановской части, делать в цикле по аксиальным ячейкам параллельно в C++ части, т.е. примерно так

# pragma omp parallel for
for (iaxialmesh = 0; iaxialmesh<Nmeshes; iaxialmesh++) {
	CallFortranCodeForAxMesh(iaxialmesh);
}

Здесь опущены все детали с private и т.п., необходимые в строке c # pragma. Таким образом, мы вызываем параллельно из C++ части функцию, которая написана на фортране. Эта фортрановская функция в свою очередь зовет массу других функций, в общей сложности которых несколько сотен.

Итак, главной модификацией фортрановской части было избавление от глобальных переменных, которые в большом количестве были помещены в модули. Именно в такие моменты приходит понимание, что глобальные переменные – мины замедленного действия. Фортрановская часть программы до этого работала на глобальных переменных, каждый раз перезаписывая эти глобальные переменные перед выполнением расчета для текущей аксиальной ячейки, а затем выкачивала нужные насчитанные данные. Теперь же была создана структура tAxmesh. Массив объектов данной структуры tAxmesh axmeshes(NtotAxMesh) благополучно хранит все данные для каждой аксиальной ячейки изолированно в памяти (разумеется, объем потребляемой программой оперативной памяти увеличился) и позволяет выполнять вычисления для каждой аксиальной ячейки параллельно. Дело в том, что многие массивы нужны только для выполнения расчета на текущем временном шаге, потом их можно без проблем затереть. Назовем эти массивы «рабочими». Как раз они-то и лежали в глобальной области. Теперь, если мы хотим проводить расчет сразу для всех ячеек, эти рабочие массивы должны быть созданы для каждой аксиальной ячейки, потому и вырос объем потребляемой памяти.

Теперь встает вопрос, как передавать во внутренние фортрановские функции информацию о местонахождении в памяти массивов, относящихся к той или иной аксиальной ячейке. Как вариант, можно передавать только один параметр - объект структуры для текущей ячейки axmeshes(iCurrentAxMesh).  Но тогда в большом количестве мест фортрановского кода пришлось бы дописывать к переменным somevariable приставку axmeshes(iCurrentAxMesh), т.е. axmeshes(iCurrentAxMesh)%somevariable. Для того чтобы это не делать, было принято решение передавать напрямую нужные переменные через интерфейсы. Объем интерфейсов функций в фортрановской части программы в результате этого изрядно подрос, т.к. раньше функции просто обращались за переменные в модули use SomeModule, теперь же нужно передавать указатели на массивы в тех местах памяти, которые соответствуют данной обсчитываемой аксиальной ячейке.

Первое опасение заключалось в том, что нет ли каких либо лимитов по число параметров, передаваемых в интерфейсе функции в Fortran 90. Опасения не оправдались, можно ставить хоть несколько сотен, проблем нет. Второе опасение заключалось в том, что не вылезем ли мы за пределы памяти, доступной для текущего вычислительного потока. Здесь также, проблем не возникло, памяти, используемой потоком по умолчанию, с лихвой хватало для вычислений достаточно объемной программы.

Настоящим сюрпризом оказалась следующая деталь. Когда я захожу в управляющую функцию в фортране с определенным id аксиальной ячейки CallFortranCodeForAxMesh(iaxialmesh), далее мне необходимо передать информацию о месте памяти, где лежит все относящееся к данной ячейке. Логично сделать это через указатель, но такой код работает некорректно!

! код, работающий некорректно
subroutine CallFortranCodeForAxMesh(iaxialmesh) bind (C, name = “CallFortranCodeForAxMesh”)
use AxMeshModule ! модуль, где дано описание структуры tAxmesh
integer(c_int), intent(in) :: iaxialmesh
type (tAxmesh), pointer :: paxmesh => NULL()
paxmesh => axmeshes(iaxialmesh)
call AxMeshComputation(paxmexh)
end CallFortranCodeForAxMesh

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

Эта проблема была решена прямой передачей объекта массива:

! корректный код
subroutine CallFortranCodeForAxMesh(iaxialmesh) bind (C, name = “CallFortranCodeForAxMesh”)
use AxMeshModule ! модуль, где дано описание структуры tAxmesh
integer(c_int), intent(in) :: iaxialmesh
call AxMeshComputation(axmeshes(iaxialmesh))
end CallFortranCodeForAxMesh

С такой реализацией потоки разбирают четко отдельные области памяти, относящиеся к различным аксиальным ячейкам.

Следует отметить, что когда мы находимся во внутренних участках фортрановского кода и считаем конкретную аксиальную ячейку, уже находясь в изолированной области памяти, относящейся к заданной аксиальной ячейке, здесь уже передача по указателю, каких-либо объектов частных структур axmeshes(iaxialmesh)%someStructureObj не вызывает проблем.

Следующий сюрприз поджидал при инициализации данных. Для того чтобы зачитать входной файл был создан массив объектов некоторой частной структуры structObj, лежащий в глобальной области, а затем эти данные раскидывались по соответствующим местам, относящимся к различным аксиальным ячейкам axmeshes(i)%structObj. Изначально это было так:

! копируются только указатели!
axmeshes(i)%structObj(j) = structObj(j)

Компилятор вновь начал выдавать конфликт по памяти при параллельной работе потоков, та самая гонка, или race condition. Оказалось, что данное копирование происходит только для указателей, т.е. указатель на массив structObj(j) был скопирован в объект каждой аксиальной ячейки. Разумеется, все потоки начинали работать с одним и тем же местом в памяти. Для того, чтобы это разрешить, необходимо полностью прописать то, что мы хотим скопировать, а именно содержимое структур, т.е.:

! копируем содержимое
axmeshes(i)%structObj(j)%var1 = structObj(j) )%var1
axmeshes(i)%structObj(j)%var2 = structObj(j) )%var1
axmeshes(i)%structObj(j)%var3 = structObj(j) )%var1
axmeshes(i)%structObj(j)%var4 = structObj(j) )%var1

Сколько бы переменных не было, их нужно перечислить все.

Сборка программы на Linux с компиляторами gcc и gfortran версии 11 не вызывала никаких проблем. Но вот на Windows с использованием Visual Studio 2017 и компилятором фортран Intel Parallel Studio XE 2019 Update5 Composer код собирался (с установкой всех нужных флагов подключения OpenMP в настройках), но не считал. Причина оказалась в конфликте версий OpenMP: в компиляторе C++ в Visual Studio 2017 стоит версия OpenMP 2.0, в упомянутом компиляторе фортрана - OpenMP 5.0. Однако, если компилятор Parallel Studio XE 2019 Update5 Composer заменить на компилятор фортрана на OneApi 2022, то проблема конфликта решается, - код считает параллельно.

Итак основные выводы:

  • при параллельном запуске фортрановской функции следует в интерфейсе напрямую указывать объект структуры, а не указатель на этот объект

  • приравнивание объекта структуры другому объекту структуры в фортране делает копирование указателей, а не содержимого структур

  • программа, написанная на C++ и Fortran 90 не будет работать в Visual Studio 2017 с компилятором фортрана Parallel Studio XE 2019 - имеет место быть конфликт версий OpenMP в компиляторах разных языков. Следует ставить либо Visual Studio 2017 + компилятор фортран OneApi 2022, или Visual Studio 2019 и выше с компилятором фортрана OneApi 2024.

Комментарии (8)


  1. rukhi7
    21.01.2025 14:32

    все хорошо, все понравилось, хочу поделиться каламбуром, который навеяло:

    мне кажется это все таки не тонкости, это толстости :) в основном.


  1. vadimr
    21.01.2025 14:32

    Вроде для совместимости с Си в Фортране надо использовать type(c_ptr), а не pointer.

    Но у меня другой вопрос - не проще было распараллелить фортрановскую программу, чем сишную? Тем более пользуетесь интеловским компилятором, который умеет автопараллелизацию.


    1. adeshere
      21.01.2025 14:32

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

      Как известно, в фортране есть атрибут save, который позволяет отвести для локального объекта постоянную область памяти. То есть, область видимости у него локальная, но память глобальная. Иногда это немного ускоряет работу (не надо заново выделять память при каждом вызове функции), а кроме того, этот атрибут гарантирует, что значение такого объекта сохраняется между вызовами. Например,

      вот такой код будет вычислять число вызовов функции MyFunc.

      Поясню для не-фортранистов, как это работает: COUNTER инициализируется один раз при старте программы, а затем при каждом входе в MyFunc увеличивается на 1:

      function MyFunc(...)
      integer, SAVE ::   COUNTER =0
      COUNTER=COUNTER+1
      ! Теперь при первом вызове функции COUNTER=1, 
      ! при втором  вызове  COUNTER=2, и т.д. 
      ! А вот что будет при распараллеливании, я не знаю
      (...)
      end

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

      Но как на самом деле поступает в таких случаях интел-компилятор фортрана, я не знаю. У меня самого до сих пор компилятор 2008г, так как перейти на более новую версию

      я не смог

      легально это теперь нельзя, а обновиться "неофициально" я не сумел


      1. omxela
        21.01.2025 14:32

        я не смог

        Так и бог с ним. Интелевский фортран, конечно, хорош (в основном, своим оптимизатором). Что касается параллельных вычислений, то спецификации OpenMP вполне достаточно, а её поддерживает и Интель- и ГНУ-фортран. Я сижу в IDE Simly Fortran, хотя раньше тоже использовал Интель. Численные задачки решаются вполне себе нормально, даже мухобойные типа электродинамических FDTD.


    1. kbtsiberkin
      21.01.2025 14:32

      Автопараллелизация, помнится, всё же не очень предсказуема. В моём опыте получалось, что написанный самостоятельно OpenMP может и не побыстрее, но постабильнее. В простом расчётном коде, когда себя для контроля происходящего хватает уж точно. AutoParallelization иной раз вроде и создаёт все потоки в должном количестве, и ядра загружены, а ускорения особо не заметно — где‑то упирается в обмен данными и кирдык.

      Надо бы провести количественное тестирование, наверное.


  1. adeshere
    21.01.2025 14:32

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

    Тут не очень понятно: а зачем указатель-то? Фортран все равно по умолчанию передает в функцию адрес объекта. В отличие от некоторых других языков, ничего никуда не копируется ;-) Проще говоря, никаких накладных расходов, как при передаче параметров по значению, нет. Поэтому обычный call никаких трюков с указателями не требует (что очень удобно для всяких тупиц вроде меня, которые только на этом языке и могут что-то писать ;-) А указатели в фортран добавлены не для этого, а для более изощренных хитростей ;-).

    Короче, если у Вас есть глобальный массив объектов axmes(N), то в фортране обычно этот самый массив напрямую в функцию и передается. Или его элемент. Как Вы и сделали чуть ниже:

    call AxMeshComputation(axmeshes(iaxialmesh))

    Это как бы стандарт


  1. adeshere
    21.01.2025 14:32

    Для того, чтобы это разрешить, необходимо полностью прописать то, что мы хотим скопировать, а именно содержимое структур, т.е. (...) ...Сколько бы переменных не было, их нужно перечислить все.

    приравнивание объекта структуры другому объекту структуры в фортране делает копирование указателей, а не содержимого структур

    А вот это явно какой-то баг. Все мои олдскулы говорят, что должна копироваться структура. Больше того, я сам сплошь и рядом использую именно такое копирование, которое у Вас не работает. Ни разу нигде не писал все переменные (элементы подструктуры) полным списком. Да и наверно не смог бы: у меня местами присутствует 4-уровневая иерархия структур. Боюсь даже подумать, как бы я в такой ситуации копировал 2-3-й уровень Вашим методом. И все работает штатно (но у меня IVF 2008).

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

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


    1. vadimr
      21.01.2025 14:32

      Согласен, здесь неоткуда взяться указателям. Скорее всего, переменные описаны как-то криво.