Pull to refresh

Хак для ускорения компиляции проекта для Fortran 95

Reading time 7 min
Views 3.1K

Эпиграф


Лет 15 назад, когда про MSBuild еще не знали, жили были люди, которые любили писать на Фортране большие расчетные программы, но не использовали make по религиозным соображениям…

Уровень подготовки


Средний.

Предисловие


Доброго времени суток!

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

Введение


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

call Foo();

?

Правильный ответ — все зависит от того где вы это пишите. Если мелом на заборе — то скоро к Вам подойдет дворник с недобрыми намерениями.

Если же вы напишите это в вашем любимом текстовом редакторе в файле FooProgram.f90, то велика вероятность, что таким образом Вы просите компилятор Фортрана вызвать в этом месте процедуру FOO, не передавая ей никаких параметров.


!FooProgram.f90
Program FooProgram

call Foo();

end program


Мой опыт подсказывает мне, что в этот момент компилятор от Intel вставляет код вызова подпрограммы _FOO.
Т.е. никакой верификации на этом этапе не происходит. Если linker при сборке объектных файлов и статических библиотеке найдет символ _FOO (фортран не различает строчные и прописные буквы, так что символы генерируются всегда в верхнем регистре. Также в Фортране запрещено начинать именование идентификатора с символа подчеркивания), он разрешит ссылку на _FOO, и у Вас все заработает.
Если же не найдет, то он так и напишет:


1>------ Build started: Project: Console1, Configuration: Debug Win32 ------
1>Compiling with Intel(R) Visual Fortran 11.1.051 [IA-32]...
1>FooProgram.f90
1>Linking...
1>FooProgram.obj : error LNK2019: unresolved external symbol _FOO referenced in function _MAIN__
1>Debug\Console1.exe : fatal error LNK1120: 1 unresolved externals
1>
1>Build log written to  "file://F:\2011_03_12\CPP_PROJECTS\Console1\Console1\Debug\BuildLog.htm"
1>Console1 - 2 error(s), 0 warning(s)
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========


Чувствует подвох?

Правильно — никакой статической проверки не произойдет. А если Foo определена в другой единице компиляции Bar.f90 следующим образом:

!Bar.f90
Subroutine Foo(bar)
 Integer, intent(in) :: bar

 Write(*,*) bar;
end subroutine

?

Неопределенность приветствует Вас, зовя свою сводную сестру Уязвимость.

Все дело в том, что FOO внутри Bar.f90 является программной единицей(Program Units) типа «External Procedure».
Но мы помним, что в Fortran95 есть еще три программных единицы:
  • Main program
  • Modules
  • Block data program units


Самое интересное из этого это, конечно же, программная единица Модуль(Module).
Именно она поможет нам обрести статическую проверку на этапе компиляции
Рассмотрим модифицированную программу:

!FooProgram2.f90
Program FooProgram
use BarModule;

call Foo();

end program

!Bar.f90
module BarModule

 contains

 Subroutine Foo(bar)
  Integer, intent(in) :: bar

  Write(*,*) bar;
 end subroutine
end module



Когда вы запустите сборку, то заботливый компилятор скажет Вам, что без параметров вызывать FOO не стоит:


1>------ Build started: Project: Console1, Configuration: Debug Win32 ------
1>Compiling with Intel(R) Visual Fortran 11.1.051 [IA-32]...
1>Bar.f90
1>FooProgram.f90
1>F:\2011_03_12\CPP_PROJECTS\Console1\Console1\FooProgram.f90(4): error #6631: A non-optional actual argument must be present when invoking a procedure with an explicit interface.   [BAR]
1>compilation aborted for F:\2011_03_12\CPP_PROJECTS\Console1\Console1\FooProgram.f90 (code 1)
1>
1>Build log written to  "file://F:\2011_03_12\CPP_PROJECTS\Console1\Console1\Debug\BuildLog.htm"
1>Console1 - 2 error(s), 0 warning(s)
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========


Как же компилятор узнал что вызов FOO в программной единице Главная программа не соответствует реализации? Отчасти он сам подсказал Вам ответ: он стал считать FOO процедурой с явным интерфейсом (a procedure with an explicit interface), который мы вероломно попытались нарушить.

Тут я схитрю и на время сменю императивную точку зрения на процесс компиляции на декларативную.

Рассмотрим два процесса:
Первый: создание в результате компиляции программной единицы модуль спецификации, содержащей в себе такие элементы как:
  • data objects
  • parameters
  • structures
  • procedures
  • operators

Эта информация помещается компилятором Фортрана в файл barmodule.mod (Передавайте привет заголовочным файлам и CLR).

Второй: импортирование спецификации модуля в другую программную единицу.
Именно это происходит во время компиляции строчки

use BarModule;


В предположении, что первый процесс неявно вызывается перед процессом два, нам становиться понятно, как компилятор смог указать нам на нашу ошибку.
Ведь теперь FOO() — это явный, а не неявный как раньше, вызов процедуры FOO. Компилятор в состоянии произвести проверку.

Вроде бы все хорошо. Все довольны.

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

Пусть у нас появляется модуль FOO_BAZmodule и процедура FOO_BAZ в нем.
Т.е. наш «тяжелый проект по расчету динамики теплоносителя в охлаждающем контуре реактора» выглядит теперь так:

!FooProgram.f90
Program FooProgram
use BarModule;

call Foo(1);

end program

!Bar.f90
module BarModule

contains

Subroutine Foo(bar)
    use FOO_BAZModule
    Integer, intent(in) :: bar

    Write(*,*) bar;
    call Foo_BAZ();
end subroutine
end module

!Foo_Baz.f90
module FOO_BAZModule
contains
    subroutine FOO_BAZ()
        write(*,*) "FOO_BAZ"
    endsubroutine
end module


Казалось бы цепочка зависимостей FooProgram < — Foo < — Foo_BAZ может быть разорвана на две:
FooProgram < — Foo и Foo < — Foo_BAZ.
Однако компилятор не всемогущ. И при любом изменении в реализации Foo_Baz вы увидите, что лог сборки выглядит так:


1>------ Build started: Project: Console1, Configuration: Debug Win32 ------
1>Compiling with Intel(R) Visual Fortran 11.1.051 [IA-32]...
1>FOO_BAZ.f90
1>Bar.f90
1>FooProgram.f90
1>Linking...
1>Embedding manifest...
1>
1>Build log written to  "file://F:\2011_03_12\CPP_PROJECTS\Console1\Console1\Debug\BuildLog.htm"
1>Console1 - 0 error(s), 0 warning(s)
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

Bar.f90 и FooProgram.f90 подверглись перекомпиляции для «перестраховки», a priori.

Поверьте — это невыносимо, когда половина приложения пересобирается из-за того, что вы добавили один лишний пробел в строковой константе…

Но что же делать, если система сборки (в моем примере — на основе Intel Visual Fortran) при моих очевидных действиях работает таким образом?

Собственно хак


Как Вы могли заметить, интерфейсы для FOO и для FOO_BAZ был сгенерированы неявно, т.к. мы предоставили компилятору лишь реализацию этих процедур. Что же — опять нам мешает неявность…
Уберем ее при помощи явного описания интерфейса для процедуры FOO_BAZ:

interface
    subroutine FOO_BAZ()
    endsubroutine    
endinterface


Поместим этот интерфейс в модуль FOO_BAZModule

!Foo_baz.f90
module FOO_BAZModule
interface
    subroutine FOO_BAZ()
    endsubroutine    
endinterface

contains
    subroutine FOO_BAZ()
        write(*,*) "FOO_BAZ 2"
    endsubroutine
end module


Но при компиляции нас ждет неприятный сюрприз:


1>------ Build started: Project: Console1, Configuration: Debug Win32 ------
1>Compiling with Intel(R) Visual Fortran 11.1.051 [IA-32]...
1>FOO_BAZ.f90
1>F:\2011_03_12\CPP_PROJECTS\Console1\Console1\FOO_BAZ.f90(8): error #6645: The name of the module procedure conflicts with a name in the encompassing scoping unit.   [FOO_BAZ]
1>compilation aborted for F:\2011_03_12\CPP_PROJECTS\Console1\Console1\FOO_BAZ.f90 (code 1)
1>Bar.f90
1>FooProgram.f90
1>
1>Build log written to  "file://F:\2011_03_12\CPP_PROJECTS\Console1\Console1\Debug\BuildLog.htm"
1>Console1 - 2 error(s), 0 warning(s)
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========


Имя модульной процедуры конфликтует с именем интерфейса.

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

Хак. Шаг второй


Вынесем реализацию интерфейса FOO_BAZ в отдельную единицу компиляции:

!Foo_baz.f90
module FOO_BAZModule
interface
    subroutine FOO_BAZ()
    endsubroutine    
endinterface

end module

!FOO_BAZ_Implementation.f90
    subroutine FOO_BAZ()
        write(*,*) "FOO_BAZ 2"
    endsubroutine


Компилируем…


1>------ Build started: Project: Console1, Configuration: Debug Win32 ------
1>Compiling with Intel(R) Visual Fortran 11.1.051 [IA-32]...
1>FOO_BAZ_Implementation.f90
1>FOO_BAZ.f90
1>Bar.f90
1>FooProgram.f90
1>Linking...
1>Embedding manifest...
1>
1>Build log written to  "file://F:\2011_03_12\CPP_PROJECTS\Console1\Console1\Debug\BuildLog.htm"
1>Console1 - 0 error(s), 0 warning(s)
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========


Что же теперь, вместо трех файлов придется компилировать четыре. Хак не сработал?
Внесем изменения в реализацию FOO_BAZ, которая теперь стала программной единицей типа «external procedure», и запустим компиляцию:


1>------ Build started: Project: Console1, Configuration: Debug Win32 ------
1>Compiling with Intel(R) Visual Fortran 11.1.051 [IA-32]...
1>FOO_BAZ_Implementation.f90
1>Linking...
1>Embedding manifest...
1>
1>Build log written to  "file://F:\2011_03_12\CPP_PROJECTS\Console1\Console1\Debug\BuildLog.htm"
1>Console1 - 0 error(s), 0 warning(s)
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========


Вот и все. Хак сработал.

Чем придется расплачиваться за этот хак, помимо лишней единицы компиляции?
Тем, что теперь мы ответственны за то, что спецификация «external procedure» FOO_BAZ удовлетворяет(совпадает) с интерфейсом FOO_BAZ из модуля FOO_BAZModule.

Выводы


Не получили ли мы тоже самое, что имели на входе?
С практической точки зрения — нет, не получили, т.к. ручная верификация необходима теперь лишь на последнем этапе — реализации интерфейса.

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

Эпилог


Конечно же сейчас, когда семейство компиляторов для платформы .NET имеет доступ ко всему проекту в тот самый момент, когда вы пишете код в Microsoft Visual Studio, эти размышления могут вызвать лишь добродушную улыбку, но не стоит забывать, что в инженерной среде не спешат переходить на управляемый код, а корпорация Intel продолжает разработку компилятора Fortran.
Да и сам Фортран не стоит на месте: уже давно нам доступны стандарты Fortran 2003 и Fortran 2008.
Мой коллега год назад обращал мое внимание на то, что предложенный сегодня на Ваш суд хак, получил свое отражение в новых стандартах, однако, насколько мне известно, это еще не реализовано в большинстве компиляторов Фортрана: как коммерческих, так и свободно распространяемых.

Ссылки


В статье использовались логи компиляции проекта Intel® Visual Fortran Console Application при помощи аддона Intel® Visual Fortran Compiler Integration for Microsoft Visual Studio* 2008, 11.1.3468.2008, Copyright © 2002-2009 Intel Corporation
* Other names and brands may be claimed as the property of others.
к Microsoft Visual Studio 2008
Version 9.0.30729.1 SP © 2007 Microsoft Corporation
И материалы из Intel® Fortran Compiler 11.1 User and Reference Guides
P.s. Автор может искренне заблуждаться относительно любого высказанного утверждения.
Tags:
Hubs:
+7
Comments 6
Comments Comments 6

Articles