现代Fortran的推荐范式
开发密度泛函软件已经有几年时间,主要是阅读和修改比较成熟的软件包,比如siesta,abinit,octopus,elk和quantum espresso。这些软件包,尤其是quantum espresso,为了多人协作而制定了很多规范,也积累了很多经验。
很久以来都有这样一个想法,总结一下这几年学到的一些东西。现在作为quantum espresso的developer之一,也应该帮助推广一下。
Fortran作为一个很老的语言,其最大的一次变革来自于90标准的推行。这也导致Fortran程序被割裂成两个不同的范式:
- 以77为标准的高效和节约内存,但完全不符合现代软件工程原理,可读性极差的程序范式;
- 以90标准以及以后的95,03,08为标准的,逐渐符合软件工程原理,逐渐向c靠拢的程序范式。
但除了这两种很“纯”的范式,混合两种范式而形成的排列组合,甚至一套代码中两种范式并存,导致一个软件包中不同的两个文件简直像两个世纪的产物(对,siesta,说的就是你)。
由于有这些混乱的范式,本该有一本权威书籍来正本清源。可惜的是,相对其他编程语言,Fortran的教科书可谓灾难。即使是以复杂、多范式著称的c++,不仅有从c++ primer这本煌煌巨著阐明语法特性,更有Effective c++这种书籍来推荐编程范式。相比之下,Fortran的书籍,无论中文的还是英文的(中文更旧更糟一些),仍以描述语言特性为主。很多语言特性,尤其是为了兼容77而保留的语言特性,实在是不值得不应该再拿出来讲,更不应该推荐使用。而相应的更为现代化的Fortran写法,我只在实际参与这些软件包时,才从实践中学得一二。因此感觉十分需要一本Effective Fortran来规范各路牛鬼蛇神。
因此,为了能一定限度上造福同僚,也为以后教(可能存在的)学生,缓慢更新这个文章,把(我认为的)Fortran应该怎么写,以碎片化的方式列举在这,尽量加以解释。显然,本编者半路出家,水平极其有限,不过既然不收钱,也就不用这么多顾虑了。
软件工程规范
- 现代Fortran一定是以主程序program和多个module组成的。
- 所有的函数function和subroutine应该封装在module中。这样既可以避免同名函数导致的链接错误,更能使编译器在链接时帮忙检查参数列表。
subroutine f (x)
! 野函数,不推荐, 编译器不会检查x是不是real,是不是一维
implicit none
real(dp), intent(in) :: x(:)
end subroutine f
module a
contains
subroutine f (x)
! 推荐, 编译器会检查传入的x是否合法
implicit none
real(dp), intent(in) :: x(:)
end subroutine f
end module
- use语句都应该加only,遵循最小化原则。
- module中的public元素(变量和函数)应该显式列出,其他的应保留为private。最小化原则。
- 如下代码中module c 中 use b, only : x 不仅是合法的,而且会修改同一个x变量,即a中的x
module a
real(dp) :: x
end module
module b
use a, only : x ! 合法,使用的是a中的x, 不推荐
end module
module c
use b, only : x ! 也合法,使用的是仍是a中的x,不推荐
end module
! 推荐写法
module d
contains
subroutine f
use a, only : x ! 合法,且d中不会如b一样多出一个x变量
end subroutine
end module
module f
use d, only : x ! 不再合法,避免了交叉引用
end module
- 所有的module,function和subroutine都应该用implicit none。
- 数组应尽量使用allocatable而不是pointer,因为某些抽风的编译器会对pointer报错。这条不是一定正确,十分依赖于编译环境,这也是Fortran的另一个大问题。
- 数组应尽量使用allocatable而不是直接指定维度,因为前者出自堆而后者出自栈,而栈的空间十分有限。例如
subroutine f(m)
implicit none
integer, intent(in) :: m
real(dp) :: a(m) !不好的写法,除非m一定很小,否则很容易爆内存
real(dp), allocatable :: b(:)
allocate(b(m)) ! 推荐写法
end subroutine
摒弃过时语法
- 不再使用固定格式。
- 不用goto。
- 使用< = /= >来表示大于小于等于和不等于,不用.eq. .neq. .gt.写法。
- 声明变量使用 integer :: a, b,而不是 integer a b.
- 不用double complex和double precision,改用 real (dp) :: x 和 complex(dp) :: x。这里dp是一个指定精度。
- 在特定情况下用大写,比如关键字。其他保持小写。
- 杜绝任何common block和data。
- 相比起逐行按位置读入文件,namelist是更为健壮且可读性更好的选择。比如
integer :: a, b, c
! 第一种, 直接读, 输入文件必须是
! 234 51 122
read(ifile, *) a, b, c
! 第二种, 用namelist,输入文件变成
! &inputCon
! a = 234, c=122, b=51
! 顺序不重要,大小写不重要,甚至有没有都不重要(当然你要有默认值)
namelist / inputCon / a, b, c
read(ifile, inputCon)
编译器兼容
- 操作矩阵时,使用a(1:N) = b(1:N), 而不是a = b 或 a(:) = b(:)。某些编译器无法检测a和b的上下限,显式写出就更为安全。
- 大多数编译器不会检查数组越界。如果不小心访问A(N)的第N+1个元素,有些编译器会出现segmentation fault,有些则会返回A(1),即循环访问。
- 在传递数组参数A(N)给subroutine b(a)时,在b中a的size在有些编译器中是无定义的。
- 一般在软件包中,dp都会在某个module中定义,以保证数值精度的统一。某些比较完善的软件包,还会定义zero,one和czero, cone作为0,1,和0+i0, 1+i0. 使用。在需要时一定要用坚持使用这些常量,而不是直接写1.0, 0.0等,以保持精度的一致性。
MPI规范
- 一般读入输入参数都是IOnode,即第一个进程/节点/CPU,所以一定记得broadcast。如果忘记,可能会有下面这几种情况:
logical :: do_cal
integer :: a
real(dp), allocatable :: x(:)
! (1) 读入一个整形, 此时除了第一个进程上有需要的a,其他的进程还是初始值
if(ionode) call read_input(a)
! (2) 所有进程的a都是同样的值(用了QE的API进行broadcast)
call mp_bcast(a, ionode_id, global_comm)
! (3) 如果没有第(2)步,而你恰好忘了给a初值,这个allocation会让你的内存爆炸
allocate(x(a))
! (4) 读入x的值,如果忘了(3), 则seg fault
if(ionode) call read_input_array(x)
! (5) 所有进程的x都是同样的值. 如果忘了,只有ionode上是正确的输入
call mp_bcast(x, ionode_id, global_comm)
! (6) 如果是个控制参数, 读入后只有ionode为.true.
if(ionode) call read_input(do_cal)
! (7) 广播,所有进程都变成.true.
call mp_bcast(do_cal, ionode_id, global_comm)
! (8) 如果忘了(7),那么只有ionode进去了
! 如果new_f里有mpi函数,程序会被block(虽然很惨但是还算幸运)
! 如果new_f里面没有mpi函数,那么结果大概率会出错(更惨,更难发现的bug)
if(do_call) call new_f()
多人协作规范
- 修改已经比较成熟的软件,能够另起一行就不要在同一行进行修改,这是为了在git管理时,能够无冲突merge。例如
use a, only : x ! 已经有这一行,但仍需要y
use a, only : y ! 另起一行,加入y
- 如果需要新增加功能,用一个新文件来承载主体内容,用一个新增的控制变量来进入你新增加的函数,这个控制变量应该是默认关闭的。
program test
use new_module, only : new_func
implicit none
logical :: call_new_func = .false.
! ........
call test_readin() ! 读入输入参数,在输入文件中可以将call_new_func设成.true.
if (call_new_func) call new_func
! .......
end program test