作为一只CFD小白,Fortran是无论如何都绕不开的语言,无数已有的程序甚至小规模的商用软件都是Fortran写的。但是这些古老的代码存在goto过多,format使用过多,大量的矩阵操作都是用do循环堆砌,以及变量命名过度简化,fixed格式导致排版乱七八糟等等问题。总之就是可读性差,复用困难。正好疫情期间躲在家,在改写已有的组内代码的同时,也将原来的.f程序升级成为.f90程序。为此学习fortran语言的书《Fortran 95 程序设计》 彭国伦著。
因为已经使用过一年左右的Fortran语言,主要关注点是之前不怎么使用的语法和功能,尤其是从F77升级到F90和F95之后的新增功能。对于已经比较熟悉的部分就会略过,只挑觉得重要,或写高性能计算程序时有用的部分说。作为这段时间改写程序的一个记录。
OK!那我们开始吧!
第一章 计算机概论
略,高性能计算和CPU运算的一些细节确实相关,不过最好参考更加专门的书籍,比如说《计算机体系结构》胡伟武
第二章 编译器的使用
win下的编译器比较常用的是Visual Fortran和VS之下的Fortran组件。笔者主要使用的是linux的编译器。Linux下的GNU会包含F77的编译器G77,编译方式如下
g77 hello.f
./a.out
F90则使用另外一个编译命令:
f90 hello.f90
而F95则使用:
F hello.f95
呃,不过现在我在用的是gfortran
,书中并未介绍。正常编译.f90是可以通过的,等到遇到问题再回到这里补充吧。
第三章 Fortran程序设计基础
Fixed 和 Free 格式
在Fortran77的时候,使用的是Fixed Format固定格式。而在Fortran90以及95,则使用Free Format自由格式。两者的主要规则罗列如下:
Fixed Format:
- 每行第7到72个字符有效,会被编译。前六个字符一般为空格
- 每行第5个字符如果是空格或者0之外的字符,就是续航符
- 每行前几个字符可以用来给定行标,方便format,goto等等命令使用
- 使用小写字母c注释
Free Format: - 行首的六个空格被取消,可编写字符扩展到132个
- 续航符为每行尾或者行首的&符号
- 行标写在每行最前面即可
- 注释用!而不是c
Fortran的数据类型和数学表达式
相对F77没有新知识
第四章 输入输出声明
输出
输出使用如下两种:
write(*,*) "String"
print *,"Hello"
其中F90使用双引号,F77使用单引号。输出特殊字符的细节此处不说明,write
的左右*星号分别代表输出到那个端口,以及输出格式。而print
只能控制输出格式,输出一定是输出到终端的。
变量的声明与kind
基础的变量声明没什么好说的,有一个新知识是声明使用的变量的bytes数
integer*4 a !F77
integer(kind=4) a !F90
real*8 b !F77
real(kind=8) b !F90
其中kind
的值代表存储变量使用的bytes
数量,上述分别代表长整型和双精度。而F90的声明可以向下兼容。其中变量某个值所需的kind值,可以通过fortran命令给出,具体需要查阅书中Page67
变量的赋值精度
为什么专门强调这个问题,在科学计算里面,有一些基础参数的设定一定要尽可能的精确。比如说测试数值格式精度,误差会下降到1e-10的情况下,如果计算区域长度的设置本来是real*8 :: a=1.0
实际赋值之后的变量为a=1.000008
,即单浮点数的精度之后,出现了误差,那么整体精度就不可能达到1e-10。
书中给出,给单精度浮点数赋值时,使用的是a=1.23E3
。而给双精度浮点数赋值时,使用的是a=1.23D3
。部分编译器即使是给双精度的变量赋值,也一定要带D
才能有足够精度的赋值。
输入
就是read
命令,这里介绍的都很基础,略。
format格式
基础用法如下:
write(*,100) a
100 format(I4)
要用到行号,书中给出了格式详细的参数说明,不过经常使用的是I,F,E,A,X这几个。详细的说明可以直接翻书或者百度,这里只简单说明罗列:
!整数I
write(*,"(I5)") 100 !5个字符宽的输出
write(*,"(I5.3)") 10 !5个字符宽的输出,最少输出3个数字
!浮点数E和D分别为单双精度
write(*,"(E15.7)") 123.45 !占15个字符,小数占7位
write(*,"(E9.2E3)") 12.34 !占9个字符,小数占2位,指数部分输出三个数字
!字符串A
write(*,"(A10)") "Hello" !10个字符宽
!移位X
write(*,"(5X,I3)") 100 !输出3个字符宽,同时先填5个空格
implicit命令
常用的就两个
implicit real*8 (a-h,o-z)
implicit none
不写的话,fortran也会默认第一行的情形,不过是单精度。不过比较推荐第二种,不然师兄的程序传给师弟的时候,师弟看得头发都快掉没了。
parameter参数
使用方法如下:
real,parameter :: a=1.0d0
real a
parameter (a=1.0d0)
赋初值与双冒号
声明的同时赋初值的话,需要使用双冒号::
,即
real :: a=1.0d0
也可以用Data进行赋初值,不过这里并不方便,不说明
等价声明
新知识,类似C语言里面的&a=b
,这里对应
integer a,b
equivalence(a,b)
声明后,a和b使用同一个内存空间。主要作用是调用一些高维数组中的单个元素,因为数组下标索引时会带来额外的运算量。
equivalence (a(1,1,5),b)
声明在程序中的作用
这部分对于高性能计算还是比较重要的。首先对于编程来说,声明本身一定要出现在数值计算之后。对于数值计算来说,声明的部分在代码编译之后,编译器会预留空间给程序使用
Fortran中的结构体type功能
给一个实例:
!结构体定义
type: person
integer :: age
integer :: length
integer :: weight
end type person
!结构体声明和赋初值
type(person) :: a=person(20,170,60)
!结构体的使用
a%age = 12
第五章 流程控制与逻辑运算
就是if
和select case
命令。其中判断语句如果对字符串使用,字符串会转换成数字进行比较,例如"abc"<"abcd"
。然后给出一个select case
的例子
select case(score)
case(60)
a=1
case(:59)
a=0
case(61:100)
a=2
case(101:)
a=3
case default
a=4
end select
pause,continue,stop
pause
代表暂停,直到用户按下Enter键。continue
功能就是继续向下执行程序,只是方便阅读。stop
是终止程序运行。
第六章 循环
do
循环的循环体有多种写法:
do i = 1,10
a=a+i
end do
do 100 i=1,10
100 a=a+i
do 100 i=1,10
a = a+i
100 continue
cycle和exit
前者为直接进行下一次循环,后者为跳出当前循环
循环的署名
新知识,例子如下:
outter: do i = 1,10
inner: do j = 1,10
....
end do inner
end do outter
第七章 数组
基本知识不提了,这里Fortran有一个其他语言中数组没有的功能,尤其是CFD中需要使用ghost point时非常方便的。
real a(-3:8)
即,下标起始位置可以人为规定,当然默认是1。
数组赋初值有以下几种技巧,其中隐含式循环为之前没用过的新知识:
integer A(5)
DATA A /1,2,3,4,5/
integer b(5)
DATA (B(I),I=2,4) /2,3,4/
integer c(5) = (/1,(2,I=2,4),5/)
对整个数组的操作
假设有三个维数相同的数组a,b,c,那么数组的操作和Matlab中的数组操作会非常类似。这是F90之后的新功能,也是Fortran适合科学计算的一个原因。
a = 5 !全部赋值为5
a=/(1,2,3)/ !数组前三个元素分别赋值1,2,3
a=b !b的元素分别赋值给a
a=b+c
a=b-c
a=b*c
a=b/c
a=sin(b) !对数组的元素分别进行四则运算或者函数运算
甚至还可以像matlab一样非常灵活的只对数组中其中一部分元素进行操作。
a(3:5)=5
a(3:)=5
a(3:5)=(/3,4,5/)
a(1:3) = b(4:6)
a(1:5:2) = 3
a(1:10) = a(10:1:-1)
a(:) = b(:,2)
a(:,:) = b(:,:,1)
除此之外,F95还提供了WHERE功能和FORALL功能,能够自动检索并进行多元素操作。和matlab中功能也类似
where (a<3)
b = a
end where
forall (i=1:size,j=1:size,i>j) a(i,j) = 1
forall (i=1:size,j=1:size,i==j) a(i,j) = 2
forall (i=1:size,j=1:size,i<j) a(i,j) = 3
数组的保存规则
Fortran中数组的保存规则和C语言恰好是相反的比如
integer a(2,3,4,5)
Fortran为下标从左向右,先跑最左侧下标。而C语言为从右向左。另外,当fortran运行时,索引数组中的一个元素,是从首地址开始,根据下标计算元素所在地址。所以维数很高的数组的元素调用会有较大的额外运算量。
另外,CPU在运行时,在地址相邻的位置调用元素可以大大提高cache的命中率,从而提高CPU运行速度。这就要求我们能够对数组下标的顺序进行调整。例如
do i = 1,5
sum = sum + a(1,1,1,1,i)
end do
do i = 1,n
do j = 1,m
a(i,j) = ...
end do
end do
就不如
do i = 1,5
sum = sum + a(i,1,1,1,1)
end do
do i = 1,n
do j = 1,m
a(j,i) = ...
end do
end do
可变大小数组
先给出用法
integer,allocatable :: a(:,:)
if (.not. allocated(a)) then
allocate(a(-1:5,5),stat=error)
end if
a(1,1) = 1
deallocate(a)
这里说明分配内存是动态的,会损失一部分运行速度,但是并不会损失特别多,是比较推荐的一种写法。而stat=error
是用来反馈是否分配内存成功,可以不写。此时动态分配的内存和C类似,是必须释放的。而if条件可以判断a是否已经被分配内存。