Modern Fortran – Part 2
Fortran 90 catapulted Fortran from a perceived “old” language to a modern language on equal footing with any other. It retained Fortran’s history of simplicity and performance, but it added features that developers had been wanting, such as free-form input, dynamic arrays, pointers, derived types, modular programming (modules), and portable precision definitions (kind).
Furthermore, a number of older features were deprecated from the Fortran standard. This deprecation process is very refreshing, and I believe it allows the language to stay simple, with a focus on numerical performance. Although taking an older Fortran 77 program and updating it to Fortran 90 involved a bit of work, the result was definitely worth the effort, and you could even find tools, including some online tools, for autoconverting code from Fortran 77 to Fortran 90.
The wheels of Fortran progress did not stop with Fortran 90. Very quickly after Fortran 90 was released, the Fortran standards group started working on the next evolution – Fortran 95. The standard was officially published in 1997 and was oriented toward resolving some outstanding issues in Fortran 90. However, it did add a new extension for High Performance Fortran (HPF) to the standard. Also, features that were noted as “obsolete” in Fortran 90 were removed in Fortran 95.
The HPF extension of Fortran 90 focuses on parallel computing. The specifications are published by the High Performance Fortran Forum (HPFF). HPF uses a data parallel model to spread computations across multiple processors, allowing multicore processors in a node that would otherwise be idle to be used. This works well for both single instruction, multiple data (SIMD) and multiple instruction, multiple data (MIMD).
HPF added a few statements and capabilities to Fortran:
- FORALL statement
- Compiler directives for array data distribution
- Procedure interface for non-HPC parallel procedures
- Additional library routines, including data scattering and gathering
The FORALL construct allows you to write array functions that look more like the original mathematical formula and makes it a bit easier to change the code for different ranges without worrying that the arrays are conformable. A quick example is the basic portion of the four-point stencil for the 2D Poisson equation problem. The formula is shown in Figure 1.
Assume the domain ranges from 1 to n for both i and j. The boundaries are i= 1, i = n, j= 1, j= n, so they don’t change. Therefore the iterations are over i= 2,n − 1 and j= 2,n−1. Here is how you can write the iteration over the domain using array notation:
a(2:n-1,2:n-1) = 0.25 * & (a(1:n-2,2:n) + a(3:n,2:n) + a(2:n,1:n-2) + a(2:n,3:n))
Using FORALL, the same can be written as:
forall (i=2:n-1, j=2:n-1) & a(i,j) = 0.25*(a(i-1,j) + a(i+1,j) + a(i,j-1) + a(i,j+1))
Using FORALL makes the code a little easier to read than the array notation. Moreover, it’s very easy to change the domain without having to modify the formula itself.
Also introduced as part of HPF were compiler directives. These directives deal with how data is distributed for processing. They are entered into the code as comments something like:
The compiler interprets any directive after !HPF$ as having to do with HPF. The range of directives is beyond the scope of this article, although you can find information about them online, along with HPF tutorials. The nice thing about HPF directives is that if the compiler does not know anything about HPF, it views the directives as comments and just moves on. This is the same approach taken by OpenACC and OpenMP (open multiprocessing).
The HPF extensions never seemed to be popular with users. A few Fortran 95 compilers implemented the HPF extensions, but others did not. While the compilers were being written, a Message Passing Interface (MPI) standard for passing data between processors, even if they weren’t on the same node, was becoming popular. It provided a way to utilize other processors in the same system or processors on other systems.
Additionally, OpenMP was released, providing a threading model for applications. As with HPF, OpenMP allowed coders to insert directives that told the compiler how to break up the computations into threads and exchange data between threads. These directives were code comments, so compilers that were not OpenMP-capable just ignored them.
Some observed that these two developments led to the decline of HPF. In fact, not many compilers offer HPF. GFortran (GNU Fortran) is not a true Fortran 95-compliant compiler because it does not implement HPF. However, it implements almost all of the other Fortran 95 features.
Fortran 95 added some additional features outside of HPF. Wikipedia reports that the following were added to the Fortran 95 standard:
- FORALL and nested WHERE constructs to aid vectorization
- User-defined PURE and ELEMENTAL procedures
- Default initialization of derived type components, including pointer initialization
- Expanded ability to use initialization expressions for data objects
- Initialization of pointers to NULL()
- ALLOCATABLE arrays clearly defined to be automatically deallocated when they go out of scope.
In Fortran 90, some features or statements were deprecated (i.e., highly recommended not to be used before the next release of the standard). From the same Wikipedia article, the deprecated Fortran 95 features included:
- Banning of DO statements using REAL and DOUBLE PRECISION index variables
- Removal of PAUSE
- Removal of ASSIGN
- Removal of the controversial assigned GOTO statement
- Removal of the H descriptor
Although I’m not a programming language aficionado, I think it’s wonderful that a language actually removes obsolete features while adding new features. All too often, new features are just added to the language, creating bloat.
Both Fortran 90 and 95 introduced features that allowed object-oriented programming (OOP). The world had come a long way now that Fortran accommodated object-oriented code! Fortran 2003, the next standard, added even more features to enhance the object-oriented capability.
You could implement quite a bit of OOP before Fortran 2003. For example, you could define the basics of classes in Fortran 90. Private and public functions could be used as needed, including constructors and destructors. You can find several articles around the web on OOP in Fortran 90 and Fortran 95. One article even compares Fortran 90 and C++, so you can understand the similarities between the two.
In Fortran 2003, the ability to create what are called type-bound procedures was introduced, which makes calling functions within a class much easier. Fortran 2003 also introduced type extensions and inheritance (key aspects of OOP), as well as polymorphism and dynamic type allocation. This collection of additions really completes support for abstract data types making OOP much easier.
Explaining or even introducing OOP in Fortran is beyond the scope of this article. If you are interested, the following features were added to round out Fortran. The Wikipedia article on Fortran 2003 mentions:
- Derived type enhancements: parameterized derived types, improved control of accessibility, improved structure constructors, and finalizers
- Procedure pointers
Classic object-oriented languages such as C++ or Python allow you to define classes that include data and methods (functions) that operate on that data. You can instantiate (create an instance) of the class that has its own data and methods or several instances of the class that each have their own unique data and methods.
In Fortran, modules are roughly equivalent to classes. Modules can contain data and methods (functions and subroutines), but there really isn’t a concept for separate instances of a module. To get around this, you create a module to contain the methods for the specific class with a derived type that contains the data. From this combination, you can create separate “instances” of the type that pass data to the methods in the modules.
I’ll look at a quick Fortran 90 example for creating and using classes by creating a module that has two public functions: One computes the area of a circle given the radius, and the other prints out the area of the circle. In the main program, I can create a derived type using the module and then use methods in the module using the derived type (Listing 1).
Listing 1: Creating and Using Classes in Fortran 90
MODULE CLASS_CIRCLE IMPLICIT NONE PRIVATE PUBLIC :: CIRCLE, CIRCLE_AREA, CIRCLE_PRINT REAL :: PI = 3.1415926535897931d0 ! Class-wide private constant TYPE CIRCLE REAL :: RADIUS END TYPE CIRCLE CONTAINS FUNCTION CIRCLE_AREA(THIS) RESULT(AREA) TYPE(CIRCLE), INTENT(IN) :: THIS REAL :: AREA AREA = PI * THIS%RADIUS**2.0 END FUNCTION CIRCLE_AREA SUBROUTINE CIRCLE_PRINT(THIS) TYPE(CIRCLE), INTENT(IN) :: THIS REAL :: AREA AREA = CIRCLE_AREA(THIS) ! Call the circle_area function PRINT *, 'Circle: r = ', this%radius, ' area = ', AREA END SUBROUTINE CIRCLE_PRINT END MODULE CLASS_CIRCLE PROGRAM CIRCLE_TEST USE CLASS_CIRCLE IMPLICIT NONE TYPE(CIRCLE) :: C ! Declare a variable of type Circle. C = CIRCLE(1.5) ! Use the implicit constructor, radius = 1.5 CALL CIRCLE_PRINT(C) ! Call a class subroutine END PROGRAM CIRCLE_TEST
Notice that the derived type is used to create the variable C. Then I can “store” the radius in C by calling the “constructor” of the class (all it does in this case is store the value). Then the program calls the “method” to print out the value by passing the derived type instance C.
In Fortran 2003, you can take advantage of some new features to make the same code look more object oriented. In the example in Listing 2, type-bound procedures can be used within the module.
Listing 2: Type-Bound Procedures
MODULE CLASS_CIRCLE IMPLICIT NONE PRIVATE PUBLIC :: CIRCLE, CIRCLE_AREA, CIRCLE_PRINT REAL :: PI = 3.1415926535897931d0 ! Class-wide private constant TYPE, PUBLIC :: CIRCLE REAL :: RADIUS CONTAINS PROCEDURE :: AREA => CIRCLE_AREA PROCEDURE :: PRINT => CIRCLE_PRINT END TYPE CIRCLE CONTAINS FUNCTION CIRCLE_AREA(THIS) RESULT(AREA) CLASS(CIRCLE), INTENT(IN) :: THIS REAL :: AREA AREA = PI * THIS%RADIUS**2.0 END FUNCTION CIRCLE_AREA SUBROUTINE CIRCLE_PRINT(THIS) CLASS(CIRCLE), INTENT(IN) :: THIS REAL :: AREA AREA = THIS%AREA() ! Call the type_bound function PRINT *, 'Circle: r = ', this%radius, ' area = ', AREA END SUBROUTINE CIRCLE_PRINT END MODULE CLASS_CIRCLE PROGRAM CIRCLE_TEST USE CLASS_CIRCLE IMPLICIT NONE TYPE(CIRCLE) :: C ! Declare a variable of type Circle. C = CIRCLE(1.5) ! Use the implicit constructor, radius = 1.5 CALL C%PRINT ! Call a type-bound subroutine END PROGRAM CIRCLE_TEST
Notice that in the module definition, I type bound the procedures, CIRCLE_AREA and CIRCLE_PRINT to the module. Then in the main program, once I instantiate the derived type C = CIRCLE(1.5), I can call methods (functions) as I would a derived type (CALL C%PRINT). This looks a bit more like OOP.
In addition to object-oriented features, Fortran 2003 also introduced new features to greatly enhance its ability to interact with C and C++ libraries or code. If you have tried to integrate C and Fortran in the past, you will realize there are some fundamental differences between the two. A big one is that multidimensional arrays in C/C++ are in the opposite order in Fortran. Additionally, all arguments in Fortran are passed by reference and not by value. This means C must pass Fortran arguments as pointers.
Finally, one difference between C and Fortran has been a source of argument for many years. By default, C arrays start with 0 (zero) and Fortran arrays start with a 1. I have heard many arguments about which is better or that there is a difference. Modern Fortran allows you to define arrays starting with 0 (zero). Actually, you can define them to start with any integer, including negative numbers.
It has been notoriously difficult to integrate Fortran and C. Although it has been done, and linkers will blissfully create a binary for you, ultimately you have to pay attention to what you are doing. More over, it has never been easy to create portable C/Fortran integration. Sometimes, you have to test the compiler(s) to determine exactly how they integrate.
Fortran 2003 added a standardized way to generate procedures, derived type declaration, and global variables that are interoperable with C. Because of standardization, Fortran 2003-compliant compilers make portability of C and Fortran integration much, much better.
The basic model for Fortran/C integration comprises three basic parts:
- Define Fortran “kinds” that map to C types. Remember that starting in Fortran 90, data types started defining the precision of variables via the kind definition.
- Use an “intrinsic module” to define the names:USE,INTRINSIC :: ISO_C_BINDING
- Use aBIND(C) attribute to specify C linkage
A few tutorials discuss how to use these three parts to integrate Fortran and C together. Discussing the details is beyond the scope of this article, but look around the web if C and Fortran integration is important.
Another set of big changes in Fortran 2003 has to do with I/O. Before Fortran 2003, all I/O was record based, which worked fine for performing I/O that had been produced by other Fortran code or for reading ASCII files. However, it doesn’t work well when reading data from a file that was created by an instrument or a spreadsheet or a database. Fortran I/O had to become much more general, including the ability to do I/O to “streams.” With Fortran 2003, you could now do both formatted and unformatted stream I/O.
Using stream file access has several benefits. The first is that the file will likely be smaller because there are no record markers. However, this depends on how you were performing I/O before you started using streams. Second, streams allow you to read data from files to which you normally would not be able, such as databases. Third, new I/O functions allow you to move to different “positions” within the file to perform I/O.
Unformatted stream writes have no record markers. Subsequent writes just append data to the file. Listing 3 shows a quick example.
Listing 3: Stream Writes
PROGRAM writeUstream IMPLICIT NONE INTEGER :: myvalue = 12345, mypos OPEN(UNIT=11, FILE="ustream.demo", STATUS="NEW", ACCESS="STREAM") WRITE(11) "first" WRITE(11) "second" INQUIRE(UNIT=11, POS=mypos) PRINT *, "Myvalue will be written at position ", mypos WRITE(11) myvalue CLOSE(UNIT=11) END PROGRAM writeUstream
The first write adds 5 bytes to the file, and the second write adds 6 bytes to the file. Therefore, the next write should start at byte 12. The INQUIRE function returns information about the file. In this case, it asks for the current position.
If the code did not use streams, then the writes would add a record marker after each write. Therefore, the position would be something greater than 12 bytes. If you compile the code with a compiler that supports streams, the result will be 12.
Using streams, you can specify a position within a file for a READ() or WRITE() function. For example, the following line writes a string at position 12 (i.e., 12 bytes into a file):
READ(11, POS=12) string
The start of a file is position 0.
A couple of quick pieces of advice when using stream output: If you use it to create an output file, older Fortran code will not be able to read it (no record markers). Only code built with compilers that implement streams will be able to read the file. Conversely, if you are reading a file written by an older version of Fortran, you can’t read it using streams – you have to read it as classic Fortran with record markers.
Other features added in Fortran 2003 include (from Wikipedia):
- Data manipulation enhancements: allocatable components, deferred type parameters, VOLATILE attribute, explicit type specification in array constructors and allocate statements, pointer enhancements, extended initialization expressions, and enhanced intrinsic procedures.
- Input/output enhancements: asynchronous transfer, stream access (previously discussed), user-specified transfer operations for derived types, user-specified control of rounding during format conversions, named constants for preconnected units, the FLUSH statement, regularization of keywords, and access to error messages.
- Procedure pointers.
- Support for IEEE floating-point arithmetic and floating-point exception handling.
- Support for international usage: access to ISO 10646 4-byte characters and choice of decimal or comma in numeric-formatted I/O.
- Enhanced integration with the host operating system: access to command-line arguments, environment variables, and processor error messages.
Fortran 95 and 2003 really pushed Fortran into the modern era by dropping old and unused features and adding newer, modern and needed features. Fortran could now handle object-oriented programming and had much better and standardized integration with C. It also incorporated some features from HPF (High Performance Fortran) that improved code readability. Fortran was still oriented toward numerical computations, but now it was much easier to express programming concepts and write portable code.
The Fortran developers were not done, however. In the next article, I cover Fortran 2008, which incorporates some new features to improve performance, and Fortran 2015, which, although still under development, shows some serious promise.
This blog represents my own viewpoints and not those of my employer, Amazon Web Services.