Вы находитесь на странице: 1из 319

Арьен Маркус

Современный
Fortran
на практике
Modern Fortran
in Practice

Arjen Markus
with Foreword by Michael Metcalf
Современный Fortran
на практике

Арьен Маркус
с предисловием Майкла Меткалфа

Москва, 2015
УДК 004.438Fortran
ББК 32.973.22
M27

V27 Арьен Маркус


Современный Fortran на практике / пер. с англ. Снастин А. В. –
М.: ДМК Пресс, 2015. – 308 с.: ил.
ISBN 978-5-97060-302-4
Язык программирования Fortran изначально был предназначен для
математических вычислений с максимальной производительностью.
В последний стандарт Fortran 2008 включено множество современных
функциональных возможностей: средства объектно-ориентированного
программирования, специализированные операции с массивами, типы,
определяемые пользователем и поддержка параллельных вычислений.
Данное учебное руководство поможет программистам на языке
Fortran научиться применять все вышеперечисленные функциональ-
ные возможности в соответствии с современными требованиями:
модульность, лаконичность, объектно-ориентированный подход и
рациональное использование ресурсов, а также организация работы
с учётом наличия нескольких процессоров. В книге рассматриваются
практические примеры взаимодействия с программами, написанными
на языке C, управления памятью, применения графики и графических
пользовательских интерфейсов, параллельные вычисления с использо-
ванием библиотек MPI, OpenMP и комассивов (coarrays). Кроме того,
автор анализирует некоторые числовые алгоритмы и их реализации, а
также показывает, как можно применить некоторые библиотеки с от-
крытыми исходными кодами.

Original English language edition published by Cambridge University Press, 32


Avenue of the Americas, New York, NY 10013-2473, USA. © Arjen Markus 2012.
Russian-language edition copyright © 2015 by DMK Press. All rights reserved.

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

ISBN 978-1-107-01790-0 (англ.) © Arjen Markus, 2012


ISBN 978-5-97060-302-4 (рус.) © Оформление, перевод на русский язык
ДМК Пресс, 2015
«Eadem mutata resurgo»
(вольный перевод: «изменяясь, я вновь воскресаю»)
Надпись на могиле Якоба Бернулли,
изначально относящаяся к логарифмической спирали

Посвящается памяти моего отца.

Мои родители научили меня любознательности.


Моя жена и мои дети всё время учат меня другим
важным вещам.
оглавление

Предисловие Майкла Меткалфа.......................... 11


Предисловие автора.......................................... 15
Глава 1. Введение в современный Fortran.............. 17
1.1. Особенности современного Fortran................................... 17
1.2. Fortran 90........................................................................... 21
1.3. Fortran 95........................................................................... 25
1.4. Fortran 2003....................................................................... 26
1.5. Fortran 2008....................................................................... 28
1.6. Что осталось неизменным.................................................. 29
Глава 2. Функции для работы с массивами............. 32
2.1. Передача массивов в аргументах....................................... 33
Производительность при использовании функций обработки
массивов.........................................................................................35
2.2. Элементные функции и автоматическое
перераспределение памяти..................................................... 36
2.3. Два более сложных примера.............................................. 38
Дистанционирование иррациональных чисел..................................38
Быстрая сортировка QuickSort.........................................................40
2.4. Компактный стиль.............................................................. 41

Глава 3. Математические абстракции................... 44


3.1. Автоматическое дифференцирование............................... 44
Проблемы при вычислениях............................................................49
3.2. Дискретное программирование......................................... 50
Управление памятью.......................................................................51
3.3. Перечислимое множество решений Диофантовых
уравнений................................................................................ 53
3.4. Отложенные или ленивые вычисления............................... 56
Оглавление 7

Глава 4. Управление памятью.............................. 60


4.1. Динамически изменяемые массивы................................... 60
4.2. Утечки памяти при использовании указателей................... 61
4.3. Увеличение размера массива............................................ 62
4.4. Строки символов с изменяемой длиной............................. 63
4.5. Сочетание автоматических и динамических массивов....... 66
4.6. Производительность массивов разных типов..................... 67
4.7. Параметризованные производные типы............................ 69
4.8. Утечки памяти в производных типах................................. 71
4.9. Производительность и доступ к памяти.............................. 76
Глава 5. Проблема интерфейса............................ 80
5.1. Подстановка параметров................................................... 82
5.2. Использование пула данных............................................... 84
Данные в модулях............................................................................85
Внутренние подпрограммы.............................................................88
5.3. Передача дополнительных аргументов.............................. 89
Массив параметров.........................................................................89
Использование функции transfer()...................................................90
Процедуры, связанные с типом.......................................................91
Указатели на процедуры..................................................................93
5.4. Управляющие конструкции................................................ 95
Библиотека OpenMP...................................................................... 100
5.5. Работа с числовыми значениями различной точности...... 102
5.6. Резюме............................................................................ 103
Глава 6. Взаимодействие с программами
на языке C на примере работы с СУБД SQLite. ...... 105
6.1. Соответствие типов данных............................................. 106
6.2. Передача аргументов между подпрограммами,
написанными на C и на Fortran................................................ 109
6.3. Соглашения об именовании и вызовах функций............... 110
6.4. Работа с производными типами....................................... 113
6.5. Создание интерфейса к СУБД SQLite............................... 116
Глава 7. Графика, GUI и Интернет. ...................... 124
7.1. Вывод результатов в графическом виде........................... 125
7.2. Графические пользовательские интерфейсы (GUI).......... 131
8 Оглавление

7.3. Интернет.......................................................................... 139


7.4. Работа с XML-файлами.................................................... 143
Глава 8. Модульное тестирование...................... 148
8.1. Инструментальные средства тестирования...................... 148
8.2. Пример: обработка трёхдиагональной матрицы............... 149
8.3. Проектирование и реализация......................................... 153
8.4. Заключительные замечания............................................. 155
Глава 9. Просмотр и рецензирование исходного
кода.............................................................. 157
9.1. Соблюдать определённость и однозначность.................. 158
Используйте явные объявления переменных и констант................ 158
Используйте предусловия............................................................. 160
Переменные, сохраняющие свои значения между вызовами......... 161
Видимость интерфейса к подпрограмме или функции................... 161
Доступность переменных и подпрограмм...................................... 161
Вариант default в блоке select и ветвь else в блоке if....................... 162
Информативные сообщения об ошибках....................................... 162
9.2. Избегать излишней сложности и запутанности................ 164
9.3. Избегать «ловушек»......................................................... 169
Правильная обработка ошибок...................................................... 169
Сравнение вещественных чисел.................................................... 169
Смешанная точность..................................................................... 171
Неожиданные результаты при работе с отрицательными
числами......................................................................................... 171
Автоматические массивы.............................................................. 172
Ошибки могут возникать не только при работе с числами.............. 173
9.4. Писать простой и понятный код....................................... 173
Глава 10. Устойчивая к ошибкам реализация
нескольких простых алгоритмов........................ 178
10.1. Обзор существующих подобных методик....................... 179
10.2. Линейная интерполяция................................................. 181
10.3. Простые статистические методы и характеристики........ 187
10.4. Поиск корней уравнения................................................ 195
Глава 11. Объектно-ориентированное
программирование.......................................... 209
11.1. Расширение типов и процедуры, связанные с типами.... 209
Передача объекта в другом аргументе........................................... 211
Оглавление 9

Расширение до трёх измерений.................................................... 212


Пример: случайные перемещения в двух и в трёх измерениях....... 215
Определение динамического типа................................................. 217
Наблюдение за частицами............................................................. 217
11.2. Интерфейсы как контракты............................................ 221
Аппроксимация множественного наследования............................ 225
11.3. Использование прототипирования................................. 226
Пример: моделирование поведения рыб....................................... 229
11.4. Абстрактные типы данных и обобщённое
программирование................................................................ 232
11.5. Изменение поведения типа данных................................ 235
11.6. Шаблоны проектирования.............................................. 237
Шаблон проектирования Factory................................................... 238
Шаблон проектирования Наблюдатель.......................................... 241

Глава 12. Параллельное программирование........ 245


12.1. Простые числа............................................................... 246
Библиотека OpenMP...................................................................... 248
Интерфейс MPI.............................................................................. 251
Комассивы.................................................................................... 255
12.2. Декомпозиция по доменам............................................ 259
OpenMP......................................................................................... 261
MPI................................................................................................ 265
Комассивы.................................................................................... 266
12.3. Другие методики параллельного программирования..... 268
12.4. Резюме.......................................................................... 270
Приложение А. Инструментальные средства для
разработки и сопровождения............................ 271
А.1. Компиляторы................................................................... 271
А.2. Средства сборки программ............................................. 272
А.3. Интегрированные среды разработки............................... 275
А.4. Средства проверки во время выполнения........................ 276
А.5. Системы управления версиями....................................... 278
А.6. Документирование исходного кода.................................. 279
А.7. Охват кода тестированием и статический анализ............. 281
Приложение Б. Некоторые нюансы
использования Fortran...................................... 285
10 Оглавление

Б.1. Особенности стандарта................................................... 285


Вычисление логических выражений по короткой схеме................. 285
Сохранение значений локальных переменных............................... 286
Ещё об инициализации.................................................................. 287
Двойная точность и вычисление правой части выражений............. 287
Передача одного и того же аргумента дважды............................... 288
REAL(4).......................................................................................... 290
Признак конца файла (EOF), вывод на экран и т. п.......................... 290
Внешние и внутренние (встроенные) подпрограммы..................... 291
Несовпадения в интерфейсах: предполагаемая форма и явная
форма массивов............................................................................ 292
Инициализация генератора случайных чисел................................ 293
Открытие одного и того же файла дважды..................................... 294
Б.2. Массивы.......................................................................... 294
Использование автоматических и временных массивов может
привести к переполнению стека.................................................... 294
Границы массивов с начальным индексом меньше 1..................... 296
Объявления массивов: dimension(:) и dimension(*)........................ 296
Б.3. Динамические библиотеки.............................................. 297
Открытие файла в программе и использование его в DLL
и наоборот..................................................................................... 297
Выделение памяти в DLL и освобождение этой памяти
в программе и наоборот................................................................ 298
Аргументы командной строки недоступны в DLL............................ 298
Подпрограммы или данные из основной программы,
используемые в DLL...................................................................... 298

Приложение В. Зарегистрированные товарные


знаки, упоминаемые в данной книге...................... 300
список литературы.......................................... 302
Предметный указатель..................................... 309
Предисловие
Майкла Меткалфа

Эпоха, когда прикладные программы создавались на языке Fortran,


почти полностью совпадает с периодом существования компьюте-
ров общего назначения. Это удивительный факт, и с учётом того, что
многие другие языки программирования высокого уровня прекрати-
ли своё существование, трудно понять, почему получилось именно
так. Возможно, исходные принципы проектирования Джона Бэкуса
(John Backus) – простота использования и эффективность выполне-
ния – стали двумя решающими факторами. Возможно, сыграла роль
преданность языку Fortran сообщества его пользователей, которые
всегда старались не отставать от новейших разработок в области тех-
нологии программирования и адаптировать язык к постоянно расши-
ряющемуся кругу требований.
В течение нескольких десятилетий Fortran считался вымирающим
языком, но, несмотря на все предсказания, оказался на удивление
живучим. Более того, в последние годы возобновилась его стандар-
тизация, и последний стандарт Fortran 2008 должен снова продлить
жизнь этому языку. С учётом этих нововведений очень жаль, что
продолжают существовать старые версии Fortran, как в форме дав-
но устаревших курсов, читаемых неисправимо упрямыми препо-
давателями, так и в виде вышедших из употребления концепций, о
которых постоянно твердят его критики. Современный Fortran – это
процедурный, императивный, компилируемый язык с синтаксисом,
соответствующим точному представлению математических формул.
Независимые процедуры могут компилироваться отдельно или объ-
единяться в модули, что упрощает создание крупномасштабных про-
грамм и библиотек процедур. В язык включены функциональные
возможности для обработки массивов, абстрактные типы данных, ди-
намические структуры данных, средства объектно-ориентированного
программирования и параллельной обработки. Fortran способен без
затруднений взаимодействовать с C. Таким образом, современный
12 Предисловие

Fortran, начиная с версии Fortran 95 (так теперь стали обозначать-


ся версии стандарта) – это мощный инструмент. Он в полной мере
поддерживает структурное программирование, а средства объектно-
ориентированного программирования, появившиеся в стандарте
Fortran 2003, стали самым значительным усовершенствованием язы-
ка, его главным нововведением. Большинство из упомянутых новых
функциональных возможностей описано в данной книге.
Но ни один стандарт до Fortran 2003 включительно не содержал
никаких средств, специально предназначенных для поддержки парал-
лельного программирования. Такая поддержка осуществлялась опос-
редованно, с привлечением вспомогательных стандартов, в частности
HPF (High-Performance Fortran), MPI (Message Passing Interface),
OpenMP и Posix Threads (Pthreads). Использование библиотек MPI
и OpenMP стало массовым явлением, но HPF в конечном счёте не
имел особого успеха. Сейчас, после принятия стандарта Fortran 2008
одним из самых сильных свойств современного языка Fortran являет-
ся непосредственная поддержка параллельного программирования,
благодаря введению чрезвычайно востребованного средства: комас-
сивов (coarrays).
Директивы HPF имели форму строк комментариев и распозна-
вались только HPF-процессором. Например, существовала воз-
можность выравнивания трёх совпадающих по форме массивов по
четвёртому с обеспечением локальности ссылок. Другие директивы
позволяли распределить обработку выравниваемых массивов по не-
скольким процессорам. С другой стороны, MPI представляет собой
универсальную библиотеку процедур для передачи сообщений, а
библиотека OpenMP, поддерживающая независимое от платфор-
мы параллельное программирование с совместным использованием
памяти, состоит из набора директив компилятора, библиотечных
подпрограмм и переменных среды, которые определяют поведение
программы во время выполнения. Posix Threads – это стандарт, опре-
деляющий спецификацию библиотеки для поддержки многопоточ-
ности.
В отличие от всех перечисленных средств, главной целью введения
комассивов является предоставление синтаксиса, минимально вли-
яющего на внешний вид программы и позволяющего распределить
по нескольким процессорам не только данные, как в модели «одна
инструкция, много данных» (Single Instruction Multiple Data, SIMD),
но и работу в соответствии с моделью «одна программа, много дан-
ных» (Single Program Multiple Data, SPMD). От программиста требу-
Предисловие 13

ется знание лишь небольшого набора новых правил. Работа с комас-


сивами – это самое важное новшество в стандарте Fortran 2008, но
кроме него была введена новая форма управляющей конструкции do
concurrent как способ распараллеливания циклов. Вполне очевид-
но, что появилась возможность обеспечения полноценного режима
параллельного выполнения, не выходя за рамки языка. Всё это также
рассматривается и сравнивается в данной книге.
Другие важные нововведения в стандарте Fortran 2008: подмодули
(submodules), более удобный доступ к объектам данных, усовершен-
ствованные средства ввода/вывода и управления выполнением, до-
полнительные внутренние процедуры, в частности, для работы с би-
тами. Fortran 2008 был опубликован в 2010 году, и в настоящее время
является действующим стандартом. Будущее языка Fortran опреде-
ляет его способность обеспечить высокую производительность вы-
числений, таким образом, комассивы становятся важнейшим инстру-
ментом языка.
Но языку программирования трудно выжить, если о нём мало что
известно. Причём должны существовать не только учебники по его
синтаксису и семантике, но и книги о практическом применении язы-
ка для решения реальных задач. Опыт работы, конкретные методики,
а также способы наиболее оптимального использования новых функ-
циональных возможностей должны передаваться новому поколению
программистов. В наше время, когда языки программирования не яв-
ляются «вещью в себе», а всё чаще используются совместно друг с
другом или в сочетании с разнообразными инструментальными сред-
ствами, необходима именно такая книга как «Современный Fortran
на практике».
Автор этой книги постоянно сотрудничает с информационным
бюллетенем ACM Fortran Forum и является активным участником
группы новостей comp.lang.fortran, где публикует множество полез-
ных советов. Его опыт научного программирования приносит пользу
сообществу не только в Нидерландах, где он проживает, но и во всём
мире, а статьи по обобщённому программированию и использованию
шаблонов проектирования в Fortran содержат много свежих идей. Та-
ким образом, квалификация автора вполне позволяет ему написать
книгу, подобную этой.
Но «Современный Fortran на практике» – это не просто сборник
предыдущих публикаций. Она содержит логически связное изло-
жение основ, и собственные материалы автора по параллельному
программированию на Fortran с использованием библиотек MPI,
14 Предисловие

OpenMP и комассивов (в кратком изложении), а также описывает


использование Fortran для создания графических приложений и для
взаимодействия с графическими пользовательскими интерфейсами.
В книге много подробных примеров кода, на которые читатель может
опираться при создании собственных программ.
Эта книга весьма полезна, и я рекомендую её всем программистам,
использующим Fortran. Все мы уже убедились в том, что Fortran про-
должает существовать и у него есть будущее.

Майкл Меткалф (Michael Metcalf)


Токио, октябрь 2011 г.
Предисловие автора

Я программирую на языке Fortran уже более 25 лет, сначала на


FORTRAN IV, немного позже на FORTRAN 77. В последнее де-
сятилетие прошлого века я и несколько моих коллег прослушали
курс по Fortran 90, прочитанный Яном ван Оостервийком (Jan van
Oosterwijk) в Техническом университете Делфта. Приблизительно в
это же время я присоединился к группе новостей comp.lang.fortran, и
многому научился в этом дружелюбном сообществе.
В определённом смысле я обычный программист на языке Fortran.
Моя основная специальность – физика, а программирование я на-
чал осваивать ещё во время учёбы, но большую часть практического
опыта приобрёл во время работы. Я стал программистом по произ-
водственной необходимости, а не из-за особого интереса к всё более
изощрённым возможностям программирования вообще и к перспек-
тивам их применения в Fortran. Позже я начал писать статьи для ин-
формационного бюллетеня ACM Fortran Forum, которые стали от-
правной точкой для данной книги.
Эта книга не научит вас программированию на языке Fortran. Для
этого существует множество специальных учебников ([22], [65]).
Цель данной книги – показать, как можно использовать современ-
ный Fortran для решения задач, существующих в настоящее время,
продемонстрировать, например, что методики, широко распростра-
нённые в мире объектно-ориентированных языков, таких как Java и
C++, вполне применимы в последней версии Fortran. Более того, в
книге описываются некоторые приемы решения задач программи-
рования, которые не так-то просто реализовать с помощью других
языков.
Если вы знакомы с языком в основном по старым его версиям,
существовавшим до появления Fortran 90, несколько первых глав
постепенно познакомят вас с операциями над массивами, с перегру-
жаемыми операциями и с некоторыми другими функциональными
возможностями, введёнными в этом стандарте. Кроме того, вы уви-
дите, что при использовании Fortran появилась возможность приме-
16 Предисловие

нения совершенно различных стилей программирования, присущих


в основном функциональным языкам программирования. В большин-
стве глав показано, как применять на практике все эти возможности
языка.
В данной книге часто встречаются ссылки на программы, которые
я написал и опубликовал на веб-сайте SourceForge, или на програм-
мы, к созданию которых я причастен в той или иной степени. Не счи-
тайте это рекламой конкретного программного обеспечения или на-
вязыванием своего мнения – это просто моя уверенность в хорошем
качестве упоминаемых программ. Во всех примерах, код которых был
написан не мной, я старался указывать авторов, но я всего лишь обыч-
ный человек, и мог забыть пару имён.
Книги, подобные этой, практически невозможно написать в пол-
ной изоляции. Я благодарен Майклу Меткалфу (Michael Metcalf)
и Яну Чиверсу (Ian Chivers), редакторам бюллетеня ACM Fortran
Forum, всем участникам группы новостей comp.lang.fortran, а также
Билу Клебу (Bil Kleb), Полу ван Делсту (Paul van Delst), Рольфу Аде
(Rolf Ade), Генри Гарднеру (Henry Gardner), Саймону Гирду (Simon
Geard), Рихарду Захенвирту (Richard Suchenwirth), Дэниелу Краф-
ту (Daniel Kraft), Рихарду Майне (Richard Maine), Стиву Лайонелу
(Steve Lionel), Кэмерону Лэйрду (Cameron Laird) и Клифу Флинту
(Clif Flynt). Спасибо им за обсуждения и обзоры моих работ, за дис-
куссии по различным аспектам программирования на языке Fortran и
программирования в целом, за уделённое мне внимание.
У этой книги есть свой веб-сайт http://flibs.sf.net/examples_
modern_fortran.html, где размещены полные исходные коды всех
примеров. Я протестировал их с помощью компиляторов gfortran и
Intel Fortran главным образом в ОС Windows, но проверял и в Linux.
Некоторые из этих программ используют самые последние нововве-
дения в стандарте Fortran, поэтому вам потребуется наиболее свежая
версия компилятора.

Ариен Маркус (Arjen Markus)


Роттердам, ноябрь 2011 г.
Глава 1.
Введение в современный
Fortran

С момента публикации стандарта FORTRAN 77 в 1978 году язык


программирования Fortran претерпел множество изменений [61].1
Вносимые изменения учитывали как новые подходы к методикам
программирования, так и новые разработки в области компьютерной
аппаратуры. Язык изначально был задуман как максимально эффек-
тивный инструмент вычислений. В самом последнем на момент напи-
сания этой книги стандарте, Fortran 2008, введена прямая поддержка
параллельной обработки средствами самого языка, то есть основная
идея эффективности получила дальнейшее развитие [71].
В этой главе приводится общий обзор различных стандартов, по-
явившихся после FORTRAN 77. Не следует считать это попыткой
полного описания всех вводимых стандартами новых функциональ-
ных возможностей, для этого потребовалась бы отдельная книга или
даже несколько книг. Более подробные описания версий стандартов
можно найти в книгах Меткалфа (Metcalf) [63], [65] или Брэйнерда
(Brainerd) и др. [36].

1.1. Особенности современного


Fortran
Стандарт Fortran 90 ввёл весьма существенные изменения по срав-
нению с повсеместно распространённым стандартом FORTRAN 77:
свободный формат исходного кода, операции с массивами, модули,
производные типы и т. п. Чтобы понять, что это значило для програм-
1
Формально в официальных документах наименование этой версии стандарта долж-
но быть записано прописными буквами: FORTRAN 77. Начиная с версии стандарта
Fortran 90, наименование языка пишется строчными буквами (буквами в нижнем
регистре).
18 Глава 1. Введение в современный Fortran

миста, рассмотрим простую задачу: имеется файл с набором чисел,


в каждой строке содержится одно значение (для простоты), необхо-
димо построить простую гистограмму, показывающую распределение
этих чисел. Программа, решающая такую задачу на FORTRAN  77,
могла выглядеть следующим образом:2
*
* Построение простой гистограммы.
*

PROGRAM HIST

INTEGER MAXDATA
PARAMETER (MAXDATA = 1000)
INTEGER NOBND
PARAMETER (NOBND = 9)
REAL BOUND(NOBND)
REAL DATA(MAXDATA)
INTEGER I, NODATA

DATA BOUND /0.1, 0.3, 1.0, 3.0, 10.0, 30.0,


& 100.0, 300.0, 1000.0/

OPEN( 10, FILE = 'histogram.data', STATUS = 'OLD', ERR = 900 )


OPEN( 20, FILE = 'histogram.out' )

DO 110 I = 1,MAXDATA
READ( 10, *, END = 120, ERR = 900 ) DATA(I)
110 CONTINUE
*
120 CONTINUE
CLOSE( 10 )
NODATA = I - 1

CALL PRHIST( DATA, NODATA, BOUND, NOBND )


STOP
*
* Файл не найден и другие ошибки.
*
900 CONTINUE
WRITE( *, * ) 'File histogram.data could not be opened'
& 'or some reading error'
END

*
* Подпрограмма для вывода гистограммы.
*
SUBROUTINE PRHIST( DATA, NODATA, BOUND, NOBND )
2
Данный листинг демонстрирует жёстко фиксированный формат FORTRAN 77: по-
зиции 1–5 – метка, позиция 6 – признак продолжения строки, позиции 7–72 – опе-
ратор. Именно в этом и состоит главная задача этого листинга. – Прим. перев.
1.1. Особенности современного Fortran 19
REAL DATA(*), BOUND(*)
INTEGER NODATA, NOBND

INTEGER I, J, NOHIST

DO 120 I = 1,NOBND
NOHIST = 0
DO 110 J = 1,NODATA
IF ( DATA(J) .LE. BOUND(I) ) THEN
NOHIST = NOHIST + 1
ENDIF
110
CONTINUE
WRITE( 20, '(F10.2,I10)' ) BOUND(I), NOHIST
120 CONTINUE
END

На Fortran 90 ту же программу можно переписать в так называ-


емом свободном формате (free form) с использованием различных
функций запросов и операций над массивами:
! Построение простой гистограммы.
!
program hist
implicit none

integer, parameter :: maxdata = 1000


integer, parameter :: nobnd = 9

real, dimension(maxdata) :: data


real, dimension(nobnd) :: &
bound = (/0.1, 0.3, 1.0, 3.0, 10.0, &
30.0, 100.0, 300.0, 1000.0/)

integer :: i, nodata, ierr

open( 10, file = 'histogram.data', status = 'old', &


iostat = ierr )

if ( ierr /= 0 ) then
write( *, * ) 'file histogram.data could not be opened'
stop
endif

open( 20, file = 'histogram.out' )

do i = 1,size(data)
read( 10, *, iostat = ierr ) data(i)

if ( ierr > 0 ) then


write( *, * ) 'Error reading the data!'
20 Глава 1. Введение в современный Fortran

stop
elseif ( ierr < 0 ) then
exit ! Обнаружен признак конца файла.
endif
enddo

close( 10 )
nodata = i - 1
call print_history( data(1:nodata), bound )

contains

! Подпрограмма для вывода гистограммы.


!
subroutine print_history( data, bound )
real, dimension(:), intent(in) :: data, bound

integer :: i

do i = 1,size(bound)
write( 20, '(f10.2,i10)' ) &
bound(i), count( data <= bound(i) )
enddo
end subroutine print_history

end program hist

Основные различия между программами:


• стандарт Fortran 90 и все последующие стандарты официаль-
но разрешали записывать исходный код программы в свобод-
ном формате с использованием букв нижнего регистра, хотя
и до этого многие компиляторы, соответствующие стандарту
FORTRAN 77, допускали такую запись в качестве неофици-
ального расширения;
• новый оператор implicit none заставляет компилятор сле-
дить за тем, чтобы все переменные объявлялись с явно указан-
ным типом. Это позволяет устранить большую группу потен-
циальных ошибок;
• использование внутренней подпрограммы (internal routine),
обозначенной ключевым словом contains, позволяет компи-
лятору проверить правильность её действительных аргумен-
тов, соответствие их количества и типов.3
• исключена необходимость использования оператора GOTO,
3
Это только одно из множества полезных свойств внутренних подпрограмм, подроб-
нее см. раздел 1.2.
1.2. Fortran 90 21

имеющего дурную репутацию, то есть его можно заменить «бо-


лее структурным» аналогом exit для завершения цикла do;
• появилась возможность использования сечений массивов
(array sections), например, data(1:nodata), для передачи
только требуемой части массива data, а также функции запро-
са (inquiry function) size(), возвращающей соответствующее
количество элементов массива. Кроме всего прочего это сдела-
ло ненужными два аргумента подпрограммы, в которых пере-
давались размеры массивов;
• использование стандартной функции count() позволило
убрать полностью цикл do при определении данных гистот-
граммы.
Тем не менее, следует отметить, что первая программа остаётся аб-
солютно корректной даже с точки зрения современных стандартов
Fortran, что является очень важным аспектом использования данного
языка. Это означает, что вы можете постепенно переходить к приме-
нению новых функциональных возможностей, а не переписывать всю
программу целиком.

1.2. Fortran 90
В стандарт Fortran 90 было введено большое число новых функцио-
нальных свойств, из которых наиболее известными стали операции
с массивами. Но стандарт определяет и другие важные нововведе-
ния:
• оператор implicit none требует от пользователя явно объяв-
лять все переменные, а компилятор обязан проверять их. Это
позволяет избавиться от опечаток в именах переменных;
• модули (modules) в сочетании с операторами или атрибутами
public и private представляют собой эффективные средства
деления больших программ на более мелкие части, режим до-
ступа к которым определяется явно в исходном коде. Кроме
того, модули позволяют формировать чётко определённые ин-
терфейсы к содержащимся в них подпрограммам, что даёт воз-
можность компилятору выполнить весь комплекс проверок и
оптимизаций. Если это недоступно, например, при взаимодей-
ствии с подпрограммами на другом языке, можно воспользо-
ваться интерфейсными блоками (interface blocks);
• не только основная программа, но также подпрограммы и
функции могут содержать так называемые внутренние под-
22 Глава 1. Введение в современный Fortran

программы (internal routines). В такой подпрограмме доступ-


ны все переменные из внешней содержащей её (под)програм-
мы, но кроме того внутренняя подпрограмма создаёт новую
область видимости (scope), где определяются локальные
переменные. Это дополнительный способ деления кода на
модули;
• современные компьютеры предоставляют разнообразные
средства управления памятью, которые не были так широко
распространены во времена стандарта FORTRAN 77, поэтому
некоторые положения стандарта Fortran 90 связаны с управле-
нием памятью:
 разрешены рекурсивные подпрограммы (recursive routines),
упрощающие реализацию рекурсивных алгоритмов;
 с помощью операторов allocate и deallocate програм-
мист может вручную регулировать размер массивов в со-
ответствии с условиями решаемой задачи. Массивы, раз-
мер которых можно регулировать вручную, делятся на две
категории: динамические (allocatable) и статические
(pointer). Первые предлагают больше возможностей для
оптимизации, тогда как вторые дают большую гибкость;
 помимо прямого изменения размеров массива програм-
мист может использовать автоматические массивы
(automatic arrays) – массивы, которые получают свой раз-
мер из формальных аргументов, автоматически создаются
при входе в подпрограмму или функцию и автоматически
удаляются при выходе;
• операции с массивами (array operations) – одно из важнейших
нововведений в стандарте Fortran 90, поскольку поощряют ла-
коничный стиль программирования и существенно упрощают
задачу оптимизации для компилятора. Эти операции поддер-
живаются не только в обычных арифметических выражениях,
но и с помощью набора обобщённых стандартных функций.
Можно работать как с целым массивом, так и с некоторой
его частью, определяемой начальной и конечной позициями, и
шагом по индексу (stride) в любом измерении.
Кроме того, функции, определённые пользователем, могут
возвращать массив как результат, расширяя возможности опе-
раций с массивами;
• к традиционному старому фиксированному формату (fixed
form), присущему языку Fortran с момента его создания, офи-
1.2. Fortran 90 23

циально добавлен свободный формат (free form), в котором


столбцы 1–6 уже не имеют какого-либо специального предна-
значения. Свободный формат придаёт исходному коду более
современный вид.
• массивы значений можно формировать «на лету» с помощью
так называемых конструкторов массивов (array constructors).
Это мощный механизм, удобный для заполнения массивов
значениями;
• как и многие современные языки программирования Fortran
поддерживает определение новых структур данных. Для это-
го предназначен механизм производных типов (derived types).
Любой производный тип состоит из элементов разных типов –
простых, таких как integer или real, или других произво-
дных типов. Производные типы можно применять точно так
же как простые – передавать их в подпрограммы, использовать
в качестве возвращаемых значений или создавать массивы
этих типов.
Кроме того, производные типы можно использовать для
создания связных списков, деревьев и других известных аб-
страктных типов данных;
• перегрузка (overloading) подпрограмм и операций, таких как
сложение и вычитание, предоставляет возможность расшире-
ния языка новыми полнофункциональными типами. Програм-
мист может определить обобщённые имена для специализиро-
ванных подпрограмм и/или переопределить операции +, – и
другие для числовых типов, не регламентируемых стандартом
(например, для рациональных чисел).
Фактически это дает возможность определять собственные
операции. Простой пример: предположим, что программа ра-
ботает с плоскими геометрическими объектами, поэтому име-
ет смысл определить операцию пересечения (intersection) для
двух таких объектов object_a .intersects. object_b, что-
бы заменить вызов функции intersect_objects( object_a,
object_b );
• теперь функции и подпрограммы могут иметь необязательные
аргументы (optional arguments), при этом функция present()
позволяет определить наличие или отсутствие аргумента. Так-
же можно вызывать функции и подпрограммы с аргументами,
указанными в произвольном порядке, при условии передачи
имен формальных аргументов, чтобы компилятор мог приве-
24 Глава 1. Введение в современный Fortran

сти в соответствие фактические аргументы с формальными.


Ещё одно усовершенствование – можно задать назначение
(intent) любого аргумента: только для ввода, только для вы-
вода или для ввода/вывода. Это дополнительное средство до-
кументирования программы, а также информация для выпол-
нения компилятором оптимизации определённого типа;
• разновидности типов (kinds) – это специфический для языка
Fortran способ определения характеристик простых типов.
Например, для переменных типа real можно выбирать оди-
ночную или двойную точность не явным указанием типа (real
или double precision), а с помощью ключевого слова kind:

integer, parameter :: double = &


select_kind_real( precision, range )
real(kind=double) :: value

• в дополнение к новой конструкции select/case, циклам do


while и циклам без условия do, появилась возможность давать
имена всем управляющим структурам. Это упрощает докумен-
тирование начала и конца таких структур. Чтобы пропустить
некоторую часть тела цикла do, теперь можно воспользовать-
ся оператором cycle (с необязательным именем конкретного
цикла) – таким способом можно выйти из нескольких вложен-
ных циклов do. Подобным образом оператор exit завершает
выполнение цикла do;
• стандарт Fortran 90 определяет большое количество функций
и подпрограмм:
 числовые функции-запросы для получения свойств моде-
ли вещественных чисел;
 функции обработки массивов, в большинстве случаев при-
нимающие выражения с массивами, как один из аргумен-
тов. Например, подсчитать число положительных элемен-
тов в массиве можно так:

integer, dimension(100,100) :: array


...
write(*,*) 'Number of positive elements: ', &
count( array > 0 )

 функции для работы с символами и функции для операций


с битами;
1.3. Fortran 95 25

• к усовершенствованиям системы ввода/вывода относится не-


продвигающий ввод/вывод (nonadvancing I/O), то есть про-
грамма может манипулировать не только целыми записями,
но также читать или записывать часть записи в одном опера-
торе, а остальную часть записи в другом.

1.3. Fortran 95
Стандарт Fortran 95 представляет собой скорректированную и до-
полненную версию предыдущего выпуска стандарта, поэтому разли-
чия не столь значительны. Тем не менее, следует обратить внимание
на следующие важные изменения:
• согласно стандарту Fortran 90, переменные с атрибутом
pointer не могут инициализироваться. Начальное состояние
не определено ни для связанных, ни для несвязанных перемен-
ных. В версии Fortran 95 введена функция null() для явной
инициализации указателей:

real, dimension(:), pointer :: ptr => null()

• динамические (allocatable) локальные переменные без


атрибута save автоматически удаляются из памяти при выхо-
де из области видимости (при возврате из подпрограммы или
из функции). Это вполне безопасно, поскольку после выхода
из области видимости таких переменных, выделенная им па-
мять становится недоступной;
• технический отчёт, как подготовительный материал для сле-
дующего стандарта, описывает, как динамически размещае-
мые (allocatable) компоненты могут входить в состав произ-
водных типов и возвращаться функциями. (Этот технический
отчёт в дальнейшем стал частью стандарта Fortran 2003 с не-
которыми дополнениями.);
• в Fortran 95 были введены типы подпрограмм pure и
elemental. Элементные подпрограммы (elemental routines) ос-
вобождают программиста от необходимости писать несколько
версий подпрограмм для работы с массивами всех требуемых
размерностей. Многие функции и подпрограммы стандарт-
ной библиотеки уже имели статус elemental, означающий,
что они работают с отдельными элементами массивов, кото-
рые могут передаваться без соблюдения какого-либо порядка.
26 Глава 1. Введение в современный Fortran

После ввода стандарта Fortran 95 программистам была предо-


ставлена возможность самим писать такие подпрограммы.
«Чистые» (pure) подпрограммы предоставляют компилято-
ру больше возможностей для оптимизации, так как считается,
что они не имеют побочных эффектов.
• следует особо отметить появление оператора forall, взятого
из языка High Performance Fortran. Он был предназначен для
расширения возможностей операций над массивами, но на
практике далеко не всегда использовался корректно. (Стан-
дартом Fortran 2008 введена более гибкая конструкция do
concurrent.)

1.4. Fortran 2003


Fortran 2003 представляет собой существенно переработанную и ис-
правленную версию стандарта, и самым главным изменением являет-
ся введение поддержки объектно-ориентированного программирова-
ния.4 Но в этой версии появились и другие нововведения, например,
стандартизация взаимодействия с подпрограммами (функциями),
написанными на языке C. Ниже кратко описаны основные нововве-
дения в стандарте Fortran 2003:
• с поддержкой объектно-ориентированного программирования
(object-oriented programming) были напрямую связаны некото-
рые новые функциональные возможности:
 производные типы теперь могут содержать процедуры
(функции и подпрограммы), связанные с данным типом
(так, что все переменные этого типа могут пользовать-
ся одним и тем же набором процедур) или с конкретной
переменной. Такие процедуры автоматически принимают
«свою» переменную (или объект в более широком смысле)
как один из аргументов, но при этом программисту предо-
ставляется возможность управления передачей объектов.
(Эти вопросы будут рассматриваться более подробно в
главе 11.);
4
Стандарт Fortran 90 определяет достаточно много функциональных свойств языка,
позволяющих программисту вплотную приблизиться к этому стилю программиро-
вания, но в нём отсутствует наследование, зачастую рассматриваемое как одна из
наиболее важных характеристик объектно-ориентированного программирования.
Поэтому Fortran 90 иногда называли языком, основанным на концепции объектов
(object-based).
1.4. Fortran 2003 27

 появилась возможность на основе производных типов


создавать новые типы, то есть «расширять» производные
типы. Это механизм наследования в Fortran. Расширенный
тип (extended type) может содержать новые компоненты
(данные и процедуры), а также переопределять процеду-
ры, связанные с родительским типом, но не имеет права
изменять их сигнатуру;
 в оператор select добавлена возможность выбора по типу
переменной. Это важно при работе с так называемыми по-
лиморфными переменными (polymorphic variables) — пере-
менными-указателями, тип которых меняется в зависи-
мости от типов связанных с ними переменных в тот или
иной момент выполнения программы. Для объявления
полиморфных переменных используется ключевое слово
class вместо type.5
 чтобы обеспечить большую гибкость механизма расши-
рения типов, для процедур добавлена характеристика аб-
страктный интерфейс (abstract interface). Вместо опреде-
ления конкретной подпрограммы интерфейсы объявляют
лишь «внешний вид» подпрограммы со списком аргумен-
тов и типом возвращаемого значения. В дальнейшем это
объявление может служить шаблоном для настоящих под-
программ;
• поскольку указатели на процедуры (procedure pointers) чаще
встречаются в виде компонентов производных типов, их мож-
но использовать как обычные переменные;
• для получения некоторых особых эффектов была введена
концепция внутренних модулей (intrinsic modules). Примером
таких особых эффектов может служить управление моде-
лью представления вещественных чисел: режим округления,
эффект исключений, связанных с операциями над веще-
ственными числами, а также организация взаимодействий с
программами на языке C, так как при этом иногда требуется
соблюдение разных соглашений об именовании и формате вы-
зова функций;
• улучшено управление памятью:
 длину строки символов теперь можно определить с помо-
щью оператора allocate;
5
В [73] сравнивается терминология объектно-ориентированных концепций в языках
Fortran и C++.
28 Глава 1. Введение в современный Fortran

 при некоторых условиях динамический массив можно ав-


томатически привести к корректному размеру;
 допускается перемещать выделенную память из одной
переменной в другую с помощью подпрограммы move_
alloc(). Это упрощает увеличение размера массива;
• появилась весьма полезная возможность потокового доступа
(stream access) к файлам, которая позволяет читать и записы-
вать содержимое файлов не только целыми записями. В част-
ности, теперь можно читать и записывать двоичные файлы,
не имеющие внутренней структуры. Для старых файлов, не
имеющих формата, (unformatted files) требовалась структура,
основанная на записях, что затрудняло взаимодействие с дру-
гими языками программирования;
• стандарт Fortran 2003 также регламентирует доступ к си-
стемной среде в форме переменных окружения (environment
variables) и к аргументам командной строки, заданным при
запуске программы. Раньше для доступа к этой информации
программист был вынужден использовать специфические воз-
можности конкретного компилятора.

1.5. Fortran 2008


В версии стандарта Fortran 2008 самым важным нововведением яв-
ляются комассивы (coarrays), но все прочие изменения языка были
незначительными. Более подробно комассивы рассматриваются в
главе 12 [72], [71]. Помимо этого в стандарте определено несколько
новых конструкций и ключевых слов, а также новые стандартные
функции:
• комассивы представляют собой механизм параллельных вы-
числений из категории «разделённое глобальное адресное
пространство» (Partitioned Global Address Space, PGAS). По
существу этот механизм обеспечивает доступность данных не-
скольким копиям программы и освобождает программиста от
обязанности выбирать конкретный способ передачи данных.
Компилятор должен сгенерировать код, выполняющий эту за-
дачу рационально и эффективно;
• массивы могут быть определены как непрерывные (contiguous).
Это позволяет компилятору повысить степень оптимизации
кода, так как элементы массива в памяти следуют друг за другом;
1.6. Что осталось неизменным 29

• конструкция block – end block определяет локальную об-


ласть видимости в программе или в подпрограмме, так что
можно объявлять новые переменные, которые будут суще-
ствовать только внутри этого блока;
• модули, введённые в стандарте Fortran 90, являются важным
механизмом деления больших программ на части. Но сам по
себе этот механизм не может обеспечить высокую степень мо-
дульности, поскольку компилироваться должен весь модуль,
целиком. В стандарт Fortran 2003 были введены подмодули
(submodules) для устранения проблем при обработке чрезвы-
чайно больших файлов исходного кода с крупными модулями.
Теперь к этому механизму добавилась возможность импорта
(import), которая используется в интерфейсных блоках для
импорта определений из внешнего модуля, что исключает не-
обходимость определения второго модуля, включающего в
себя только требуемые определения;
• ещё один шаг к уменьшению зависимости от оператора GOTO, –
оператор exit, выполняющий переход к концу блока if или
select;
• оператор do concurrent можно рассматривать как более гиб-
кую альтернативу оператору/блоку forall. Он сообщает ком-
пилятору, что данный код может выполняться в параллельном
режиме (в данном случае параллельное выполнение осущест-
вляется в рамках одной и той же копии, а не между нескольки-
ми копиями, как при использовании комассивов);
• внутренние процедуры (internal procedures) теперь можно пере-
давать как действительные аргументы, при этом внутренним
процедурам доступны переменные в подпрограмме, где опре-
делены такие процедуры. Это упрощает решение некоторых
проблем с интерфейсами (см. главу 5);
• в набор стандартных функций включены разнообразные
функции Бесселя (Bessel functions) и функции запросов при
работе с битами.

1.6. Что осталось неизменным


Все описанные выше новые функциональные возможности языка
Fortran не повлияли на существующие программы, соответствующие
старым стандартам. Исключение составили лишь те свойства, кото-
30 Глава 1. Введение в современный Fortran

рые были удалены или выведены из употребления (их применение


не рекомендуется).6
Старые программы должны успешно компилироваться, при усло-
вии отсутствия в них удалённых и давно забытых свойств и возмож-
ностей языка, таких как вычисляемый оператор GOTO.7
В действительности, помимо упомянутой поддержки старого кода,
в реализации современного Fortran многое осталось неизменным:
• Fortran нечувствителен к регистру символов, в отличие от
многих языков программирования, используемых в наши дни;
• в языке отсутствует понятие файла как организационной и
структурной единицы исходного кода. В частности, исход-
ный код не может находиться вне программы, подпрограммы,
функции или модуля;
• по своей сущности Fortran ориентирован на эффективное
выполнение. Это особенно заметно в последнем стандарте
Fortran 2008, где все нововведения направлены на то, чтобы
помочь компилятору в создании быстрых и эффективных про-
грамм.
Тонкости реализации нововведений могут удивить програм-
мистов, использующих языки, подобные C (см. приложе-
ние Б);
• тип данных любого выражения или подвыражения зави-
сит только от операндов и операции, но не от контекста. Это
оправдано с точки зрения существенного упрощения програм-
мы, но опять-таки может давать неожиданные результаты (см.
приложение Б). Вот один небольшой пример:
real :: r
r = 1 / 3

Здесь переменная r получит значение 0.0, а не 0.333333..., так


как сначала выполняется операция деления двух целых чисел, ре-
зультатом которой является новое целое число, а затем полученное
целое число преобразуется в вещественное.
Точно такое же разделение операций и преобразования их резуль-
татов справедливо для операций с массивами:
integer, dimension(10) :: array
6
Самой значимой из всех удалённых функциональных возможностей является ис-
пользование переменных типа real для управления циклом do ([68], раздел 2.1.5).
7
Большинство компиляторов продолжают поддерживать эти устаревшие возможно-
сти, поэтому старые программы можно компилировать и запускать.
1.6. Что осталось неизменным 31
array = 2 * array(10:1:-1)

Эта инструкция выполняется (по крайней мере, теоретически)


следующим образом:

integer, dimension(10) :: array


integer, dimension(10) :: tmp
integer :: i

! Сначала вычисляется правая часть выражения, и результат


! сохраняется во временном массиве.
do i = 1,10
tmp(i) = 2 * array(11-i)
enddo

! Теперь результат правой части выражения копируется в массив


! слева от знака равенства.
do i = 1,10
array(i) = tmp(i)
enddo

Это означает, что код, подобный приведённому выше, всегда рабо-


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

Стандарт Fortran 90 вместе с операциями над массивами ввёл поня-


тие функций, возвращающих массивы. Такие функции способствуют
использованию лаконичного, легко читаемого стиля программирова-
ния, во многом похожего на тот, за который высказался Джон Бэкус
(John Backus), создатель языка Fortran, в своей речи при получении
награды ACM Award [13].
Стандарт Fortran 90 позволил программисту определять функции,
возвращающие массивы данных, в стандарте Fortran 95 была введе-
на концепция элементных функций (elemental functions) и процедур.
В сочетании с некоторыми функциональными возможностями, опре-
деляемыми стандартом Fortran 2003, средства работы с массивами
стали ещё более мощными и простыми в использовании.
Многие встроенные функции напрямую работают с массивами, а
специализированные операции языка для обработки массивов спо-
собны преобразовать массив целиком без лишних циклов, порой ме-
шающих понять замысел программиста. Например, предположим,
что имеется массив, из которого требуется вывести только значения,
большие некоторого заданного предела. Это можно сделать таким
способом:
do i = 1,size(data)
if( data(i) > threshold ) then
write( *, '(f10.4)' ) data(i)
endif
enddo

Но если воспользоваться встроенной функцией pack(), решение


предложенной задачи можно записать более кратко:

write( *, '(f10.4)' ) pack( data, data > threshold )


2.1. Передача массивов в аргументах 33

2.1. Передача массивов


в аргументах
Стандартная функция pack() возвращает только элементы массива,
для которых истинно логическое выражение во втором аргументе.
Например, следующий фрагмент:

integer, dimension(5) :: x = (/ 1, 2, 3, 4, 5 /)
write( *,* ) pack( x, x > 3 )

выведет числа 4 и 5, потому что это единственные элементы масси-


ва x, значения которых больше 3. Результат всегда будет массивом,
даже если заданному условию соответствует всего один элемент или
вообще ни одного элемента, поскольку допускается существование
массивов нулевого размера.
Результат выполнения функции pack() можно передать другой
функции или подпрограмме, например, функции, определяющей ос-
новные статистические характеристики:

write( *, '(a,f10.4)' ) 'Mean of values above mean: ', &


mean( pack( data, data > mean(data) ) )

где функция mean() может выглядеть так:

real function mean( data )


real, dimension(:) :: data

mean = sum( data ) / max( 1, size(data) )


end function mean

Сравним это решение с другим подходом, когда массив обраба-


тывается поэлементно (предполагается, что используется функция
mean(), определённая выше):

real, dimension(...) :: data


real, dimension(size(data)) :: work ! Массив для выбранных по
! условию данных

meanv = mean(data)
count = 0

do i = 1,size(data)
if( data(i) > meanv ) then
34 Глава 2. Функции для работы с массивами

count = count + 1
work(count) = data(i)
endif
enddo

write( *, '(a,f10.4)' ) 'Mean of values above mean: ', &


mean( work(1:count) )

Можно обойтись и без функции mean(), так как она очень проста:

meanv = mean( data ) ! Нужна для определения порогового значения

sumv = 0.0
count = 0
do i = 1,size(data)
if( data(i) > meanv ) then
count = count + 1
sumv = sumv + data(i)
endif
enddo

write( *, '(a,f10.4)' ) 'Mean of values above mean: ', &


sumv / max(1,count)

Очевидно, что использование функций, подобных pack(), позво-


ляет писать более краткий и в большинстве случаев более понятный
код. Но и у этих функций есть свои недостатки:
• отсутствует возможность прямого доступа к отдельным эле-
ментам. Например, для определения второго наибольшего
элемента в массиве сначала потребуется сохранить результат
работы функции сортировки sort() во временном массиве;
• если результат нужен в нескольких местах, придётся скопиро-
вать его в исходный массив или передавать результат в функ-
цию или подпрограмму;
• вышеупомянутые функции создают временные массивы, для
которых в соответствующее время должна выделяться и осво-
бождаться динамическая память. Это может отрицательно по-
влиять на производительность программы.
От контекста, в котором используются функции обработки мас-
сивов, полностью зависит отношение к перечисленным выше недо-
статкам. По крайней мере, такой стиль программирования весьма
лаконичен, если брать в расчёт количество строк кода, да и сам код
становится более понятным. К тому же компиляторы постоянно со-
2.1. Передача массивов в аргументах 35

вершенствуются, так что производительность не следует считать


главной заботой программиста.

Производительность при использовании


функций обработки массивов
Чтобы наглядно продемонстрировать различия в производительно-
сти, вычислим двумя разными способами среднее значение случай-
ных чисел, больших заданного порогового значения:
• фильтрация данных, больших заданного порогового значения,
с помощью функции pack() и определение среднего значения
отфильтрованных данных:

call random_data( data )


...
call mean_pack( pack( data, data > threshold ), mean1 )

Реализация подпрограммы mean_pack:


subroutine mean_pack( data, mean )
real, dimension(:) :: data
real :: mean

mean = sum( data ) / max( 1, size(data) )


end subroutine mean_pack

• передача всего массива (заполненного случайными числами)


в подпрограмму, которая выбирает данные по условию и сум-
мирует их в цикле do:

call random_data( data )


...
call mean_do_loop( data, threshold, mean2 )

Реализация подпрограммы выбора данных по условию:

subroutine mean_do_loop( data, threshold, mean )


real dimension(:) :: data
real :: threshold
real :: mean
integer :: i
integer :: count

mean = 0.0
36 Глава 2. Функции для работы с массивами

count = 0
do i = 1,size(data)
if( data(i) > threshold ) then
mean = mean + data(i)
count = count + 1
endif
enddo

mean = mean / max(1,count)


end subroutine mean_do_loop

Затраченное время измеряется с помощью подпрограммы system_


clock(): системное время запрашивается до и после выполнения
цикла, в котором каждый из методов вызывается 1000 раз. Разность
между значениями системного времени даёт время, затраченное на
выполнение (в тактах таймера). Результаты измерений показаны в
табл. 2.1.
Таблица 2.1. Время, затраченное на вычисление среднего
арифметического элементов массива (в тиках таймера)

Общее количество Использование Использование Отношение


данных цикла do операций с массивами
1000 16 47 2.9
10000 62 172 2.8
100000 556 1219 2.2
1000000 6594 12515 1.9

Измерения позволяют оценить соотношение производительности


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

2.2. Элементные функции


и автоматическое
перераспределение памяти
В Fortran 90 только стандартные функции и специализированные
операции над массивами могут быть использованы для работы с
2.2. Элементные функции и автоматическое ... 37

массивами любой размерности. Например, нет никаких различий в


применении функции вычисления синуса для скалярного значения,
одномерного или n-мерного массива – код один и тот же. Если по-
добные вычисления нужно выполнить с помощью функции, реали-
зующей функцию Бесселя J0, потребуются отдельные версии для ска-
лярных аргументов, для аргументов в виде одномерных массивов и
т. д., а также интерфейс, чтобы скрыть различия в именах версий этой
функции.
В стандарте Fortran 95 были введены так называемые элементные
функции (elemental functions) и подпрограммы. Они должны соответ-
ствовать определённому набору условий, так как порядок обработки
элементов массива такой функцией теоретически не определён. Это
означает, что достаточно написать единственную версию элементной
функции:

program test_j0
real :: scalar
real, dimension(10,10) :: matrix

scalar = 1.0
call random_number( matrix )
matrix = matrix * 100.0

write(*,*) 'J0(1) = ', j0( scalar )


write(*,*) 'Случайные значения x в диапазоне (0,100):'
write(*,'(10f10.4)') j0( matrix )

contains

real elemental function j0( x )


real :: x

j0 = ...
end function j0
end program test_j0

Приведённое решение вполне применимо, если возвращаемое


функцией значение имеет ту же форму, что и принимаемый аргумент
(или принимаемые аргументы). В противном случае использование
элементных функций невозможно.
Если заранее неизвестно, сколько элементов будет содержать
результат, как это часто происходит при использовании функции
pack(), присваивание результата какому-либо массиву вызывает за-
труднения – формы должны совпадать. Но эту проблему позволяют
38 Глава 2. Функции для работы с массивами

решить введённые стандартом Fortran 2003 динамические массивы


(allocatable arrays):
real, dimension(:), allocatable :: positive_values
positive_values = pack( data, data > 0.0 )

При использовании этой возможности1 массив в левой части вы-


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

2.3. Два более сложных примера


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

Дистанционирование иррациональных
чисел
Первый пример требует небольшого пояснения: это задача из области
теории чисел, относящаяся к методам упорядочения иррациональных
чисел [66]. Рассматриваются следующие числа (α – иррациональное
число):

V = {x | x = nα mod 1, n = 1 .. N} (2.1)

Если отсортировать эти числа, расстояния между ними примут


одно из трёх возможных значений (здесь интервал [0,1) заворачива-
ется так, что значения 0.9 и 0.1 оказываются удалены друг от друга на
расстояние 0.2, а не 0.8). Следующая программа демонстрирует это.
Она создаёт массив иррациональных чисел, сортирует их (с помощью
простого алгоритма) и выводит расстояния между ними:

program nearest_neighbors
implicit none
integer :: i

write(*,'(f10.4)') &
cdiff( sort( (/ (mod( i * sqrt(2.0), 1.0 ), i = 1,20) /) ) )

contains

1
По меньшей мере один компилятор требует указывать специальный ключ для
включения поддержки динамических массивов.
2.3. Два более сложных примера 39
function cdiff( array )
real, dimension(:) :: array
real, dimension(size(array)) :: cdiff

cdiff = abs( array - cshift( array, 1 ) )


cdiff = min( cdiff, 1.0 - cdiff )
end function cdiff

function sort( array )


real, dimension(:) :: array
real, dimension(size(array)) :: sort
real :: temp
integer :: i, j
integer, dimension(1) :: pos

!
! Извлечение наименьших элементов поочерёдно, по одному.
!
sort = array
do i = 1,size(sort)
pos = minloc( sort(i:) )
j = i + pos(1) - 1
temp = sort(j)
sort(j) = sort(i)
sort(i) = temp
enddo
end function sort
end program nearest_neighbors

В этом примере функция sort() сначала выполняет поиск пози-


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

program nearest_neighbors
implicit none
integer :: i
integer, parameter :: n = 20
real, dimension(n) :: data

do i = 1,n
data(i) = mod( i * sqrt(2.0), 1.0 )
40 Глава 2. Функции для работы с массивами

enddo

call sort( data )


call cdiff( data )

write(*,'(f10.4)') data

contains

subroutine cdiff( array )


real, dimension(:) :: array
real, dimension(size(array)) :: work
integer :: i

do i = 1,size(array)-1
work(i) = array(i+1) - array(i)
enddo
work(size(array)) = array(size(array)) - array(1)

do i = 1,size(array)
array(i) = min( abs( work(i) ), abs( 1.0 - work(i) ) )
enddo
end subroutine cdiff

subroutine sort( array )


real, dimension(:) :: array
real :: temp
integer :: i, j

do i = 1,size(array)
do j = i+1,size(array)
if( array(i) > array(j) ) then
temp = array(i)
array(i) = array(j)
array(j) = temp
endif
enddo
enddo
end subroutine sort
end program nearest_neighbors

Быстрая сортировка QuickSort


С помощью конструкторов массивов можно делать ещё более уди-
вительные вещи. Предлагаемая ниже реализация широко извест-
ного алгоритма быстрой сортировки QuickSort, возможно, не самая
быстрая, и потребляет немного больше памяти, чем необходимо, но
весьма компактна:
2.4. Компактный стиль 41
recursive function qsort_reals( data ) result( sorted )
real, dimension(:), intent(in) :: data
real, dimension(1:size(data)) :: sorted

if( size(data) > 1 ) then


sorted = &
(/ qsort_reals( pack( data(2:), &
data(2:) > data(1) ) ), &
data(1), &
qsort_reals( pack( data(2:), &
data(2:) <= data(1) ) ) /)
else
sorted = data
endif
end function qsort_reals

Этот небольшой фрагмент кода обладает двумя важными свой-


ствами:
• массивы в Fortran могут иметь нулевую длину, что позволя-
ет обойтись без обработки нескольких особых случаев в этой
функции;
• условия подобраны аккуратно, поэтому динамически созда-
ваемый массив имеет в точности тот же размер (длину), что и
входной массив. Кроме того, эта функция должна правильно
работать, если одно и то же значение встретится несколько
раз.
В качестве «домашнего задания» читателю предлагается написать
версию этой же функции без использования новшеств последних
стандартов, с применением более привычных средств. Также вы мо-
жете попробовать создать более надёжную версию, которая лучше об-
рабатывает наихудшие варианты.

2.4. Компактный стиль


Компактный стиль программирования не всегда является наилуч-
шим способом решения задачи: предложенная выше подпрограмма
QuickSort создаёт несколько копий исходного массива во время ре-
курсии, приблизительно log2 N раз, но если пользоваться этой под-
программой разумно, она окажется полезной при создании программ,
лёгких для понимания.
Ниже приводится ещё один пример компактного решения другой
задачи. Предположим, имеется табличная функция с тремя незави-
симыми переменными, и необходимо найти значение для некоторой
42 Глава 2. Функции для работы с массивами

тройки этих переменных путем многомерной линейной интерполя-


ции (см. табл. 2.2, где показано решение для двух измерений). Во-
первых, нужно найти «ячейку», в которой располагается искомая
тройка и весовые коэффициенты в каждом измерении (это можно
сделать для каждого измерения). Например, это могут быть индексы
от (i1,i2,i3) до (i1+1,i2+1,i3+1) с весами (w1,w2,w3). Для интерполя-
ции требуются значения только из углов этой ячейки, и можно из-
влечь их и поместить в массив с 2×2×2 элементами:

real, dimension(:,:,:) :: table


real, dimension(2,2,2) :: cell

cell = table(i1:i1+1,i2:i2+1,i3:i3+1)

Таблица 2.2. Табличная функция, линейная интерполяция по двум


измерениям. Выделена ячейка, содержащая пару (12.0, 16.7)

y\x … 5.0 10.0 25.0 100.0 …


… … … … … … …
2.5 … 0.1 0.9 2.3 7.8 …

5.0 … 0.5 2.0 3.5 11.0 …


10.0 … 1.4 3.1 4.7 12.5 …

20.0 … 2.6 5.3 6.5 14.0 …


… … … … … … …

Если воспользоваться очевидной формулой интерполяции, по-


требуется записать все восемь условий, причем, число условий воз-
растает с увеличением числа измерений. Но интерполяцию можно
выполнять и по отдельному измерению: просто вычислять значения
в сечении массива, соответствующем определённой ячейке, тем са-
мым уменьшая количество измерений на 1. Эту операцию следует
повторять, пока число измерений для данной ячейки не станет ну-
левым. Fortran позволяет просто и понятно записать предложенное
решение:

cell(:,:,1) = w3 * cell(:,:,1) + (1.0 - w3) * cell(:,:,2)


cell(:,1,1) = w2 * cell(:,1,1) + (1.0 - w2) * cell(:,2,1)
cell(1,1,1) = w1 * cell(1,1,1) + (1.0 - w1) * cell(2,1,1)
result = cell(1,1,1)
2.4. Компактный стиль 43

Адаптация этого кода для работы с любым числом измерений не


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

Определение операций для производных типов, использование опе-


раций над массивами в Fortran 90 и в более поздних версиях, а также
возможность хранения указателей на процедуры, предоставляемая
Fortran 2003 и последующими версиями, обеспечивают поддержку
стиля программирования высокого уровня, присущего функциональ-
ным языкам, таким как Lisp. В этой главе мы рассмотрим несколь-
ко примеров: автоматическое дифференцирование, дискретное (или
целочисленное) программирование, Диофантовы уравнения и дина-
мические выражения.

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

x e− x
f ( x) = − (3.1)
(1 − x 2 ) 2 1 + cos 2 x
и её первую производную:

1 + 3x 2 2
x 1 + cos x − sin 2 x
f ' ( x) = − e (3.2)
(1 − x 2 ) 3 (1 + cos 2 x) 2
3.1. Автоматическое дифференцирование 45

Определение производной функции представляет собой чисто


механический процесс, при котором нужно лишь соблюдать основ-
ные правила дифференцирования. Поэтому определение произво-
дной можно автоматизировать, используя перегруженные операторы
(overloaded operators) [38].
Начнём с производного типа autoderiv:
type autoderiv
real :: v
real :: dv
end type autoderiv

В этом типе компонент v представляет значение независимой пере-


менной или значение функции, а компонент dv – значение произво-
дной. Суммирование двух таких переменных выполняется с помо-
щью следующей функции:
function add_vv( x, y ) result( z )
type(autoderiv), intent(in) :: x
type(autoderiv), intent(in) :: y
type(autoderiv) :: z

z%v = x%v + y%v


z%dv = x%dv + y%dv
end function add_vv

Умножение выполняется чуть сложнее, поскольку необходимо


применить правило умножения для производных (обратите внима-
ние на атрибуты intent(in), которые потребуются немного позже):
function mult_vv( x, y ) result( z )
type(autoderiv), intent(in) :: x
type(autoderiv), intent(in) :: y
type(autoderiv) :: z

z%v = x%v * y%v


z%dv = x%dv * y%v + x%v * y%dv
end function mult_vv

Аналогично определяются функции вычитания, деления и прочих


математических операций, необходимых для вычисления произво-
дной. После этого в блоках интерфейсов эти функции связываются с
арифметическими операциями:
interface operator(+)
module procedure add_vv
46 Глава 3. Математические абстракции

end interface
interface operator(*)
module procedure mult_vv
end interface

2 1
Теперь применим эту методику к функции f ( x) = cos x /( 1 + x ) − :
2
function f( x ) result( z )
type(autoderiv), intent(in) :: x
type(autoderiv) :: z

z = cos(x) / (1 + x**2) - 0.5


end function f

Благодаря возможности перегрузки операторов данная функция


выглядит как обычная математическая функция, только тип аргумен-
та и тип возвращаемого значения определены особым образом.
Основная программа выводит значения функции и значения её
первой переменной:

program print_table
use automatic_differentiation

type(autoderiv) :: x, y

do i = 0,10
x%v = 0.1 * i
x%dv = 1.0 ! x – дифференцируемая переменная.
y = f( x )

write(*,'(310.4)') x%v, y%v, y%dv


enddo

contains
function f( x ) result( z )
...
end function f
end program print_table

Эту же методику можно применять и для нахождения корней урав-


нения f(x) = 0 методом Ньютона. Математически этот метод записы-
вается в виде:
f ( xk )
xk +1 = xk − (3.3)
f ' ( xk )
3.1. Автоматическое дифференцирование 47

где xk+1 – очередное приближённое значение корня при условии схо-


димости итерации. Метод Ньютона можно записать в виде следую-
щей подпрограммы:
subroutine find_root( f, xinit, tolerance, root, found )
use automatic_differentiation

interface
function f( x )
use automatic_differentiation
type(autoderiv), intent(in) :: x
type(autoderiv) :: f
end function
end interface

type(autoderiv), intent(in) :: xinit


type(autoderiv), intent(out) :: root
real :: tolerance
logical :: found

integer :: iter
integer, parameter :: maxiter = 1000
type(autoderiv) :: fvalue
type(autoderiv) :: newroot

found = .false.
root = xinit
root%dv = 1.0

do iter = 1,maxiter
fvalue = f( root )

newroot = root - fvalue%v / fvalue%dv


found = abs( newroot%v - root%v ) < tolerance
root = newroot

if( found .or. abs( root%v ) > huge(1.0)/10.0 ) exit


enddo
end subroutine find_root

Передача интерфейса в функцию в качестве аргумента не является


стандартной операцией, следовательно, необходимо явно определить,
какая именно разновидность функции используется. Это делается в
интерфейсном блоке:1
interface
function f( x )
1
Согласно стандарту Fortran 2003 и более поздним версиям можно воспользоваться
абстрактными интерфейсами (abstract interfaces) (см. раздел 11.2).
48 Глава 3. Математические абстракции

use automatic_differentiation
type(autoderiv), intent(in) :: x
type(autoderiv) :: f
end function
end interface

Внутри интерфейсного блока нужно обязательно объявить об


использовании модуля, содержащего определение производного
типа с помощью оператора use или import, введённого стандартом
Fortran 2003.
Отметим, что выполнение цикла поиска корня может быть оста-
новлено по трём условиям (также см. главу 10):
• если разность между новым приближённым значением и пре-
дыдущим находится в пределах заданной допустимой погреш-
ности;
• если выполнено заданное максимальное число итераций;
• если возникает сомнение в сходимости итераций: значение
корня увеличивается с каждой итерацией.
Применение описанной методики не ограничивается функциями
одной переменной или только первой производной от функции. Ее
вполне можно распространить на функции двух или трёх переменных:
type autoderiv_two_vars
real :: v
real :: dvdx
real :: dvdy
end type autoderiv_two_vars

В качестве примера рассмотрим правило умножения для функции


двух переменных:

function mult_vv( x, y ) result( z )


type(autoderiv_two_vars), intent(in) :: x
type(autoderiv_two_vars), intent(in) :: y
type(autoderiv_two_vars) :: z

z%v = x%v * y%v


z%dvdx = x%dvdx * y%v + x%v * y%dvdx
z%dvdy = x%dvdy * y%v + x%v * y%dvdy
end function mult_vv

Для определения частных производных ∂ƒ⁄∂x и ∂ƒ⁄∂xy необхо-


димо воспользоваться специальными «векторами»: x = (x0, 1, 0) и
y = (y0, 0, 1).
3.1. Автоматическое дифференцирование 49

То есть, для функции f(x,y) = xy результат будет представлен сле-


дующим образом:
f%v = x0 ∙ y0 (3.4)
f%dvdx = 1 ∙ y0 + x0 ∙ 0 = y0 (3.5)
f%dvdy = 0 ∙ y0 + x0 ∙ 1 = x0 (3.6)

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


изводных высших порядков реализация становится всё более слож-
ной, но общий принцип остаётся неизменным.

Проблемы при вычислениях


Методике автоматического дифференцирования присущи некоторые
недостатки. Прежде всего, она плохо работает для функций, которые
имеют устранимую сингулярную (особую) точку (removable singular
point), как например функция sinc. Но может возникнуть и более се-
рьёзная проблема – потеря значимых разрядов мантиссы (catastrophic
cancellation или loss of significance, LOS) и как следствие – потеря точ-
ности вычислений. Вот один из примеров:
ln
ln x
f ( x) = ( x ≠ 1) (3.7)
x −1
f ( x) = 1 ( x ≠ 1) (3.8)

Первая производная этой функции:


1 ln
ln x
f ' ( x) = − (3.9)
x( x − 1) ( x − 1) 2
x − 1 − x ln
ln x
= (3.10)
x( x − 1) 2
значение которой стремится к –1/2 для x → 1.
При вычислении производной этой функции в окрестности x = 1
с использованием модуля автоматического дифференцирования, для
значения, приблизительно равного x = 1.00001, наблюдается очень
большая относительная погрешность (см. рис. 3.1). Причина в том,
что две составляющие производной в формуле 3.9 вычисляются от-
дельно, при этом обе составляющие являются большими числами в
50 Глава 3. Математические абстракции

окрестности x = 1, и производится вычитание одной составляющей из


другой. Это приводит к серьёзной потере точности.
0.01

0.005

0.0

-0.005

-0.01
1.0e-007 1.0e-006 1.0e-005 1.0e-004 1.0e-003
x-1
Рис. 3.1. Ошибка при определении первой производной
функции f(x) = ln x / (x – 1) в окрестности x = 1
Если пересмотреть подход к решению и аппроксимировать лога-
рифм рядом Тейлора, работать с такой функцией будет гораздо про-
ще. Подобный подход, когда рассматриваются несколько вариантов
решения, следует применять не только для реализации автоматиче-
ского дифференцирования.

3.2. Дискретное
программирование
В качестве следующего примера рассмотрим такую задачу: для це-
лых чисел x, y ≥ 0 найти пару (x,y), для которой значение функции
f(x,y) = 3x + 4y будет максимальным с учётом следующих граничных
условий:
10x + 3y ≤ 200 (3.11)
3x + 7y ≤ 121 (3.12)
Конечно, можно сразу применить очевидный метод решения
«в лоб»: с учётом ограничений значение x должно находиться меж-
3.2. Дискретное программирование 51

ду 0 и 20, а y – в диапазоне [0,17]. Следовательно, цикл по всем парам


(x,y) из этих диапазонов даст требуемый ответ.
Но предложенную задачу можно решить более изящным способом,
используя операции с массивами и соответствующие производные
типы:
type(integer_set) :: xset
type(integer_set) :: yset
type(integer_pair_set) :: c

integer, dimension(1) :: idx

xset = range(0,20) ! Значения x в требуемом диапазоне.


yset = range(0,17) ! Значения y в требуемом диапазоне.

c = cartesian_product( xset, yset ) ! Создание всех возможных пар

Сначала определяются наборы используемых целых чисел xset и


yset, а также множество c целочисленных пар, содержащий все воз-
можные пары (x,y), где значения x и y ограничены заданными диа-
пазонами.
После этого применяются ограничения 3.11 и 3.12 в соответствии с
условиями решаемой задачи:
! Выбрать только те пары, которые соответствуют заданным условиям.
!
c = select( c, 10 * x(c) + 3 * y(c) <= 200 )
c = select( c, 3 * x(c) + 7 * y(c) <= 121 )

Теперь остались только пары, соответствующие условиям задачи.


Последний этап решения – выбор пары, дающей максимальное зна-
чение функции f(x,y) = 3x + 4y:
idx = maxloc( 3 * x(c) + 4 * y(c) )
write(*,*) 'Решение: ', getelem( c, idx(1) )

Примечание: Вспомогательная функция getelem() скрывает подробности


внутренней структуры производного типа integer_pair_set, встроенная
функция maxloc() возвращает массив индексов, даже если её аргументом
является одномерный массив, а x и y – это функции, возвращающие коорди-
наты x и y соответственно.

Управление памятью
Управление памятью скрыто от пользователя для его же удобства.
Вместо принимаемого по умолчанию метода присваивания для про-
52 Глава 3. Математические абстракции

изводных типов используется специализированная подпрограмма


определения начальных значений для переменных типов integer_
set и integer_pair_set:
• производный тип integer_set содержит массив элементов,
который требует выравнивания по размеру для соответствия
фактическому массиву:

type integer_set
logical :: from_function
integer, dimension(:), pointer :: elements => null()
end type integer_set

• операция присваивания по умолчанию замещается двумя про-


цедурами модуля:

interface assignment(=)
module procedure assign_set
module procedure assign_pair_set
end interface

• если в правой части выражения присваивания располагается


результат выполнения функции, компоненту from_function
в производном типе присваивается значение .true., чтобы
требуемый фрагмент памяти мог стать доступным через ука-
затель, а не выделялся как новый:

subroutine assign_set( setleft, setright )


type(integer_set), intent(inout) :: setleft
type(integer_set), intent(in) :: setright

setleft%from_function = .false.
if( associated( setleft%elements ) ) then
deallocate( setleft%elements )
endif
if( .not. setright%from_function ) then
allocate( setleft%elements( size(
setright%elements ) ) )
setleft%elements = setright%elements
else
setleft%elements => setright%elements
endif
end subroutine assign_set

В версии Fortran 2003 имеется подпрограмма move_alloc(), так


что можно использовать весьма похожую методику для компонентов
3.3. Перечислимое множество решений ... 53

с атрибутом allocatable. Можно даже доверить эту задачу механиз-


му автоматического перераспределения памяти:

if( .not. setright%from_function ) then


setleft%elements = setright%elements
else
call move_alloc( setright%elements, setleft%elements )
endif

3.3. Перечислимое множество


решений Диофантовых
уравнений
Указатели на процедуры, в соответствии с определением стандарта
Fortran 2003, позволяют реализовать поиск перечислимого множе-
ства решений Диофантовых уравнений в обобщённой форме. Такие
уравнения имеют только целые или рациональные корни. Одним из
примеров может служить знаменитая великая, или последняя теорема
Ферма – xn + yn = zn, которая не имеет нетривиальных решений для
n > 2. Другой хорошо известный пример – уравнение Пелла [82]:
y2 = Ax2 ± 1 (3.13)
(A – свободное от квадратов, или бесквадратное число)
В отличие от теоремы Ферма эти уравнения имеют бесконечное
множество решений. Вот один из подходов к поиску решений: будем
рассматривать левую и правую части уравнения как отдельные мно-
жества точек (x, t) и (y, s):

S1 = {(x,t) | t = Ax2 + 1, x ∈ N} (3.14)


S2 = {(y,s) | s = y2, y ∈ N } (3.15)
Затем определим ещё одно множество:
S3 = {(x,y) | (x,t) ∈ S1 ⋀ (y,s) ∈ S2 ⋀ t = s } (3.16)
Если предположить, что t и s в этих множествах являются возрас-
тающими функциями от x и y, для определения элементов множества
S3 существует простой алгоритм:
1. Установить начальные значения x и y равными 0.
2. Определить t = Ax2 + 1 и s = y2.
3. Если t = s, решение найдено.
54 Глава 3. Математические абстракции

4. Если t < s, увеличить значение x на 1 и перейти к пункту 2.


5. если t > s, увеличить значение y на 1 и перейти к пункту 2.
Для реализации этого математического алгоритма используется
производный тип, который может хранить описания из уравнений
3.14 и 3.15:
type enum_func
type(enum_func), pointer :: f => null()
type(enum_func), pointer :: g => null()
procedure(enumeration), pointer, pass(t) :: enum => null()
end type enum_func

При определении данного типа применяется возможность рекур-


сивной ссылки на этот же тип. Кроме того, используется абстрактный
интерфейс, позволяющий определить, какая из процедур enum будет
использоваться в данном случае (ключевое слово class подробно
рассматривается в главе 11):
abstract interface
integer function enumeration( t, idx )
import :: enum_func
class(enum_func), intent(in) :: t
integer, intent(in) :: idx
end function enumeration
end interface

Множества S1 и S2 реализуют следующие две функции:


integer function y_square( t, idx )
class(enum_func), intent(in) :: t ! Не используется.
integer, intent(in) :: idx

y_square = idx ** 2
end function y_square

integer function x_pell( t, idx )


class(enum_func), intent(in) :: t ! Не используется.
integer, intent(in) :: idx

x_pell = 2 * idx ** 2 + 1
end function x_pell

Реализация множества S3 немного сложнее. Функция combine()


содержит описанный выше алгоритм:
integer function combine( t, idx )
class(enum_func), intent(in) :: t
3.3. Перечислимое множество решений ... 55
integer, intent(in) :: idx

integer :: count, i, j, fi, gj

count = 0
i = 0
j = 0

fi = t%f%enum(i)
gj = t%g%enum(j)

do while( count < idx )


if( fi == gj ) then
count = count + 1
i = i + 1
else if( fi < gj ) then
i = i + 1
else
j = j + 1
endif
fi = t%f%enum(i)
gj = t%g%enum(j)
enddo
combine = i - 1
end function combine

Теперь нужно описать множество S3 в терминах языка Fortran и


найти число его элементов:
xright = func( x_pell )
yleft = func( y_square )

xpell = combination( xright, yleft, combine )


ypell = combination( yleft, xright, combine )

do i = 1,5
x = xpell%enum(i)
y = ypell%enum(i)
write(*,*) '>>', i, x, y
enddo

где func() – всего лишь вспомогательная функция для настройки


компонента enum:
function func( f )
procedure(enumeration) :: f
type(enum_func) :: func

func%enum => f
end function func
56 Глава 3. Математические абстракции

Вывод программы выглядит следующим образом:


>> 1 0 1
>> 2 2 3
>> 3 12 17
>> 4 70 99
>> 5 408 577

Вставляя другие функции, например, h(x) = x3 + 2, можно решать


другие Диофантовы уравнения. Разумеется, приведённая выше реа-
лизация combine() предполагает, что решения действительно суще-
ствуют, при этом совершенно не учитывается тот факт, что исполь-
зуемые целочисленные значения имеют ограниченный диапазон. Но
это всего лишь незначительные подробности, не влияющие на общий
подход к решению, заключающийся в преобразовании математиче-
ской задачи в программу на языке Fortran, которая очень близка к
математической форме записи.

3.4. Отложенные или ленивые


вычисления
Перегрузку операций можно использовать с ещё большей эффектив-
ностью, если вместо запуска подпрограмм, выполняющих вычисле-
ния, немедленно, сохранять соответствующие операции. Для этого
нужно создать дерево синтаксического разбора (парсинга – parsing)
выражения, чтобы впоследствии можно было вычислить это выраже-
ние. Таким способом можно создавать новые выражения, а при не-
обходимости и новые функции прямо в выполняющейся программе.
Конечно, вы не сможете в полной мере использовать синтаксис обыч-
ных функций, но требуемой цели достигнете.
Вот простой пример:
type(integer_operand), target :: x
type(integer_operand), target :: y
type(integer_relation), pointer :: relation
!
! Сохранение отношения, но НЕ результата.
!
relation => x + y == 0

x = 1
y = -1
write(*,*) 'x, y: 1, -1 ', integer_relation_eval( relation )

x = 2
3.4. Отложенные или ленивые вычисления 57
y = -1
write(*,*) 'x, y: 2, -1 ', integer_relation_eval( relation )

Сначала нужно определить переменную relation, описывающую


отношение между двумя переменными x и y, которые можно рассма-
тривать как шаблоны для подстановки конкретных целочисленных
значений. Затем, путём присваивания реальных значений этим пере-
менным, вы определяете, выполняется ли это отношение. Вся необхо-
димая работа производится функциями integer_relation_eval()
и integer_eval():

function integer_relation_eval( relation) result(value)


type(integer_relation) :: relation
logical :: value

call integer_eval( relation%first )


call integer_eval( relation%second )

select case( relation%relation )


case( 1 )
!
! Отношение: равенство.
!
value = relation%first%value == relation%second%value
case( 2 )
...
... другие отношения (>, <, ...)
end select
end function integer_relation_eval

Функция integer_eval() представляет собой рекурсивную под-


программу:

recursive subroutine integer_eval( x )


type(integer_operand) :: x

if( associated( x%first ) ) call integer_eval( x%first )


if( associated( x%second ) ) call integer_eval( x%second )

select case( x%operation )


case( 0 )
! Ничего не делать.
case( 1 )
x%value = x%first%value + x%second%value
case( 2 )
x%value = x%first%value * x%second%value
case( 3 )
58 Глава 3. Математические абстракции

x%value = x%first%value ** x%second%value


case default
! Ничего не делать.
end select
end subroutine integer_eval

Секрет заключается в перегруженных операциях для производного


типа integer_operand, которые вместо непосредственного выполне-
ния вычислений сохраняют информацию о том, какая операция была
вызвана и с какими операндами:

function integer_add( x, y ) result(add)


type(integer_operand), intent(in), target :: x
type(integer_operand), intent(in), target :: y
type(integer_operand), pointer :: add

allocate( add )

add%operation = 1
add%first => x
add%second => y
end function integer_add

Обратите внимание, что функция возвращает указатель на резуль-


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

type(integer_operand), target :: x
type(integer_operand), target :: y
type(integer_pair_set) :: set

write(*,*) 'Принадлежат ли множеству пар чисел {(x,y) | y**2 = 3*x**2 + 1} &


пары (1,2) и (3,3)?'
set = create_pair_set( y**2 == 3*x**2 + 1, range(x,0,0), range(y,0,0) )

write(*,*) 'Пара (1,2) принадлежит этому множеству? ',


has_element( set, (/1,2/) )
write(*,*) 'Пара (3,3) принадлежит этому множеству? ',
has_element( set, (/3,3/) )
3.4. Отложенные или ленивые вычисления 59

Результат выполнения этой программы:

Принадлежат ли множеству пар чисел {(x,y) | y**2 = 3*x**2 + 1}


пары (1,2) и (3,3)?
Пара (1,2) принадлежит этому множеству? T
Пара (3,3) принадлежит этому множеству? F

Обратите внимание на следующие важные положения:


• Функция range() представляет собой средство для исследо-
вания всех значений x и y в заданном диапазоне, и пары, для
которых выполняется отношение, могут быть добавлены в
кэш. В данном случае при заданном нулевом диапазоне в кэш
не добавляется ничего.
• Функция has_element() просто проверяет, содержится ли за-
данная пара целых чисел в некотором множестве. Это делается
с помощью вычисления сохранённого отношения для задан-
ной пары чисел, значения которых соответствуют двум пере-
менным x и y.
Таким образом, используя функциональные возможности
Fortran 90, можно реализовать стиль программирования, который на
первый взгляд кажется невозможным для компилируемых языков со
статической типизацией, подобных Fortran.
Глава 4.
Управление памятью

Появление динамических массивов и указателей существенно упро-


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

4.1. Динамически изменяемые


массивы
Fortran 90 и последующие версии стандарта предлагают три способа
создания массивов изменяемого размера:
• автоматические массивы (automatic arrays) создаются при вхо-
де в подпрограмму или функцию и автоматически удаляются
при выходе из подпрограммы. Этот способ хорошо подходит
для обработки массивов. Единственный недостаток – такие
массивы часто создаются в стеке программы, следовательно,
не должны иметь слишком большой размер.1
Нечто похожее можно делать со строками символов: длина
локальных строковых переменных в подпрограммах может
динамически изменяться, как показано в разделе 4.4;
• динамические массивы (allocatable arrays) требуют явного при-
менения оператора allocate, но поскольку они не имеют атри-
бута save, компилятор удалит их при возврате из подпрограм-
мы (начиная с версии Fortran 95). Разумеется, вы сами можете
явно удалять такие массивы, если они больше не нужны;
• указатели (pointers) – наиболее гибкий способ работы с па-
мятью. Например, с помощью указателя можно выбирать из
1
Некоторые компиляторы предоставляют ключи для управления размещением ав-
томатических массивов: либо в стеке (высокая скорость доступа), либо в куче (бо-
лее устойчивы к ошибкам).
4.2. Утечки памяти при использовании указателей 61

массива не только диапазоны смежных элементов, но и наборы


элементов, не являющихся смежными:
real, dimension(:,:), allocatable, target :: array
real, dimension(:,:), pointer :: ptr

allocate( array( 20, 20 ) )


ptr => array(1:20:5,3:4)

Здесь переменная ptr указывает на элементы array(1,3),


array(6,3) и т. д., то есть:

ptr(1,1) => array(1,3)


ptr(2,1) => array(6,3)
...
ptr(1,2) => array(1,4)
ptr(2,2) => array(6,4)
...

Но поскольку указатель может также ссылаться на фиксиро-


ванную память и выделяемую автоматически, компилятору
очень трудно, а иногда невозможно определить, можно ли без-
опасно освободить память, на которую указывают подобные
переменные. Другими словами, при использовании перемен-
ных-указателей программист сам отвечает за освобождение
выделяемой памяти.
Начиная с версии Fortran 2003, появились ещё два способа, заслу-
живающих внимания: строки символов с динамически изменяемой
длиной и параметризованные производные типы.
Далее в этой главе будут рассматриваться перечисленные выше
типы массивов.

4.2. Утечки памяти


при использовании указателей
В следующем фрагменте память, выделенная при помощи перемен-
ной-указателя ptr, становится недоступной, потому что указатель
удаляется при выходе из подпрограммы:
subroutine alloc_and_forget( amount )
integer :: amount
real, dimension(:), pointer :: ptr ! Локальная переменная.

allocate( ptr( amount ) )


end subroutine alloc_and_forget
62 Глава 4. Управление памятью

Такие утечки памяти очень трудно обнаружить, и если эта под-


программа вызывается часто, всё больший объем выделяемой памяти
становится недоступным, и так продолжается, пока не будет исчерпа-
на вся память.
Существуют инструментальные средства, позволяющие находить
ошибки подобного рода, например valgrind (см. приложение А). Но
при некоторых условиях не так-то просто избежать этих ошибок, на-
пример, при использовании производных типов, содержащих указа-
тели или компоненты, размещаемые в динамической памяти (см. раз-
дел 4.8).

4.3. Увеличение размера


массива
Достаточно часто возникает необходимость увеличить размер масси-
ва, чтобы продолжать добавление в него новых значений, например,
при чтении входных данных. До версии Fortran 2003 существовал
единственный способ увеличения размера массива:
• создать указатель на массив с новым размером;
• скопировать содержимое старого массива в новый временный
массив;
• освободить память, выделенную старому массиву, затем пере-
направить его указатель на новый временный массив.

Делается это примерно так:

real, dimension(:,:), pointer :: array, tmp


integer :: newsize, oldsize

allocate( tmp( newsize ) )


tmp(1:oldsize) = array
deallocate( array )
array => tmp

Здесь используются указатели вместо динамических массивов, не-


смотря на преимущества последних. В данном контексте использова-
ние динамических массивов возможно, но потребуются две операции
копирования вместо одной.
В версии Fortran 2003 подпрограмма move_alloc() позволяет на-
писать такой код:
4.4. Строки символов с изменяемой длиной 63
real, dimension(:), allocatable :: array, tmp
integer :: newsize, oldsize

allocate( tmp( newsize ) )


tmp(1:oldsize) = array
deallocate( array )
call move_alloc( tmp, array )

В данном случае используются все преимущества динамических


массивов.

4.4. Строки символов


с изменяемой длиной
Как и массивы, локальные строки символов могут иметь длину, кото-
рая определяется во время выполнения. Это показано в следующем
фрагменте кода с реализацией функции index() для поиска подстро-
ки в строке без учёта регистра букв:

integer function index_ignore_case( stringa, stringb )


implicit none

character(len=*), intent(in) :: stringa, stringb


character(len=len(stringa)) :: copya
character(len=len(stringb)) :: copyb

copya = stringa
copyb = stringb

! Преобразование в верхний регистр.


call toupper( copya )
call toupper( copyb )

index_ignore_case = index( copya, copyb )


end function index_ignore_case

Начиная с версии Fortran 2003, можно создавать строки символов


с длиной, определяемой во время выполнения (так называемые стро-
ки с отложенным определением длины (deferred-length strings)):

character(len=:), allocatable :: string


integer :: size

size = ...
allocate( character(len=size) :: string )
64 Глава 4. Управление памятью

Это очень удобно при анализе содержимого некоторого файла или


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

module readline_utility
use iso_fortran_env
implicit none

contains

subroutine readline( lun, line, success )


integer, intent(in) :: lun
character(len=:), allocatable, intent(out) :: line
logical, intent(out) :: success

character(len=0) :: newline

call readline_piece_by_piece( newline )

contains

recursive subroutine readline_piece_by_piece( newline )


character(len=*) :: newline

character(len=10) :: piece
integer :: ierr
integer :: sz

read( lun, '(a)', advance = 'no', size = sz, &


iostat = ierr ) piece

if( ierr /= 0 .and. ierr /= iostat_eor ) then


allocate( character(len=len(newline)) :: line )
line = newline
success = .false.
return
endif

! Достигнут конец строки?


!
if( sz >= len( piece ) ) then
call readline_piece_by_piece( newline // piece )
else
allocate( character(len=len(newline)+sz) :: line )
line = newline // piece(1:sz)
success = .true.
endif
end subroutine readline_piece_by_piece
4.4. Строки символов с изменяемой длиной 65
end subroutine readline
end module readline_utility

Общедоступная подпрограмма readline – всего лишь удобный


интерфейс к функциональности модуля, а всю работу выполняет ре-
курсивная подпрограмма readline_piece_by_piece:
• инструкция read использует непродвигающую форму ввода/
вывода (nonadvancing I/O) для чтения небольшой части теку-
щей строки в файле. По достижении конца строки указатель
позиции в файле перемещается в начало следующей строки
или в конец файла;
• проверив условие достижения конца строки, подпрограмма
либо рекурсивно вызывает сама себя, либо выделяет память
для сохранения всей строки в переменной line. Переменная
sz содержит число прочитанных и сохранённых символов, и
используется для определения конца строки.
Когда подпрограмма рекурсивно вызывает себя, в качестве
аргумента передаётся более длинная строка, которая составля-
ется «на лету» непосредственно перед вызовом, путем объеди-
нения ранее обработанной части строки и нового прочитанно-
го фрагмента.
По достижении конца строки работа завершается, и выполня-
ется выход из подпрограммы с признаком успешного заверше-
ния, вплоть до возврата в вызывающую программу;
• по достижении конца файла или при возникновении каких-ли-
бо ошибок выделяется память для текущей строки, куда сохра-
няется все содержимое, которое удалось прочитать из файла;
• в приведённом примере подпрограмма readline_piece_
by_piece() определена как внутренняя по отношению к
readline  – подпрограмма располагается между инструкци-
ями contains и end subroutine readline. Кроме того, ис-
пользуется стандартный модуль iso_fortran_env с целью
получения доступа к целочисленному параметру iostat_eor,
необходимому для определения условия достижения конца
строки.
Этот пример показывает, как можно использовать различные ме-
тодики для управления памятью. Но вероятнее всего это не самый
эффективный способ чтения строк произвольной длины из файла.
В качестве альтернативного решения можно воспользоваться циклом
do loop, в котором выполняется постепенное выделение памяти для
66 Глава 4. Управление памятью

строки с постоянно увеличивающейся длиной. Преимущество такого


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

4.5. Сочетание автоматических


и динамических массивов
Как уже отмечалось, автоматические массивы очень просты в исполь-
зовании, но применение больших автоматических массивов вызыва-
ет определённые трудности. Ниже предлагается способ преодоления
этих трудностей. Алгоритм вычисления срединного значения (меди-
аны) для набора данных требует их сортировки, но при этом крайне
нежелательно нарушать порядок расположения элементов исходного
массива.2 Следовательно, необходимо скопировать данные и выпол-
нить сортировку полученной копии. Так как массив может иметь лю-
бой размер, реализация различает небольшие массивы данных, для
которых используются автоматические массивы, и массивы данных
больших размеров, для которых явно выделяется рабочий массив
требуемого размера:
subroutine get_median( array, median )
implicit none

real, dimension(:), intent(in) :: array


real, intent(out) :: median

! Произвольно выбранный предельный размер автоматического


! массива.
integer, parameter :: threshold = 2000

real, dimension(min(size(array), threshold)) :: auto_array


real, dimension(:), allocatable :: alloc_array

if( size( array ) < threshold ) then


auto_array = array
call get_median_sub( auto_array, median )
else
allocate( alloc_array(size(array)) )
alloc_array = array
call get_median_sub( alloc_array, median )
endif

contains
2
Существуют алгоритмы вычисления срединного значения без предварительной со-
ртировки, но их применение не соответствует цели данного примера.
4.6. Производительность массивов разных типов 67
subroutine get_median_sub( array, median )
real, dimension(:), intent(in) :: array
real, intent(out) :: median

call sort_data( array )

! Срединное значение массива равно либо элементу,


! находящемуся в середине массива, либо среднему
! значению двух элементов, расположенных слева и
! справа от центра массива.
!
median = 0.5 * (array((size(array)+1)/2) + &
array(size(array)+2/2))
end subroutine get_median_sub
end subroutine get_median

В этом примере подпрограмма get_median() сама выбирает бо-


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

4.6. Производительность
массивов разных типов
Из практического опыта известно, что накладные расходы при ис-
пользовании массивов с атрибутом pointer выше, чем для прочих
массивов, так как указатель может ссылаться на фрагментирован-
ную память, а кроме того допускает возможность совмещения имён
(aliasing, два имени для одного выделенного блока памяти), что пре-
пятствует оптимизации любого типа. Чтобы получить представление
о конкретных накладных расходах, измерим время выполнения про-
стого алгоритма с использованием массивов разных типов:
Код реализации алгоритма:

subroutine compute_mean( data, sz, mean )


real, dimension(:) :: data
integer :: sz
real :: mean

integer :: i

do i = 1,sz
68 Глава 4. Управление памятью

data(i) = i
enddo

mean = sum( data(1:sz) ) / sz


end subroutine compute_mean

Массив data может быть автоматическим массивом, локальным ди-


намическим массивом или локальным указателем на массив. В двух
последних случаях явно выделяется память, поэтому очевидны на-
кладные расходы на операции выделения и освобождения этой памя-
ти. Второй вариант предусматривает передачу массива с атрибутом
allocatable или pointer в вызывающую программу, но не в саму
рассматриваемую подпрограмму.
Числовые значения на рис. 4.1 – это измеренное время выполнения
разных версий подпрограммы 10 000 раз. Результаты воспроизводи-
мы с погрешностью, не превышающей нескольких процентов. Для
нормализации данных на рис. 4.1 использовалось среднее значение
относительно размера массива.
2.0
С передачей
Fixed статического массива
array passed
Allocatable
С передачейarray passed массива
динамического
Pointer
С arrayуказателя
передачей passed на массив
Local automatic
Локальный array
автоматический массив
1.5 Local allocatable
Локальный array массив
динамический
Local pointer
Локальный array на массив
указатель

1.0

0.5

0.0
1.0 10.0 100.0 1000.0 10000.0

Рис. 4.1. Производительность массивов разных типов; показано


нормализованное время выполнения как функция от размера массива
Здесь трудно делать обобщающие выводы, поскольку результаты
измерений существенно отличаются в зависимости от компилятора,
ключей компиляции, и операционной системы. Тем не менее, можно
заметить, что локальные массивы с атрибутом pointer постоянно тре-
буют на 10–20 процентов больше времени для обработки из-за допол-
нительных затрат на операции выделения и освобождения памяти.
4.7. Параметризованные производные типы 69

Также заметны отличия между автоматическими и динамическими


массивами (обозначенные треугольными символами), хотя эти ре-
зультаты зависят от выбора платформы.
Если передавать массивы с разными атрибутами в подпрограмму,
где аргумент-шаблон объявлен без соответствующего атрибута, про-
изводительность становится вполне сопоставимой. То есть, в данном
случае отказ от атрибута pointer оправдан:

real, dimension(:), pointer :: data


integer :: sz
real :: mean

interface
subroutine compute_mean( array, sz, mean )
real, dimension(:) :: array
integer :: sz
real :: mean
end subroutine
end interface

allocate( data(sz) )

call compute_mean( data, sz, mean )

Но не следует забывать, что указатели могут ссылаться на массивы,


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

4.7. Параметризованные
производные типы
В стандарте Fortran 2003 введены так называемые параметризован-
ные производные типы (parametrized derived types) в качестве инстру-
ментов управления памятью. Фактически это является обобщением
механизма разновидностей типов (kind mechanism) из Fortran 90 и
приема объявления длины строки символов, существующего ещё со
времён FORTRAN 77. Например, предположим, что перед нами по-
ставлена задача обработки изображения. Было бы удобно иметь про-
изводный тип, хранящий данные так, что все подробности (например,
размеры изображения) скрыты от пользователя. Это можно сделать с
помощью параметризованных производных типов:
70 Глава 4. Управление памятью

type image_data( rows, columns )


integer, len :: rows, columns
integer, dimension(rows,columns) :: data
end type

Предполагается, что данные изображения представлены как дис-


кретные величины насыщенности (или яркости), то есть в виде от-
дельных целых чисел.
После этого можно объявлять переменные данного типа с конкрет-
ными размерами:

type(image_data(100,100)) :: picture
type(image_data(5,5)) :: mask

Кроме того, размеры можно изменять динамически, в соответствии


с конкретной задачей:3

type(image_data(:,:)), allocatable :: picture


integer :: rows, columns
...
! Чтение размеров изображения.
read( 10, * ) rows, columns
allocate( image_data(rows,columns) :: picture )

Если необходима ещё одна переменная этого типа с такими же


параметрами, можно воспользоваться приемом распределения по ис-
точнику (sourced allocation):

type(image_data(:,:)), allocatable :: picture1, picture2


allocate( picture2, source = picture1 )

Суть распределения по источнику заключается в том, что новая


переменная, размещаемая в памяти, имеет те же размеры (объем па-
мяти), что и исходная переменная, и ей присваивается такое же зна-
чение.4
Переменные параметризованного типа можно передавать в под-
программы практически так же, как передаётся длина символьных
строк:
3
Для определения производного типа можно также воспользоваться kind-
параметрами, но разновидности (kinds) фиксируются во время компиляции, поэто-
му их невозможно переопределить динамически.
4
В Fortran 2008 введена возможность использования исходной переменной только
в качестве «шаблона» («mold»), когда копируются только параметры типа, но не
значение.
4.8. Утечки памяти в производных типах 71
subroutine convolution( picture, mask, result )
type(image_data(*,*)) :: picture, mask, result
...
end subroutine convolution

или для большей ясности:

subroutine convolution( picture, mask, result )


type(image_data(rows=*,columns=*)) :: picture, mask, result
...
end subroutine convolution

Разумеется, для работы с подобными параметризованными типами


необходимо знать действительные значения параметров конкретного
типа. Эти параметры доступны как неявные компоненты:

write( *, * ) 'Размеры изображения: ', &


picture%rows, picture%columns

4.8. Утечки памяти


в производных типах
Для производных типов с компонентами-указателями, используемых
в таких операциях как сложение или присваивание, существует ве-
роятность утечек памяти. В данном разделе описывается простая ме-
тодика, позволяющая избежать утечек памяти. Даже, когда атрибут
pointer заменяется на атрибут allocatable, может оказаться полез-
ным немного изменённый вариант этой методики.5
Рассмотрим три переменные a, b и c производного типа chain с
компонентами-указателями. Переменные используются в следую-
щем выражении, где .concat. представляет операцию, определён-
ную пользователем:

a = b .concat. c

Сам производный тип и связанные с ним операции реализованы в


следующем модуле:
module chains
type chain
5
Майкл Лист (Michael List) и Дэвид Кар (David Car) описывают обобщённую мето-
дику подсчёта ссылок для управления памятью [53].
72 Глава 4. Управление памятью

integer, dimension(:,:), pointer :: values => null()


end type chain

interface assignment(=)
module procedure assign_chain
module procedure assign_array
end interface assignment(=)

interface operator(.concat.)
module procedure concat_chain
end interface operator(.concat.)

contains

subroutine assign_array( ic, jc )


type(chain), intent(inout) :: ic
integer, dimension(:) :: jc

if( associated( ic%values ) ) deallocate( ic%values )


allocate( ic%values(1:size(jc)) )
ic%values = jc
end subroutine assign_array

subroutine assign_chain( ic, jc )


type(chain), intent(inout) :: ic
type(chain), intent(in) :: jc

if( associated( ic%values ) ) deallocate( ic%values )


allocate( ic%values(1:size(jc%values)) )
ic%values = jc%values
end subroutine assign_chain

function concat_chain( ic, jc )


type(chain), intent(in) :: ic, jc
type(chain) :: concat_chain
integer :: nic, njc

nic = size( ic%values )


njc = size( jc%values )

allocate( concat_chain%values(1:nic+njc) )
concat_chain%values(1:nic) = ic%values(1:nic)
concat_chain%values(nic+1:nic+njc) = jc%values(1:njc)
end function concat_chain
end module chains

Производный тип представляет цепочку целых чисел, которая


может быть увеличена с помощью операции .concat., результатом
которой является новый элемент данных типа type(chain), содер-
4.8. Утечки памяти в производных типах 73

жащий объединённые массивы из двух операндов. При присваивании


переменной типа chain другой переменной того же типа выполняет-
ся копирование массива значений исходной переменной.
Когда переменной типа chain присваивается новое значение, ста-
рый блок памяти обязательно должен быть освобождён, после чего
выделяется новый блок памяти требуемого размера (как показано в
подпрограммах assign_array и assign_chain). В противном случае
образовались бы две ссылки на один блок памяти или этот блок стал
бы вообще недоступным. Но дело в том, что программа не будет ос-
вобождать память, выделенную во временном объекте, создаваемом
операцией .concat., так как неизвестно, можно ли безопасно осво-
бодить эту память.
Поэтому код:

a = b .concat. c

вполне допустим, но приводит к утечке памяти.


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

call concatenate( a, b, c )

с инструкцией

a = b .concat. c

или с инструкцией

a = concat( b, c )

Наилучшим решением, позволяющим использовать операции,


подобные .concat., практически без утечек памяти, является мар-
кировка (mark) производных типов, благодаря которой выделенная
память может быть освобождена сразу, как только она станет ненуж-
ной. Необходимо лишь немного изменить определение производного
типа:

type chain
integer, dimension(:), pointer :: values => null()
74 Глава 4. Управление памятью

logical :: tmp = .false.


end type chain

При использовании этого нового типа функция concat_chain()


может пометить свой результат как временный. Теперь все функции
в модуле смогут проверить, являются ли их аргументы временными,
и безопасно освободить память, если временные аргументы не будут
использоваться в дальнейшем:

function( concat_chain( ic, jc )


type(chain), intent(in) :: ic, jc
type(chain) :: concat_chain
integer :: nic, njc

nic = size(ic%values)
njc = size(jc%values)

allocate( concat_chain%values(1:nic+njc) )
concat_chain%values(1:nic) = ic%values(1:nic)
concat_chain%values(nic+1:nic+njc) = jc%values(1:njc)

concat_chain%tmp = .true. ! Объект помечен как временный.

call cleanup( ic, .true. ) ! Освобождение памяти временных


! аргументов.
call cleanup( jc, .true. )
end function concat_chain

Аналогичные действия выполняются в подпрограммах assign_


array() и assign_chain().
Задача подпрограммы cleanup() – скрыть подробности освобож-
дения памяти, ранее выделенной для внутренних массивов:

subroutine cleanup( ic, only_tmp )


type(chain) :: ic
logical :: only_tmp

if( .not. only_tmp .or. ic%tmp ) then


if( associated( ic%values ) ) deallocate( ic%values )
endif
end subroutine cleanup

При использовании этой методики ответственность за устранение


всех возможных утечек памяти ложится на программиста, пишущего
4.8. Утечки памяти в производных типах 75

и использующего вышеупомянутые модули. Программист непремен-


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

type chain
integer, dimension(:), allocatable :: values
logical :: tmp = .false.
end type chain

Объединение двух таких переменных становится проще, если


воспользоваться механизмом автоматического выделения памяти в
Fortran 2003:

function concat_chain( ic, jc )


type(chain), intent(in) :: ic
type(chain), intent(in) :: jc
type(chain) :: concat_chain

concat_chain%values = (/ ic%values, jc%values /)


end function concat_chain

Но и в подпрограмме присваивания можно избежать операций яв-


ного выделения памяти и копирования, если правая часть представ-
ляет собой временный объект:

subroutine assign_chain( ic, jc )


type(chain), intent(inout) :: ic
type(chain) :: jc

if( jc%tmp ) then


call move_alloc( jc%values, ic%values )
else
ic%values = jc%values ! Автоматическое выделение
! памяти.
endif
ic%tmp = .false.
end subroutine assign_chain
76 Глава 4. Управление памятью

4.9. Производительность
и доступ к памяти
Ещё один заслуживающий внимания аспект управления памятью:
как именно осуществляется доступ к памяти. В 1982 году Меткалф
(Metcalf) в своей книге [60] определил правило, сохранявшее акту-
альность в течение нескольких десятилетий вплоть до появления
самых современных компьютеров, в соответствии с которым локаль-
ность (locality) доступа к памяти является самым главным факто-
ром, влияющим на производительность программы. Причина в том,
что операции доступа к памяти несколько медленнее по сравнению с
числовыми операциями. Было предложено аппаратное решение этой
проблемы в виде нескольких уровней кэш-памяти с разными разме-
рами и скоростями доступа. Эффективное использование этих кэшей
является задачей компилятора, который должен предоставлять дан-
ные для выполнения операций настолько быстро, насколько это воз-
можно [7], [32].
Программист может внести свой вклад в повышение эффективно-
сти, аккуратно применяя апробированные методики доступа к памя-
ти. Следующая программа обеспечивает доступ к элементам матри-
цы, расположенным в трёх разных строках и столбцах [31]:

program measure_matrix
implicit none
double complex, allocatable :: a(:,:)
integer :: i, j, l, m, n
integer :: t1, t2, rate

write(*,*) 'Введите степень 2 '


read(*,*) n

call system_clock( count_rate = rate )

allocate( a(n+4,n+4) )

! Устранение небольшого эффекта кэширования, в результате которого


! выполнение первой итерации требует намного больше времени.
!
m = n - 4
do l = 1,max(1,100000000/(n*n))
call test( a, m, n-4 )
enddo

do m = n-4, n+4
4.9. Производительность и доступ к памяти 77
a = 1.0d0

call system_clock( t1 )
do l = 1,max(1,100000000/(n*n))
call test( a, m, n-4 )
enddo
call system_clock( t2 )
write(*,'(i4,f8.3)') m, (t2 - t1) / real( rate )
enddo

contains
subroutine test( a, m, n )
! Проход в "неправильном" порядке.
integer :: m, n
double complex :: a(m,m)

integer :: i, j

do i = 2,n-1
do j = 2,n-1
a(i,j) = (a(i+1,j-1) + a(i-1,j+1)) * 0.5
enddo
enddo
end subroutine
end program

Таблица 4.1. Результаты измерений времени как функции


от размера матрицы. Результаты для размеров, являющихся
степенями 2, выделены жирным

Размер Время Размер Время Размер Время Размер Время Размер Время

28 0.156 60 0.218 124 0.250 252 0.266 508 0.640


29 0.156 61 0.219 125 0.266 253 0.265 509 0.641
30 0141 62 0.203 126 0.250 254 0.266 510 0.672
31 0.172 63 0.219 127 0.265 255 0.375 511 0.719
32 0.156 64 0.391 128 0.500 256 0.656 512 0.750
33 0.140 65 0.218 129 0.250 257 0.297 513 0.781
34 0.172 66 0.219 130 0.266 258 0.266 514 0.687
35 0.141 67 0.219 131 0.250 259 0.281 515 0.657
36 0.156 68 0.219 131 0.266 260 0.281 516 0.640

В табл. 4.1 показаны результаты измерения времени выполнения


программы. В случаях, когда размер матрицы равен степени числа 2,
операция выполняется приблизительно в два раза дольше, чем во всех
остальных случаях. Это следствие промахов кэша (cache misses). Раз-
78 Глава 4. Управление памятью

мер столбцов матрицы конфликтует с размером кэша, поэтому вре-


менное хранение части данной матрицы в более быстрой кэш-памяти
становится неэффективным.
При достаточно больших размерах столбца описанный выше эф-
фект исчезает, как можно видеть в нижней части табл. 4.1.6
Этот недостаток можно устранить путём обхода матрицы по столб-
цам (column first), а не по строкам (row first), или используя нечётные
размеры матрицы.
Другим примером является поиск в связанном списке. Связан-
ные списки достаточно удобны для быстрого выполнения операций
вставки и удаления данных, но фрагменты памяти, занимаемые раз-
личными элементами списка, не обязательно размещаются рядом.
Это означает, что кэширование блоков памяти, скорее всего, будет
менее эффективным, чем при использовании массивов. Следователь-
но, поиск элемента в связанном списке может занимать в два или в
три раза больше времени, чем поиск элемента в обычном массиве, что
подтверждается данными в табл. 4.2.
Таблица 4.2. Результаты измерений времени поиска элемента в
списке при использовании различных структур данных
Структура данных Время, мс
Простой массив 1.55
Список с уплотнением 2.30
Список с неравномерно размещёнными элементами 4.97

Оба связанных списка содержат элементы, определённые следую-


щим образом:

type linked_list
real :: value
type(linked_list), pointer :: next => null()
end type linked_list

Элементы для списка с уплотнением размещались в цикле с пред-


положительным выделением смежных фрагментов памяти, в то вре-
мя как для второго списка между элементами вставлялись дополни-
тельные «пустоты»:
6
Данный эффект зависит от конкретного компилятора и ключей компиляции. По-
казанные здесь результаты были получены с помощью компилятора Intel Fortran.
При использовании компилятора gfortran измеренные времена выполнения имели
большие значения, и описываемый эффект наблюдался не столь явно.
4.9. Производительность и доступ к памяти 79
do i = 2,size(data)
allocate( element )
allocate( dummy(11*i) )
element%value = data(i)
plist%next => element
plist => element
enddo

То есть, элементы второго списка располагаются в памяти доста-


точно далеко друг от друга, из-за чего использование кэш-памяти не
даёт никакого преимущества.
Глава 5.
Проблема интерфейса

В этой главе основное внимание сконцентрировано на том, как сде-


лать обычные штатные средства (вычисления/программирования)
достаточно обобщёнными, чтобы они стали полезными для широкого
круга приложений, но сохранили бы уровень специализации, необхо-
димый для их практического применения. Примерами могут послу-
жить библиотеки для интегрирования простых дифференциальных
уравнений и чтения XML-файлов (см. раздел 7.4). Если говорить о
втором примере более конкретно: XML-файлы можно читать фраг-
ментами и напрямую работать с полученными данными или сохра-
нить эти данные в подходящей структуре для дальнейшего обраще-
ния к ним – два подхода, реализованные в механизмах SAX1 и DOM2.
Сейчас мы рассмотрим разные реализации более простой задачи:
интегрирование функции на заданном интервале.3 Здесь для нас важ-
ны не подробности вычислений, хотя сами по себе они достаточно ин-
тересны, а подходящие способы интегрирования регулярной функ-
ции, зависящей от одного или нескольких параметров. Например,
способы интегрирования функции f:

f(x) = e–ax cos bx (a,b >= 0) (5.1)

на интервале [0,10].
Общая библиотека методов интегрирования может содержать та-
кую подпрограмму:
module integration_library
implicit none

contains
1
Simple API for XML
2
Document Object Model
3
Оливейра (Oliveira) и Стюарт (Stewart) [68] назвали это задачей среды (environment
problem). Кроме того, эта задача довольно подробно рассматривается в [65].
Проблема интерфейса 81
subroutine integrate_trapezoid( f, xmin, xmax, steps, result )
interface
real function f( x )
real, intent(in) :: x
end function f
end interface

real, intent(in) :: xmin, xmax


integer, intent(in) :: steps
real, intent(out) :: result

integer :: i
real :: x, deltx

if( steps <= 0 ) then


result = 0.0
return
endif

deltx = (xmax - xmin) / steps


result = (f(xmin) + f(xmax)) / 2.0
do i = 1,steps-1
x = xmin + i * deltx
result = result + f(x)
enddo
result = result * deltx
end subroutine integrate_trapezoid
end module integration_library

Сразу очевидны ограничения такой реализации: интерфейс для


любой подпрограммы, вычисляющей математическую функцию f,
является неизменяемым (неадаптируемым), поэтому не позволяет
передавать какие-либо параметры, например, a и b из формулы 5.1.
Если нельзя отказаться от приведённой реализации, остаются
лишь две возможности:
• включать значения параметров прямо в реализацию функ-
ции f;
• использовать «пул данных» (блок COMMON или переменные
модуля), доступный как из подпрограммы реализации функ-
ции f, так и вне её, чтобы можно было выбрать параметры пе-
ред вызовом подпрограммы интегрирования.
Если допускается изменение реализации библиотеки, можно ор-
ганизовать прямую передачу дополнительных параметров или вос-
пользоваться такой методикой, как например, передача данных в об-
ратном направлении (reverse communication).
82 Глава 5. Проблема интерфейса

Далее все эти методы будут рассматриваться с точки зрения четы-


рёх основных стандартов языка Fortran: FORTRAN 77, Fortran 90/95,
Fortran 2003 и Fortran 2008.

5.1. Подстановка параметров


Если нужно лишь несколько значений параметров a и b, можно про-
сто написать специализированные версии функции f и передавать их
в подпрограмму интегрирования:4
c
c Определение интегрируемой функции.
c
real function f( x )
real x

real a, b
save a, b
data a, b / 1.0, 2.0 /

f = exp(-a*x) * cos(b*x)
end

Более сложный способ – чтение параметров из файла:


c
c Определение интегрируемой функции.
c
real function f( x )
real x

logical first
real a
real b
save a, b, first
data first / .true. /

if( first ) then


first = .false.
open( 10, file = 'function_values.inp' )
read( 10, * ) a
read( 10, * ) b
4
Следующий пример почти полностью соответствует стандарту FORTRAN 77 (ис-
пользование букв нижнего регистра формально не разрешается; в последующих
примерах встречаются имена с длиной более 6 символов). Этот стиль не будет при-
меняться постоянно, кроме случаев, когда необходимо особо выделить какое-либо
свойство стандарта FORTRAN 77, которое заменено более современными средства-
ми в стандартах, опубликованных позже.
5.1. Подстановка параметров 83
close( 10 )
endif

f = exp(-a*x) * cos(b*x)
end

Здесь недостатком является невозможность передать в программу


имя файла, поэтому содержимое файла читается только во время пер-
вого прохода (не существует способа объединить параметризованную
функцию с различными вариантами значений).
Тем не менее, в FORTRAN 77 имеется такое малоизвестное сред-
ство, как оператор entry, которым можно воспользоваться в данном
случае:

c
c Определение интегрируемой функции.
c
real function f( x )
real x

logical first
real a
real b
save a, b

f = exp(-a*x) * cos(b*x)
return
c
c Обеспечение доступа к параметрам A и B.
c (Установка значения функции f – синоним для
c данной функции setab в наглядной форме.)
c
entry setab( ain, bin )
real ain, bin
a = ain
b = bin
f = 0.0
end

Такой подход обеспечивает доступ к переменным a и b средствами


альтернативного интерфейса, определённого с помощью оператора
entry [62]. Его можно использовать следующим образом:

program integrate_function

real xmin, xmax, result, dummy


integer steps
84 Глава 5. Проблема интерфейса

external f
c
c Установка значений параметров перед вызовом подпрограммы
c интегрирования (формально setab является функцией, такой же
c как f, поэтому setab можно вызывать как функцию).
c
dummy = setab( 1.0, 2.0 )

xmin = 0.0
xmax = 10.0
steps = 10

call integrate_trapezoid( f, xmin, xmax, steps, result )


write(*,*) 'Результат интегрирования: ', result
end

Преимущества этого способа очевидны: нужно лишь написать ре-


ализацию самой функции, возможно, с некоторыми дополнениями,
чтобы сделать её немного более гибкой. Значения параметров доступ-
ны только для этой функции (то есть, являются полностью приват-
ными параметрами).
Единственный недостаток заключается в необходимости реализа-
ции нескольких специализированных версий или же придётся созда-
вать обходные пути для установки значений параметров. Кроме того,
если потребуется вывести более подробную информацию,5 например,
при проходе через критическую (сингулярную) точку, следствием
чего становится ненадёжность результата, сделать это будет не так-то
просто.

5.2. Использование пула данных


Пример с оператором entry похож на второй способ, обсуждае-
мый ниже: использование блоков COMMON или переменных модуля.
В FORTRAN 77 подобный подход выглядел так:

c
c Определение интегрируемой функции.
c
real function f( x )
real x
real a, b
common /fparam/ a, b

5
Это очень важно, если описываемый способ применяется для решения других ти-
пов задач программирования/вычисления.
5.2. Использование пула данных 85
f = exp(-a*x) * cos(b*x)
end

Следующая программа демонстрирует обычный способ создания и


использования интерфейса посредством блоков COMMON:
program integrate_function
real xmin, xmax, result
integer steps
external f
c
c Необходимо повторить определение блока COMMON
c для доступа к параметрам функции
c
real a, b
common /fparam/ a, b
c
c Установка значений параметров перед вызовом подпрограммы
c интегрирования
c
a = 1.0
b = 2.0

xmin = 0.0
xmax = 10.0
steps = 10
call integrate_trapezoid( f, xmin, xmax, steps, result )

write(*,*) 'Результат интегрирования: ', result


end

Однако, блоки COMMON имеют плохую репутацию:


• в каждую подпрограмму, использующую блок COMMON, при-
ходится включать один и тот же фрагмент исходного кода с
определением блока;
• в одном и том же блоке COMMON, размещаемом в разных ме-
стах, допускается использовать разные имена и даже разные
типы переменных (неизменным остаётся только имя блока),
поскольку переменные в блоке всего лишь предоставляют до-
ступ к некоторым фрагментам памяти.

Данные в модулях
Переменные модуля намного надёжнее: они определяются в одном
месте – в модуле – и доступ к ним можно получить, подключив этот
модуль. В Fortran 90 предпочтительнее выглядит следующая реали-
зация:
86 Глава 5. Проблема интерфейса

module functions
implicit none
!
! Общедоступные параметры.
!
real :: a, b
contains
real function f( x )
real, intent(in) :: x

f = exp(-a*x) * cos(b*x)
end function f
end module functions

Следующая программа демонстрирует использование данной


функции:
program integrate_function

use integration_library
use functions

implicit none

real :: xmin, xmax, result


integer :: steps
!
! Установка значений параметров перед вызовом функции интегрирования.
!
a = 1.0
b = 2.0

xmin = 0.0
xmax = 10.0
steps = 10
call integrate_trapezoid( f, xmin, xmax, steps, result )

write(*,*) 'Результат интегрирования: ', result


end program integrate_function

При использовании модулей компилятор получает возможность


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

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


другим потоком.
Подробности регулирования и управления зависят от конкретной
реализации многопоточности. Например, ниже приводится возмож-
ное решение описанной проблемы при использовании библиотеки
OpenMP (также см. главу 12):
module functions
implicit none
!
! Общедоступные параметры.
! Для простоты предполагается наличие не более 10 потоков.
!
real, dimension(10), private :: a
real, dimension(10), private :: b
contains
subroutine setab( ain, bin )
real, intent(in) :: ain, bin
integer :: thid

thid = omp_get_thread_num()
a(thid) = ain
b(thid) = bin
end subroutine setab

real function f( x )
real, intent(in) :: x
integer :: thid

thid = omp_get_thread_num()
f = exp(-a(thid)*x) * cos(b(thid)*x)
end function f
end module functions

Вместо единого набора переменных, которые могут использовать-


ся несколькими потоками (вероятнее всего, некорректно), каждому
потоку передается собственный набор переменных. К сожалению,
такой подход усложняет реализацию, особенно если количество па-
раметров велико:
program integrate_function
use integration_library
use functions

implicit none

real :: xmin, xmax, result


real :: a, b
88 Глава 5. Проблема интерфейса

integer :: steps, thid

xmin = 0.0
xmax = 10.0
steps = 10

!$omp parallel
!$omp private( a, b, result, thid )
!
! Установка значений параметров для каждого конкретного потока.
!
thid = omp_get_thread_num()
a = 1.0 * thid
b = 2.0 * thid
call setab( a, b )

call integrate_trapezoid( f, xmin, xmax, steps, result )


write(*,*) 'Интегрирование для a = ', a, ' b = ', b, &
' -- результат: ', result
!$omp end parallel
end program integrate_function

Внутренние подпрограммы
С вводом в действие стандарта Fortran 2008 появилась возможность
передачи внутренних подпрограмм (internal routines) в качестве дей-
ствительных аргументов. На этом основано новое решение, также
обеспечивающее безопасность потоков, – создание удобного интер-
фейса для функции, определяемой в рассматриваемом примере. Так
как константы a и b размещены в вызывающей программе или под-
программе, внутренняя подпрограмма feval, передаваемая в подпро-
грамму, выполняющую интегрирование, может напрямую обращать-
ся к этим константам. В свою очередь feval вызывает функцию f,
передаваемую как аргумент, которая содержит реализацию математи-
ческой функции. Главная программа адаптирует интерфейс функции
f к виду, требуемому подпрограммой интегрирования:

subroutine integrate_function( f, xmin, xmax, a, b, result )


use integration_library

implicit none

interface
real function f( x, a, b )
real, intent(in) :: x, a, b
end function f
5.3. Передача дополнительных аргументов 89
end interface

real, intent(in) :: a, b, xmin, xmax


real, intent(out) :: result
integer :: steps

steps = 10
call integrate_trapezoid( feval, xmin, xmax, steps, result )
contains
real function feval( x )
real, intent(in) :: x
feval = f( x, a, b )
end function feval
end subroutine integrate_function

5.3. Передача дополнительных


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

Массив параметров
Передача массива параметров – самое простое решение:

subroutine integrate_trapezoid( f, params, xmin, xmax, steps, result )


...
interface
real function f( x, params )
real, intent(in) :: x
real, dimension(:), intent(in) :: params
end function f
end interface

real, dimension(:) :: params


...
end subroutine

Функция f может быть реализована вполне очевидным способом:

real function f( x, params )


real, intent(in) :: x
90 Глава 5. Проблема интерфейса

real, dimension(:), intent(in) :: params

f = exp(-params(1)*x) * cos(params(2)*x)
end function f

Но если набор параметров состоит не только из вещественных чи-


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

Использование функции transfer()


В FORTRAN 77 не так много средств, способных помочь в решении
рассматриваемой задачи, но в Fortran 90/95 появилась возможность
использовать функцию transfer() для преобразования произволь-
ных данных в массив чисел с плавающей точкой и наоборот:

type function_parameters
real :: a
real :: b
end type function_parameters

type(function_parameters) :: params

! Определение типа для функции transfer().


real, dimension(1) :: real_array
...
call integration_trapezoid( f, transfer( params, real_array ), &
xmin, xmax, steps, result )
...

Это рабочее, но не самое удачное решение: ответственность за


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

program integrate

type function_parameters
real :: a, b
6
Подобный подход и решение с помощью внутренней подпрограммы часто называют
шаблоном проектирования «фасад» (Façade pattern) [54].
5.3. Передача дополнительных аргументов 91
end type function_parameters

type(function_parameters) :: params
...
call integration_trapezoid_ab( f, params, xmin, xmax, steps, result )
...
contains
!
! Этот код при необходимости можно перенести во включаемый файл,
! чтобы скрыть подробности реализации от пользователя.
!
subroutine integration_trapezoid_ab( f, params, xmin, xmax, &
steps, result )
...
type(function_parameters) :: params

! Определение типа для функции transfer().


real, dimension(1) :: real_array

call integration_trapezoid( f, transfer(params,real_array), &


xmin, xmax, steps, result )
end subroutine
end program integrate

Процедуры, связанные с типом


Стандарт Fortran 2003 предоставляет гораздо больше возможностей
для решения рассматриваемой задачи более красивым способом
([65], также см. главу 11):
module integration_library
implicit none

type, abstract :: user_function


! Данных нет — это резервирующий шаблон.
contains
procedure(function_evaluation), deferred, &
pass(params) :: eval
end type user_function

abstract interface
real function function_evaluation( x, params )
import :: user_function
real :: x
class(user_function) :: params
end function function_evaluation
end interface

contains
92 Глава 5. Проблема интерфейса

subroutine integrate_trapezoid( params, xmin, xmax, steps, result )


class(user_function) :: params
real, intent(in) :: xmin, xmax
integer, intent(in) :: steps
real, intent(out) :: result

integer :: i
real :: x
real :: deltx

if( steps <= 0 ) then


result = 0.0
return
endif

deltx = (xmax - xmin) / steps


result = (params%eval(xmin) + params%eval(xmax)) / 2.0

do i = 2,steps
x = xmin + (i - 1) * deltx
result = result + params%eval(x)
enddo
result = result * deltx
end subroutine integrate_trapezoid
end module integration_library

Следующий пример иллюстрирует использование этого модуля.


Обратите внимание, что реализация функции f теперь является ча-
стью типа user_function:

module functions
use integration_library
implicit none

type, extends(user_function) :: my_function


real :: a
real :: b
contains
procedure, pass(params) :: eval => f
end type my_function
contains
real function f( x, params )
real, intent(in) :: x
class(my_function) :: params

f = exp(-params%a * x) * cos(params%b * x)
end function f
end module functions
5.3. Передача дополнительных аргументов 93

Теперь, вместо имени интегрируемой функции передаётся произ-


водный тип, который содержит заданную интегрируемую функцию:

program test_integrate
use integration_library
use functions

implicit none

type(my_function) :: params
real :: xmin, xmax, result
integer :: steps

params%a = 1.0
params%b = 2.0
xmin = 1.0
xmax = 10.0
steps = 10

call integrate_trapezoid( params, xmin, xmax, steps, result )


write(*,*) 'Результат интегрирования: ', result
end program test_integrate

Абстрактный производный тип user_function предоставляет


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

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

module integration_library
implicit none

type, abstract :: function_parameters


94 Глава 5. Проблема интерфейса

procedure(eval), pointer, pass(params) :: feval


end type function_parameters

abstract interface
real function eval( x, params )
import :: function_parameters
class(function_parameters) :: params
end function eval
end interface
contains
subroutine integrate_trapezoid( params, xmin, xmax, steps, result )
interface
real function f( x, params )
import function_parameters
real, intent(in) :: x
class(function_parameters) :: params
end function f
end interface

class(function_parameters) :: params

... (тот же код, что и в предыдущей реализации) ...

end subroutine integrate_trapezoid


end module integration_library

После этого можно менять интегрируемые функции, не создавая


для каждой новый тип:

module functions
use integration_library
implicit none

type, extends(function_parameters) :: my_parameters


real :: a, b
end type my_parameters
contains
real function f( x, params )
real, intent(in) :: x
class(my_parameters) :: params

f = exp(-params%a * x) * cos(params%b * x)
end function f

!
! Функция g() не использует параметр b, но в остальном
! имеет точно такие же требования к данным,
! поэтому для неё можно использовать тип my_parameters.
!
5.4. Управляющие конструкции 95
real function g( x, params )
real, intent(in) :: x
class(my_parameters) :: params

g = params%a * x
end function g
end module functions

program test_integrate
use integration_library
use functions
implicit none

type(my_parameters) :: params
real :: xmin, xmax, result
integer :: steps

params%a = 1.0
params%b = 2.0
xmin = 1.0
xmax = 10.0
steps = 10

params%feval => f ! Первая функция.


call integrate_trapezoid( params, xmin, xmax, steps, result )
write(*,*) 'Результат интегрирования функции f: ', result

param%feval => g ! Вторая функция.


call integrate_trapezoid( params, xmin, xmax, steps, result )
write(*,*) 'Результат интегрирования функции g: ', result
end program test_integrate

5.4. Управляющие конструкции


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

module integration_library
implicit none

type integration_parameters
96 Глава 5. Проблема интерфейса

private
integer :: state = -1 ! Параметры не инициализированы.
integer :: steps
integer :: i
real :: x, xmin, xmax, deltx
real :: result, sum
end type integration_parameters

!
! Параметры, описывающие действия.
!
integer, parameter :: get_value = 1
integer, parameter :: conpleted = 2
integer, parameter :: failure = 3
contains
subroutine set_parameters( data, xmin, xmax, steps )
type(integration_parameters) :: data
real, intent(in) :: xmin, xmax
integer, intent(in) :: steps

if( steps <= 0 ) then


return
endif

data%xmin = xmin
data%xmax = xmax
data%steps = steps
data%state = 1
data%sum = 0.0
data%i = 0
data%deltx = (xmax - xmin) / steps
end subroutine set_parameters

subroutine integrate_trapezoid( data, value, result, action, x )


type(integration_parameters) :: data
real, intent(in) :: value
real, intent(out) :: result
integer, intent(out) :: action
real, intent(out) :: x

result = 0.0
if( data%state == -1 ) then
action = failure
return
endif

!
! Разделение процесса вычисления на отдельные шаги.
!
select case( data%state )
5.4. Управляющие конструкции 97
case(1)
x = data%xmin
action = get_value
data%state = 2
case(2)
data%result = 0.5 * value
x = data%xmax
action = get_value
data%state = 3
case(3)
data%result = data%result + 0.5 * value
x = data%xmin + data%deltx
data%i = 1
action = get_value
data%state = 4
case(4)
data%result = data%result + value
if( data%i < data%steps - 1 ) then
data%i = data%i + 1
x = data%xmin + data%i * data%deltx
action = get_value
data%state = 4
else
result = data%result * data%deltx
action = completed
endif
case default
write(*,*) 'Программная ошибка – &
неизвестное состояние: ', &
data%state
stop
end select
end subroutine integrate_trapezoid
end module integration_library

Данная подпрограмма должна использоваться как часть специали-


зированной управляющей структуры, например:

module functions
implicit none
contains
real function f( x, a, b )
real, intent(in) :: x, a, b

f = exp(-a*x) * cos(b*x)
end function f
end module functions

program test_integrate
98 Глава 5. Проблема интерфейса

use integration_library
use functions
implicit none

real :: xmin, xmax, result, value, x


real :: a, b
integer :: steps
integer :: action

type(integration_parameters) :: data

a = 1.0
b = 2.0
xmin = 1.0
xmax = 10.0
steps = 10

call set_parameters( data, xmin, xmax, steps )

do
call integrate_trapezoid( data, value, result, action, x )

select case( action )


case( get_value )
value = f( x, a, b )
case( completed )
exit
case( failure )
write(*,*) 'Ошибка: некорректные аргументы'
case default
write(*,*) 'Программная ошибка: &
неизвестная операция – ', &
action
end select
enddo
write(*,*) 'Результат интегрирования: ', result
end program test_integrate

Мы превратили процедуру интегрирования в конечный автомат


(finite state machine), выполняющий цикл до достижения конечно-
го состояния, где мы получаем итоговый результат. Код, выполняю-
щий эту работу, полностью независим от вычисления интегрируемой
функции. Более того, поскольку выполнением требуемых этапов
(шагов) управляет пользовательский код (в основной программе),
такой подход позволяет не задумываться о проблеме корректности
поведения функции на всём интервале интегрирования и обходиться
без библиотек общего назначения, предоставляющих средства кон-
троля (или какие-либо другие средства и возможности). Кроме того,
5.4. Управляющие конструкции 99

если потребуется написать несколько подпрограмм для выполнения


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

program test_integrate
use functions
implicit none

real :: xmin, xmax, result, value, x


real :: a, b
integer :: steps
integer :: action

a = 1.0
b = 2.0
xmin = 1.0
xmax = 10.0
steps = 10

call integrate_trapezoid_ab( f, xmin, xmax, steps, result )


write(*,*) 'Результат интегрирования: ', result
contains
!
! Следующий код можно поместить во включаемый файл (INCLUDE file)
! для любой функции, использующей два параметра a и b.
!
! Параметры a и b передаются из вмещающего программного компонента.
!
subroutine integrate_trapezoid_ab( f, xmin, xmax, steps, result )
use integration_library

... тот же код, что и в предыдущем примере ...

end subroutine integrate_trapezoid_ab


end program test_integrate

В данном случае использование такого подхода, иногда называе-


мого интерфейсом обратной связи (reverse communication), выглядит
100 Глава 5. Проблема интерфейса

излишне запутанным, но в других ситуациях он может быть вполне


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

Библиотека OpenMP
Для решения описанной выше задачи среды (environment problem)
можно воспользоваться методиками параллельных вычислений. Это
может показаться несколько странным, так как обычно параллель-
ные вычисления применяют для улучшения производительности
программ, но кроме того такие методики предоставляют разделённые
программные среды, а это как раз то, что нужно.
Необходимо создать два потока: один для выполнения интегри-
рования и другой для вычисления значения функции в заданной ко-
ординате x. Библиотека OpenMP позволяет сделать это с помощью
директивы section, которая используется в следующей программе:

program test_integrate
use integration
use functions
implicit none

real :: x, a, xbegin, xend, result


integer :: steps
logical :: next
type(integration_status) :: status

a = 1.0
xbegin = 0.0
xend = 10.0
steps = 10

! Все объекты могут использоваться совместно.

call start_integration( status )

!
! Первая секция предназначена для интегрирования функции,
! вторая – для вычисления значения этой функции.
!

!$omp parallel sections

!$omp section
call integrate_trapezoid( status, xbegin, xend, &
5.4. Управляющие конструкции 101
steps, result )

!$omp section
do
call get_next( status, x, next )
if( .not. next ) exit
call set_value( status, f(x,a) )
enddo

!$omp end parallel sections

write(*,*) 'Результат интегрирования: ', result


end program test_integrate

Сложность такого подхода заключается в синхронизации двух по-


токов, но все необходимые действия по синхронизации скрыты в под-
программах integrate() и get_next():

subroutine get_next( status, x, next )


type(integration_status) :: status
real :: x
logical :: next

!
! Синхронизация обращений к памяти.
!
!$omp flush
if( status%done ) then
next = ,false.
else
do while( .not. status%next )
!$omp flush
enddo

!
! Это одно из мест, где потоки могут воздействовать
! друг на друга, поэтому необходимо использование
! критической секции.
!
!$omp critical
x = status%xvalue
status%next = .false.
status%ready = .false.
!$omp end critical

next = .true.
endif
end subroutine get_next
102 Глава 5. Проблема интерфейса

В подпрограмме integrate_trapezoid() синхронизация осу-


ществляется аналогично.

5.5. Работа с числовыми


значениями различной точности
До сего момента мы рассматривали только вопросы передачи произ-
вольного набора данных в функцию. Вторым важным свойством обоб-
щённого интерфейса является возможность выполнения вычислений
с одинарной или двойной точностью. В настоящее время Fortran не
позволяет определить подпрограмму или функцию, а точность вы-
числений задать позже. Для каждой необходимой степени точности
программист вынужден определять отдельную подпрограмму.
В стандарте Fortran 90 был введён атрибут kind, который дал воз-
можность хотя бы собрать все определения точности в одном месте:

integer, parameter :: wp = kind(1.0)


real(wp) :: x

Здесь определён параметр wp (working precision – действующая


точность), который получает значение, соответствующее одинарной,
или обычной точности – kind (разновидность типа) литеральной
константы 1.0.
Эту возможность, а также оператор include можно использовать
для определения подпрограммы с «обобщённой» разновидностью
типа:

module single
implicit none

! Константа с одинарной (обычной) точностью.


integer, parameter :: wp = kind(1.0)

include "generic.f90"
end module single

module double
implicit none

! Константа с двойной точностью.


integer, parameter :: wp = kind(1.0d0)

include "generic.f90"
5.6. Резюме 103
end module double

module unite
use single
use double
end module unite

Включаемый файл «generic.f90» содержит весь код, независимый


от конкретной разновидности типа:
interface print_kind
module procedure print_kind_wp
end interface
private :: print_kind_wp

contains
subroutine print_kind_wp( x )
real(kind=wp), intent(in) :: x

write(*,*) 'Разновидность типа (kind): ', kind(x)


end subroutine print_kind_wp

Следующая программа показывает, как это всё работает:

program test_kinds
use unite

call print_kind( 1.0 )


call print_kind( 1.0d0 )
end program test_kinds

Здесь следует отметить, что из-за размещения двух версий под-


программы print_kind_wp() в разных модулях, в общем интерфейсе
print_kind() не требуется ссылаться на уникальные имена, благода-
ря чему писать блоки интерфейсов стало намного проще. Более того,
это даёт возможность вынести блоки интерфейса и реализацию в от-
дельный включаемый файл.

5.6. Резюме
Проектирование качественного интерфейса для любого обобщённо-
го программного средства, как, например, библиотека методов чис-
ленного интегрирования, представляет собой достаточно трудную
задачу. Функциональные возможности современных версий языка
Fortran позволяют рассмотреть и оценить множество вариантов ре-
104 Глава 5. Проблема интерфейса

шения этой задачи, даже если приходится работать с реализацией, ис-


ключающей вмешательство в её внутреннее содержимое.
Характеристики каждого решения по организации интерфейса,
рассмотренного в данной главе, приведены в табл. 5.1.
Таблица 5.1. Обзор программных решений, продемонстрированных
в данной главе
Безопас- Безопас- Простота Соот-
Решение ность ность исполь- Гибкость2 ветствие
типов потоков зования2 стандарту
Подставляемые средней
да да нет F77
параметры сложности
Функция с опе- средней
да нет1 нет F77
ратором entry сложности
средней
Блок COMMON нет нет1 умеренная F77
сложности
Переменные средн