作者:王咏刚
1. 不断变革的Fortran
2004年5月,Fortran2003,这个新世纪里诞生的Fortran语言新标准,终于在ISO、IEC的联合工作组JTC1/SC22/WG5以及美国Fortran委员会NCITS/J3的共同努力下,走完了编撰、修订的最后一步。不出意外的话,再过一两年,支持Fortran2003的开发工具就会充斥Fortran世界;那些仍在坚守Fortran77阵地的老用户们也会自然而然地把质疑的目光从Fortran90/95投向更富有革命精神的Fortran2003。
要了解Fortran2003,我们有必要先简单回顾一下Fortran语言的历史。
1954到1957年间,世界上第一种高级程序设计语言Fortran诞生于IBM公司。Fortran这个名字的原意是IBM Mathematical FORmula TRANslation,其设计目的在于为科研人员提供一种符合数学思维习惯的高级语言,以满足科学计算的需要。1960年代,在美苏太空竞赛的大背景下,在国防、教育和科研领域对高性能计算工具的迫切需求下,Fortran语言蓬勃发展,成为了当时统治计算机世界的高级语言之王。
1962年,为了统一不同公司、不同硬件平台上的Fortran语言,人们开始了Fortran语言标准化的尝试,这也是程序设计语言发展史上的第一次标准化历程。1972年,Fortran 66标准(标准编号来自标准草案的制定时间)正式发布。但因为标准文档过于简单,约束力不强,Fortran 66标准发布后,Fortran语言的统一问题并没有得到彻底解决。
1978年,Fortran语言标准的第一个修订版本正式发布,这就是我们熟悉的Fortran77了。Fortran77细致描述了Fortran语言的各种特征,让Fortran成了一种真正规范、高效和强大的结构化程序设计语言。此后,无数性能优异的Fortran77编译器和开发工具的问世更是让Fortran77成为了几乎所有理工科学生的必修课。
尽管Fortran77的影响力一直延续到了今天,但Fortran语言不断变革的历程却从未停止过。为了改变Fortran77那种老旧的、从打孔卡片遗传来的语言风格,为了给Fortran注入更多的现代特征,人们于1991年发布了崭新的Fortran90标准。除了自由的代码风格外,Fortran90还为Fortran语言引入了模块、接口、自定义(派生)数据类型和运算符、可动态分配和参与复杂运算的数组、泛型过程、指针、递归等重要的语法特征。这不但使结构化的Fortran语言更趋完善,也使其具备了少量的面向对象特性。
1997年发布的Fortran95标准在Fortran90的基础上,吸收了HPF语言(High Performance Fortran,Fortran语言在并行环境下的一个变种)的优点,提高了Fortran语言在并行任务中的表达和计算能力,并进一步完善了派生类型、指针、数组等要素的相关语法。
以Fortran 66为基准,我们可以把后续的Fortran77/90/95以及本文介绍的Fortran2003均视为对Fortran语言标准的修订。在历次修订中,Fortran77和Fortran95是修订幅度相对较小的版本,而Fortran90和Fortran2003则是锐意变革的“大修”版本。如果说Fortran90是在“形式”上让Fortran脱胎换骨的话,那么,Fortran2003就近乎彻底地解决了Fortran语言现代化的问题:完整的面向对象机制、灵活的语法特征、统一的接口标准……这些难道还不足以让Fortran迷们跃跃欲试吗?
2. 面向对象的Fortran
从结构化程序设计到面向对象,这是大多数高级语言的发展、演变规律,Fortran也未能免俗。在Fortran77中,由于缺乏对数据抽象和数据封装的基本支持,人们很难引入面向对象的理念和方法。在Fortran90/95中,尽管我们可以用派生类型(Derived Type)和模块(Module)模拟一部分面向对象的特性,但却无法实现真正意义上的封装和继承。
对数据和相关操作的封装是面向对象的基础。传统的结构化程序设计语言大多拥有简单的数据封装能力(如Pascal语言中的record),但缺乏对操作封装的支持。当我们为结构化程序设计语言引入面向对象机制时,在语法层面一般有两种思路:一种是引入一个全新的语法容器,允许用户借助该容器定义出封装了数据和操作的统一数据类型,例如,C++在C语言的基础上扩展出了class的概念,并以class为中心支持面向对象编程;另一种是通过某种机制,将原有的数据封装容器和表述操作的过程或函数捏合在一起,例如,Ada 95利用Ada 83中已有的概念,将private type和相关操作封装在package中,创建了完整的面向对象机制。Fortran2003对Fortran90/95的改造正是遵循着后一种思路进行的。
首先,Fortran2003第一次引入了过程指针(Procedure Pointer)的概念。Fortran90/95只支持普通指针。为了实现回调机制,Fortran90/95特意引入了用过程名充当形式参数(Dummy Procedure)的语法,但这种语法还无法解决在数据类型中内嵌相关操作的问题。在Fortran2003中,我们可以使用过程指针将数据和操作绑定在一起:
TYPE point REAL :: x, y PROCEDURE (testif), POINTER :: test END TYPE这种绑定是一种运行时的动态绑定,我们可以根据需要为过程指针赋值,这类似于我们在C语言的struct中嵌入函数指针。
其次,如果与类型相关的操作在编译时已经预知,我们就可以直接使用Fortran2003提供的类型绑定过程(Type-bound Procedure),将数据和操作关联在一起:
TYPE point REAL :: x, y CONTAINS PROCEDURE :: test => my_test END TYPE这样,在同一个模块中,类型和相关操作相互关联,用户可以直接使用CALL p1%test或类似的代码来访问特定操作,这已经非常近似于Ada 95的封装机制了。
和C++或Java语言相比,Fortran2003的这种过程绑定方式较为靠近语言设计的底层。例如,对于绑定在point类型中的my_test过程,我们必须显示指明,该过程的第一个参数为point类型:
SUBROUTINE my_test(a) TYPE(point), INTENT(IN) :: a END SUBROUTINE这里的参数a相当于C++或Java方法中隐含的this参数,其目的是将调用该操作的对象实例传入过程。我把这种显示实现this参数的做法称为“赤裸裸的面向对象”。尽管写起来稍嫌繁琐,但这种“赤裸裸”的做法也有不少好处。例如,我们可以在Fortran2003中使用NOPASS属性禁止传递对象实例,或通过PASS属性改变对象实例参数在过程参数表中的位置。
Fortran90/95已经允许用户自定义运算符过程。现在,Fortran2003则进一步允许用户将运算符过程绑定到特定的类型,其方法和绑定普通过程类似:
TYPE point REAL :: x, y CONTAINS GENERIC :: OPERATOR(+) => plus GENERIC :: ASSIGNMENT(=) => assign END TYPE有了完善的数据和操作封装机制,Fortran90/95中简单的访问权限控制属性PRIVATE和PUBLIC就不能满足要求了。Fortran2003扩展了PRIVATE和PUBLIC的使用范围,允许PRIVATE类型中出现PUBLIC成员,同时又增加了PROTECTED属性,以限定某个成员的取值只能由自己所属的模块修改。
Fortran2003增强了派生类型的初始化和构造语法。尽管我们在Fortran2003中还不能像在C++里那样随心所欲地定义各种构造函数,但仍可以较精确地控制对象的初始化过程。此外,Fortran2003允许我们为类型定义终结子例程(Final Subroutine),以完成对象销毁时的清理工作。终结子例程可以定义多个,像这样:
TYPE point REAL :: x, y CONTAINS FINAL :: finish1, finish2 END TYPE在Fortran2003中,继承机制是通过类型扩展(Type Extension)实现的。相关的语法和大多数面向对象语言的继承机制没有什么不同:
TYPE, EXTENDS(point) :: funny_point LOGICAL :: funny = .TRUE. END TYPE在扩展类型中,可以直接通过基类型的名称来引用基类型的成员,这类似于C++中引用基类成员的语法。
单从封装和继承这两个层面看来,Fortran2003对Fortran90/95的扩展就是革命性的。当年,Fortran90刚问世的时候,许多人迫不及待地宣称Fortran已经是面向对象的语言了。殊不知,Fortran90/95中的面向对象机制还不如Visual Basic 6.0中的类模块(Class Module)来得成熟,更不要说与真正面向对象的Fortran2003相提并论了。
3. 自由的Fortran
灵活性一直是Fortran语言的一个弱项。早期的Fortran甚至不支持内存的动态分配。Fortran90引入了指针的概念,全面支持数据的动态存取,同时也引入了泛型过程(Generic Procedure)的机制,以支持与C++的函数重载类似的功能。在此基础上,Fortran2003通过参数化派生类型、覆盖、多态型等机制,进一步增强了Fortran语言的灵活性。例如:
TYPE point(k, n) INTEGER, KIND :: k = KIND(0.0D0) INTEGER, LEN :: n REAL(KIND=k) :: x, y CHARACTER(LEN=n) :: name END TYPE基于上面这样的参数化派生类型(Parameterized Derived Type),我们可以在定义对象时,根据需要指定对象的具体类型参数,也可以在程序运行期间,动态指定可分配内存对象的类型参数(Deferred Type Parameter)。
除了Fortran90已经实现的重载(Overloading)机制以外,Fortran2003还允许扩展类型覆盖(Overriding)基类型中的类型绑定过程。更重要的是,Fortran2003通过抽象类型(Abstract Type)、延迟绑定(Deferred Binding)、多态实体(Polymorphic Entity)、类型选择结构(SELECT TYPE)等要素完整地实现了面向对象语言必备的多态机制。举例来说,我们可以声明下面这样的抽象类型:
TYPE, ABSTRACT :: point REAL :: x, y CONTAINS PROCEDURE(testif), DEFERRED :: test END TYPE ABSTRACT INTERFACE SUBROUTINE testif(p) CLASS(point), INTENT(in) :: p END SUBROUTINE END INTERFACE上述代码相当于在C++或Java语言中声明了一个抽象类,其中的test过程相当于C++语言中的一个纯虚函数,只有接口声明,而没有实现代码。在此基础上,我们可以从抽象类型point扩展出具体的子类型。
当某个对象的具体类型在运行时才能确定时,我们可以用CLASS关键字(这里的CLASS指的是一组有继承关系的类型,与C++中的class并不等同)定义一个具有多态功能的指针或可分配对象:
CLASS(point), POINTER :: p
现在,指针p可以指向任何由point扩展的具体类型的对象,这种使用方式已经和我们在C++中用基类指针引用派生类对象的做法一模一样了。使用这种具有多态性特征的实体时,我们还可以用Fortran2003提供的内部过程SAME_TYPE_AS和EXTENDS_TYPE_OF来判定对象的类型,这是一种简单的运行时类型识别(RTTI)机制,其功能和C++语言中的typeid关键字相仿。
更有趣的是,Fortran2003允许代码根据多态实体的实际类型,在运行时执行特定的程序流程:
CLASS(point), POINTER :: p p => a SELECT TYPE ( pp => p ) TYPE IS (point_3d) PRINT *, pp%z TYPE IS (point_2d) PRINT *, pp%x END SELECT这种语法在程序设计语言中比较少见。在Java或C#等具备较强的RTTI和Reflection功能的语言中,我们可以用switch语句结合类型标识实现类似的功能,如下面的C#代码:
void foo(Point p) {
switch(p.GetType().FullName) {
case "Science.Point2D":
MessageBox.Show("2D point");
break;
case "Science.Point3D":
MessageBox.Show("3D point");
break;
}
}
但在Fortran这样不依赖于虚拟机和中间代码的纯编译型语言中,能够拥有如此方便的SELECT TYPE语句,实在是Fortran用户的一大幸事。
其他一些较小的语法功能扩充也能反映出Fortran2003制定者对语言灵活性的追求。比方说,ASSOCIATE语句可以让Fortran用户体验到比C语言的宏定义更为便捷的复杂表达式替换功能:
ASSOCIATE ( Z => EXP(-(X**2+Y**2)) * COS(THETA) ) PRINT *, A+Z, A-Z END ASSOCIATE再比方说,在Fortran2003中,可以拥有ALLOCATABLE属性的变量已不再限于数组对象,参数化派生类型的对象也可以在定义时省略参数,而在动态分配时指明:
TYPE(point(KIND(0.0D0), n=20)) :: a TYPE(point(KIND(0.0D0), n=:)),ALLOCATABLE :: b ALLOCATE(b,SOURCE=a)对于科学计算语言中最重要的数组(矩阵)操作,Fortran2003也提供了更多的新功能。例如,为可动态分配的数组变量赋值时,目标数组的形状可以根据源数组的形状自动调整,并自动完成内存空间的重新分配,用户也可以使用新增的MOVE_ALLOC内部过程强制改变某个数组的形状:
REAL, ALLOCATABLE :: a(:), temp(:) ALLOCATE(a(-n:n) ALLOCATE(temp(-2*n:2*n)) temp(::2) = a CALL MOVE_ALLOC(TO=a, FROM=temp)Fortran2003允许我们为指针类型的参数增加INTENT属性,这为那些与动态数据结构(如链表)相关的算法程序提供了更多的选择空间。Fortran2003引入了IMPORT语句,允许接口声明中引用接口所在模块内的相关定义。Fortran2003还允许我们在USE语句中,改变被引用模块内自定义运算符的名称。
总之,借助Fortran2003提供的更加灵活的语法特性,我们能更容易地编写出可以适应不同数据类型、不同应用环境的可复用代码。对于Fortran语言拓展应用领域、发掘潜在用户的目标而言,这些灵活性都是至关重要和不可或缺的。
4. 高性能的Fortran
与其他科学计算语言或工具相比,Fortran语言在性能上历来是出类拔萃的。因为语言本身专门针对数值计算、矩阵处理等功能进行了优化,大多数Fortran编译器产生的可执行代码在效率上甚至会超过以高效著称的C或C++语言。为了在高性能的并行处理系统(如IBM的“深蓝”和我国的“曙光”)上获得更出色的执行效率,Fortran95还特意吸收了HPF语言的优点,为Fortran语言增添了若干支持并行计算的语法特征(比如著名的FORALL语句和PURE过程)。
继续改进Fortran语言的计算性能当然也是Fortran2003的任务之一。一个最明显的例子是,Fortran2003引入了VOLATILE属性。这个属性类似于C语言或Java语言中的volatile关键字。编写过并发或实时应用的程序员都知道这个含义为“易变”的关键字的价值:在并发系统中,如果没有这个关键字的帮助,我们就必须时刻警惕共享数据的取值是否已被正确刷新。
不过,相对而言,Fortran语言本身的并行计算机制仍不算十分健全。编写并发或实时程序时,Ada语言中的任务(Task)管理和同步(Synchronization)机制,或是Java语言中的多线程同步特性都可以为程序员提供更有力的支持。
实际上,Fortran95和Fortran2003陆续引入并行语法的目的之一是消除标准Fortran语言与已经存在并得到广泛应用的各种Fortran语言变种之间的隔阂,允许同一份Fortran代码在不同的语言环境间移植。至少到Fortran2003为止,标准Fortran语言还没有能力完全替代以HPF为代表的“高性能”Fortran语言变种。仅就适应高性能并行计算环境的能力而言, HPF等Fortran变种也仍有足够的理由继续存在和发展,直到未来某一个大而全的Fortran标准把它们真正统一起来为止。
为了适应现代CPU的时钟精度,Fortran2003扩展了内部过程SYSTEM_CLOCK的功能,允许其COUNT_RATE参数为INTEGER或REAL类型。
对IEEE浮点数标准(IEEE 754)的完全支持是Fortran2003在提高计算精确度和规范性方面的又一个努力。在Fortran2003中,IEEE标准中描述的下溢(Underflow)可以被正确识别和处理,内部模块IEEE_ARITHMETIC中也为此增加了IEEE_SUPPORT_UNDERFLOW_CONTROL、IEEE_GET_UNDERFLOW_MODE、IEEE_SET_UNDERFLOW_MODE等几个相关的内部过程。
另一个可以大幅提升程序执行效率的改进是Fortran2003新增加的异步I/O机制。也就是说,执行比较耗时的I/O操作时,程序不必阻塞在I/O语句上,可以继续执行后续的指令。这种机制类似于我们用WIN32函数操作磁盘文件时,可以借助FILE_FLAG_OVERLAPPED标记指明I/O操作应异步进行。
基于Fortran2003,当我们在外部文件的OPEN语句中指明ASYNCHRONOUS=’YES’时,后续拥有ASYNCHRONOUS=’YES’属性的READ或WRITE语句即以异步方式执行。此后,我们可以继续执行其他语句,或使用WAIT语句等待异步I/O执行完毕。
异步I/O是高性能应用软件必备的特性之一。在Fortran90/95的时代里,为了实现异步I/O功能,不同的开发商总会在自己的Fortran编译器或程序库中以各自不同的方式,增加对异步I/O的支持。Fortran2003统一异步I/O语法的做法显然有助于Fortran应用的移植和推广。