1. 为什么在2025年还有人折腾Fortran
第一次跟人聊起我在用Fortran,对方的表情基本可以概括为三个字:图啥呢。这反应我太熟了,毕竟在大多数人印象里,Fortran是那种躺在计算机历史博物馆里的语言,跟打孔卡片、磁带机是一个年代的东西。但实际情况是,如果你去翻一翻天气预报中心的代码库、计算流体力学的开源项目、量子化学的求解器,甚至一些金融衍生品定价的底层库,Fortran的身影到处都是,而且往往是性能最核心的那几块。
我最初接触Fortran纯粹是被逼的。当时手上有一个数值模拟的活儿,用Python写了个原型,跑一次要四十多分钟,迭代一次改个参数再等四十分钟,一天下来跑不了几轮。后来有人丢给我一份Fortran的参考实现,同样的算例,编译完跑下来不到两分钟。这个差距不是几倍,是二十倍。从那天起我就知道,在数值计算这个圈子里,Fortran不是情怀,是实打实的生产力工具。
这篇文章想做的事情很具体:给那些听说过Fortran、但一直没敢下手的人一条能走通的路。我会从环境搭建讲到语法核心,再讲到实际写数值程序时那些文档里不会写的坑。不管你是做物理模拟、工程仿真、气象计算,还是单纯想给自己的Python程序写一个高性能的计算内核,这里的内容都能直接用上。我不打算把它写成一本教科书,而是写成一份"我当初要是有人这么告诉我就好了"的笔记。
需要提前说明的是,Fortran并不是要取代你现有的工具链。它更像是一把专门用来切硬骨头的刀,你平时用Python做数据处理、画图、调库,这些都很好,但遇到计算密集的那部分,把Fortran嵌进去,整个程序的性能曲线会完全不一样。理解这一点,后面的内容你读起来会顺畅很多。
2. 上手之前先把编译环境这件事搞明白
2.1 编译器怎么选:gfortran、ifort还是别的
Fortran的编译器选择比大多数语言要简单,因为主流就那么几个。我按实际使用频率排一下。
gfortran是GNU编译器集合里的Fortran前端,免费、开源、跨平台,Linux上基本自带或者一条命令就能装。对于入门和绝大多数中小型项目,它完全够用。我到现在日常开发还是以gfortran为主,调试方便,报错信息也算清楚。
ifort是Intel的编译器,现在逐步被ifx取代。它的优势在于对Intel CPU的优化做得非常到位,同样的代码用ifort编译出来,在Intel平台上跑得比gfortran快百分之十到三十是常有的事。如果你做的是大规模科学计算,且目标机器是Intel架构,值得花时间配一下。不过它是商业软件,虽然有免费的社区版,但授权条款需要自己确认清楚。
还有一个nvfortran,是NVIDIA家的,主要面向GPU加速场景。如果你打算把计算搬到GPU上,这个编译器配合OpenACC或者CUDA Fortran用起来很顺。但入门阶段不建议碰,先把CPU上的东西写明白再说。
我的建议很直接:入门用gfortran,等你的程序真的跑到了性能瓶颈,再考虑换编译器做对比测试。不要一上来就纠结哪个编译器快百分之几,那个阶段的瓶颈是你的代码写得对不对,不是编译器。
2.2 在Linux上把工具链装起来
Linux是Fortran最舒服的平台,没有之一。以常见的发行版为例,安装过程简单到没什么好说的:
# Debian/Ubuntu系 sudo apt update sudo apt install gfortran # 验证安装 gfortran --version装完之后你会得到一个gfortran命令。编译一个源文件的基本命令是:
gfortran -o myprogram myprogram.f90这里-o指定输出文件名,myprogram.f90是源文件。Fortran的源文件扩展名有讲究:.f90及以后的(.f95、.f03、.f08)代表自由格式,.f和.for代表固定格式。新手一律用.f90,不要碰固定格式,那是上古时代的写法,列位置有严格要求,纯属给自己找麻烦。
如果你还想用Makefile来管理编译,那再装个make就行,通常系统里已经有了。调试的话,gdb配合-g编译选项可以单步调试Fortran代码,体验虽然比不上现代IDE,但够用。
2.3 Windows和macOS上的折中方案
Windows上我推荐两条路。一条是用MSYS2,它提供了一个类Unix的环境,里面可以直接pacman -S mingw-w64-x86_64-gcc-fortran装gfortran,然后用命令行编译。另一条是装Intel oneAPI工具包,图形化安装,装完有ifx可用,还附带一个基于Visual Studio的集成环境。如果你习惯IDE,oneAPI那套体验更好;如果你想要轻量,MSYS2更省事。
macOS上最简单的是用Homebrew:brew install gcc,这个包里面就带了gfortran。注意装完之后命令可能叫gfortran-13之类的带版本号的名字,需要自己做个软链接或者直接用全名。
不管哪个平台,装完之后第一件事是写一个最简单的程序编译运行一遍,确认工具链是通的。这个习惯能帮你省掉后面无数"到底是代码错了还是环境没配对"的纠结。
program hello implicit none print *, "Hello, Fortran" end program hello保存为hello.f90,编译运行,屏幕上出现那行字,环境就算搞定了。
2.4 编辑器与开发体验的取舍
Fortran没有那种"官方推荐IDE"的说法,实际用什么完全看个人习惯。我用过几种组合,说一下感受。
VS Code加Modern Fortran插件是目前比较平衡的方案。语法高亮、跳转定义、简单的补全都有,配合gdb还能断点调试。缺点是插件的语言服务器偶尔会抽风,大项目里索引慢。
纯文本编辑器加命令行是另一派做法。Vim或者Emacs配上Fortran的语法文件,写起来其实很高效,尤其是你熟悉了之后。缺点是没有自动补全,拼错变量名要等编译才报错。
还有一种是用Intel oneAPI自带的IDE,如果你在Windows上且用ifx,这个组合的调试体验是最好的,但比较重。
我的实际选择是VS Code写代码、命令行编译、gdb调试。这个组合不依赖任何商业软件,跨平台一致,迁移成本低。关键不是用哪个编辑器,而是你要把编译和运行的命令练熟,因为Fortran的开发循环里,命令行操作占了很大比重。
3. Fortran的语法骨架:抓住这几根柱子就不会乱
3.1 program、implicit none和那些必须记住的固定结构
Fortran程序的骨架非常固定,一个最小的完整程序长这样:
program main implicit none ! 变量声明区 integer :: i real :: x ! 执行语句区 i = 1 x = 3.14 print *, i, x end program main这里面有几个东西是必须理解的。program main和end program main把整个程序包起来,名字可以自己取。implicit none这一行必须写,没有例外。它的作用是禁止隐式类型推断。Fortran有一个历史遗留特性:如果你用了一个没声明过的变量,它会根据变量名的首字母自动推断类型,i到n开头的是整数,其他是实数。这个特性在几十年前可能方便,现在是纯粹的灾难来源,一个拼写错误会变成一个新变量,编译器不报错,结果全错。implicit none就是把这个口子堵死。
变量声明用::分隔类型和变量名,可以一行声明多个同类型变量,用逗号隔开。注释用!开头,从!到行尾都是注释。这些跟现代语言比不算优雅,但胜在清晰。
注意:
implicit none必须写在所有变量声明之前,写在program语句之后的第一行是最稳妥的做法。如果你写在声明中间,编译器会报错。
3.2 变量类型与kind参数:精度这件事不能含糊
Fortran的基本类型有integer、real、complex、character、logical。看起来跟别的语言差不多,但真正的门道在kind参数上。
默认的real是单精度,大约7位有效数字。做数值计算的时候,这个精度往往不够,你需要双精度。Fortran里写双精度的标准做法是:
integer, parameter :: dp = kind(1.0d0) real(dp) :: x这里kind(1.0d0)返回双精度实数的kind值,把它存到一个常量dp里,后面所有需要双精度的地方都用real(dp)。这个写法比直接写real(8)更可移植,因为不同编译器上real(8)的含义可能不一样,但kind(1.0d0)永远指向双精度。
整数也有kind,integer, parameter :: i8 = kind(1_8)之类,但日常用得少。字符类型character(len=20)需要指定长度,长度不够会截断,这个坑后面会细说。
精度问题在数值计算里是头等大事,我见过太多因为混用单双精度导致结果发散的情况。一个实用的习惯是:在程序开头定义好dp,然后全文统一用real(dp),不要出现裸的real。
3.3 数组:Fortran真正的看家本领
如果只能选一个理由说明为什么数值计算要用Fortran,我会选数组。Fortran的数组是语言级别的公民,不是后来补上去的库功能,这带来的差异是根本性的。
声明一个数组:
real(dp), dimension(100) :: a real(dp) :: b(100) ! 等价的另一种写法 real(dp), dimension(10, 20) :: matrix数组下标默认从1开始,这是Fortran的传统,跟C系的从0开始不一样。你可以改成从0开始,但没必要,习惯就好。
Fortran数组最强大的地方是整体操作。你可以直接写:
a = 0.0_dp ! 整个数组赋值为0 a = a + 1.0_dp ! 每个元素加1 b = a * 2.0_dp ! 逐元素相乘 c = matmul(m1, m2) ! 矩阵乘法这些操作不需要写循环,编译器会自动生成高效的代码,而且往往比你手写的循环更快,因为它能做向量化优化。这是Fortran在数值计算上碾压大多数语言的直接原因。
数组还支持切片:
a(1:50) = b(51:100) ! 把b的后半段赋给a的前半段 a(1:100:2) = 0.0_dp ! 每隔一个元素赋值切片操作配合整体运算,能让数值算法的代码短到不可思议,而且可读性极好。我写过一个有限差分求解器,核心的更新逻辑用数组整体操作写出来只有几行,用C写同样的逻辑要几十行循环。
提示:Fortran是列优先存储,也就是第一个下标变化最快。写嵌套循环的时候,最内层循环应该对应第一个下标,这样内存访问是连续的,性能差别可能有好几倍。这一点跟C相反,从C转过来的人特别容易踩。
3.4 控制流与循环:do、if和select case
Fortran的控制流语法直白到没什么好讲的,但有几个细节值得点出来。
if语句:
if (x > 0.0_dp) then y = sqrt(x) else if (x < 0.0_dp) then y = 0.0_dp else y = 1.0_dp end ifdo循环:
do i = 1, n a(i) = a(i) * 2.0_dp end do do while (condition) ! ... end doselect case:
select case (mode) case (1) call method_one() case (2) call method_two() case default call method_default() end select这些跟其他语言大同小异。真正需要注意的是循环的性能。前面说了列优先的事,这里再强调一次:do j = 1, n; do i = 1, m这样的嵌套,内层是i,对应第一个下标,内存连续,快。反过来写,慢。这个差别在小数组上看不出来,数组一大就是几倍的差距。
另外,Fortran的do循环变量在循环结束后是未定义的,不要依赖循环结束后的值。这个跟某些语言不一样,容易出隐蔽的bug。
3.5 函数与子程序:subroutine和function的分工
Fortran把可调用的过程分成两类:subroutine和function。区别很简单,function返回一个值,subroutine不返回值(但可以通过参数传出结果)。
function square(x) result(y) real(dp), intent(in) :: x real(dp) :: y y = x * x end function square subroutine normalize(a, n) integer, intent(in) :: n real(dp), intent(inout) :: a(n) real(dp) :: norm norm = sqrt(sum(a**2)) a = a / norm end subroutine normalize这里的关键是intent属性。intent(in)表示只读,intent(out)表示只写,intent(inout)表示可读可写。养成给每个参数标intent的习惯,这不仅是文档,编译器还会据此做优化,而且能帮你抓住误改只读参数的bug。
result(y)指定返回值变量名,不写的话默认用函数名本身作为返回变量。我建议都写result,清晰。
函数和子程序可以放在contains块里,成为内部过程,也可以单独放在模块里。实际项目里,绝大多数过程都应该放在模块里,原因下一节讲。
4. 模块化:让代码从能跑到能维护的分水岭
4.1 module为什么是Fortran项目的组织核心
新手写Fortran最常见的做法是所有代码堆在一个文件里,一个program从头写到尾。几十行的脚本这样没问题,但一旦超过几百行,就会变成一团乱麻。Fortran给出的解决方案是module。
一个模块长这样:
module math_utils implicit none private public :: square, normalize real(dp), parameter :: eps = 1.0e-12_dp contains function square(x) result(y) real(dp), intent(in) :: x real(dp) :: y y = x * x end function square subroutine normalize(a, n) integer, intent(in) :: n real(dp), intent(inout) :: a(n) ! ... end subroutine normalize end module math_utils模块里可以放常量、变量、函数、子程序。别的程序单元用use math_utils就能访问里面公开的东西。private和public控制可见性,默认全部公开,但我强烈建议默认private,显式public,这样模块的接口是受控的,内部实现随便改不影响调用方。
模块带来的好处是实打实的。第一,它把数据和操作绑定在一起,形成清晰的边界。第二,模块里的过程有显式接口,编译器能做完整的参数检查,传错类型、传错个数都会在编译期报错。第三,模块之间有依赖关系,编译器能自动处理编译顺序,你不需要手动排。
4.2 显式接口与隐式接口的坑
Fortran有一个历史遗留问题:外部过程如果没有显式接口,编译器就按隐式接口处理,参数类型和个数都不检查。这意味着你调用一个函数时传错了参数,编译器可能一声不吭,运行时才出问题,而且往往是莫名其妙的结果。
模块里的过程自动有显式接口,这是用模块的又一个理由。如果你确实需要写独立的外部过程,那就要用interface块显式声明接口,或者把它放进模块。我的做法是:所有过程都放模块里,一个例外都不留。这样接口问题从根上就不存在了。
这个坑我踩过。早期写的一个程序,调用一个外部函数时少传了一个参数,编译器没报错,运行时读到了栈上的垃圾值,结果算出来的数看着像那么回事但完全不对,查了两天才定位到。从那以后我就再也没写过模块外的过程。
4.3 编译顺序与Makefile的最小可用配置
用模块之后,编译顺序就有讲究了。如果module_a被module_b使用,那必须先编译module_a。手动排顺序很烦,所以实际项目都用Makefile或者CMake来管理。
一个最小可用的Makefile大概长这样:
FC = gfortran FFLAGS = -O2 -Wall -fcheck=all OBJS = math_utils.o main.o program: $(OBJS) $(FC) $(FFLAGS) -o $@ $(OBJS) %.o: %.f90 $(FC) $(FFLAGS) -c $< clean: rm -f *.o *.mod program这里-O2开优化,-Wall开警告,-fcheck=all开运行时检查(数组越界、未初始化变量等)。开发阶段一定要开-fcheck=all,它能在运行时抓住很多隐蔽的错误,代价是程序慢一些。发布的时候再去掉。
.mod文件是编译模块时生成的,里面存了模块的接口信息。它跟.o文件一样是中间产物,clean的时候要一起删掉。有时候改了模块但没重新编译依赖它的文件,会出现各种奇怪的链接错误,这时候make clean再make通常能解决。
CMake是更现代的选择,尤其项目大了之后。但入门阶段Makefile够用,而且能让你看清楚编译到底发生了什么。
5. 数值计算实战:从零写一个能跑的程序
5.1 一个热传导求解器的完整实现
光讲语法没意思,我们直接写一个能跑的东西。选一个经典的例子:一维热传导方程的显式差分求解。这个问题足够简单,但包含了数值计算程序的几乎所有要素:数组操作、循环、时间步进、边界条件、结果输出。
物理模型是:一根长度为L的杆,初始温度分布已知,两端温度固定,求温度随时间的演化。控制方程是∂u/∂t = α ∂²u/∂x²,用显式差分格式离散:
u_i^{n+1} = u_i^n + r * (u_{i+1}^n - 2u_i^n + u_{i-1}^n)
其中r = α * dt / dx²,稳定性要求r ≤ 0.5。
module heat_solver implicit none private public :: solve_heat integer, parameter :: dp = kind(1.0d0) contains subroutine solve_heat(nx, nt, alpha, length, total_time, u) integer, intent(in) :: nx, nt real(dp), intent(in) :: alpha, length, total_time real(dp), intent(inout) :: u(nx) real(dp) :: dx, dt, r real(dp), allocatable :: u_new(:) integer :: i, n dx = length / real(nx - 1, dp) dt = total_time / real(nt, dp) r = alpha * dt / (dx * dx) if (r > 0.5_dp) then print *, "警告:r =", r, "超过稳定性限制0.5,结果可能发散" end if allocate(u_new(nx)) do n = 1, nt u_new(1) = u(1) ! 左边界固定 u_new(nx) = u(nx) ! 右边界固定 do i = 2, nx - 1 u_new(i) = u(i) + r * (u(i+1) - 2.0_dp*u(i) + u(i-1)) end do u = u_new end do deallocate(u_new) end subroutine solve_heat end module heat_solver主程序:
program main use heat_solver implicit none integer, parameter :: dp = kind(1.0d0) integer, parameter :: nx = 101, nt = 5000 real(dp), parameter :: alpha = 1.0_dp real(dp), parameter :: length = 1.0_dp real(dp), parameter :: total_time = 0.1_dp real(dp) :: u(nx) integer :: i ! 初始条件:中间高,两端低 do i = 1, nx u(i) = sin(3.14159265358979_dp * real(i-1, dp) / real(nx-1, dp)) end do call solve_heat(nx, nt, alpha, length, total_time, u) ! 输出结果 do i = 1, nx print *, real(i-1, dp) / real(nx-1, dp), u(i) end do end program main这个程序虽然小,但结构是完整的:模块封装求解逻辑,主程序负责初始化和输出。编译运行之后,把输出重定向到文件,用任何绘图工具都能画出温度分布。
5.2 性能优化的几个实际手段
上面这个程序能跑,但离"快"还有距离。我按实际收益从高到低说几个优化手段。
第一,编译优化选项。-O2是基础,-O3更激进,-march=native让编译器针对当前CPU生成指令。这三个加上去,性能通常能提升两三倍。但-march=native编译出来的程序换台机器可能跑不了,发布的时候要注意。
第二,循环顺序。前面反复强调的列优先。这个例子里是一维数组,体现不出来,但如果是二维、三维,循环顺序的影响巨大。
第三,减少临时数组的分配。上面用了u_new做临时存储,每次时间步进都要整体拷贝一次。更好的做法是用两个数组交替,避免拷贝:
! 交替使用u和u_new,每步交换角色 do n = 1, nt ! 用u计算u_new ! 然后交换 tmp => u u => u_new u_new => tmp end do用指针做交换,避免数据拷贝。这个技巧在时间步进类算法里很常用。
第四,用数组整体操作替代显式循环。上面内层的循环可以写成:
u_new(2:nx-1) = u(2:nx-1) + r * (u(3:nx) - 2.0_dp*u(2:nx-1) + u(1:nx-2))一行顶一个循环,而且编译器能更好地向量化。实测下来,这种写法通常比手写循环快,代码还更短。
第五,考虑OpenMP。如果数组很大,加几行OpenMP指令就能多核并行:
!$omp parallel do do i = 2, nx - 1 u_new(i) = u(i) + r * (u(i+1) - 2.0_dp*u(i) + u(i-1)) end do !$omp end parallel do编译时加-fopenmp,运行时用OMP_NUM_THREADS控制线程数。对于计算密集的循环,加速比接近核数。
5.3 跟Python的混合编程:把Fortran当计算内核
实际工作中,纯Fortran的项目越来越少,更常见的是Python做上层逻辑,Fortran做计算内核。这条路走通了,你既能享受Python的生态,又能拿到Fortran的性能。
最省事的方案是用f2py,它是NumPy自带的工具。你写好Fortran子程序,用一条命令就能生成Python可调用的模块:
f2py -c -m heat_core heat_solver.f90然后在Python里:
import heat_core import numpy as np u = np.sin(np.linspace(0, np.pi, 101)) heat_core.solve_heat(101, 5000, 1.0, 1.0, 0.1, u)f2py会自动处理数组的内存布局转换,你不需要手动操心。需要注意的是Fortran的数组是列优先,NumPy默认是行优先,f2py会做转置,对于一维数组没影响,多维数组要留意。
另一个方案是用iso_c_binding写C接口,然后用ctypes或者cffi调用。这个更灵活但更繁琐,适合需要精细控制内存的场景。
我自己的习惯是:原型阶段全用Python,确认算法正确之后,把最耗时的部分用Fortran重写,用f2py接回去。这样开发效率和运行效率都不丢。
6. 那些文档不会告诉你的踩坑记录
6.1 数组越界不报错的恐怖
Fortran默认不做数组越界检查。你写a(101)访问一个长度为100的数组,编译器不报错,运行时也不报错,它只是默默地读到相邻内存里的数据,或者更糟,写到相邻内存里。结果就是程序行为完全不可预测,可能这次跑对了下次跑错了,可能改一个完全无关的地方就崩了。
这个坑的可怕之处在于它的隐蔽性。我遇到过一次,一个数组越界写坏了旁边一个循环变量的值,导致循环次数不对,但算出来的结果看着还挺合理,直到对比参考解才发现偏差。查了大半天。
解决办法就是开发阶段永远开着-fcheck=all。它会在每次数组访问时检查边界,越界立刻报错并指出位置。代价是程序慢一些,但开发阶段这点性能损失完全值得。发布的时候再去掉。
6.2 单双精度混用的隐蔽bug
Fortran里1.0是单精度,1.0d0是双精度。如果你在双精度的计算里混进了一个单精度的常量,整个表达式的精度可能就被拉低了。更隐蔽的是,这种精度损失往往不会让结果明显错误,只是有效数字少了几位,你不做严格的误差分析根本发现不了。
我见过一个案例,一个物理模拟程序,所有变量都声明成双精度,但有一个常量写成了3.14159而不是3.14159d0,结果长时间积分之后误差累积,最终结果偏了百分之几。查了很久才定位到这个常量。
习惯就是:只要用了real(dp),所有相关的常量都加_dp后缀。定义dp的时候顺便定义pi = 3.141592653589793_dp这样的常量,全文用pi,不要到处写数字。
6.3 字符串处理的那些反直觉设计
Fortran的字符串处理跟现代语言比相当原始。character(len=20)声明一个长度20的字符串,赋值时如果源字符串短于20,右边补空格;长于20,截断。这个"补空格"特性会导致比较出问题:"abc"和"abc "在Fortran里可能不相等,因为长度不同。
比较字符串的时候要用trim()去掉尾部空格:
if (trim(str1) == trim(str2)) then ! ... end if字符串拼接用//:
full_name = trim(first_name) // " " // trim(last_name)不trim的话,中间会夹着一堆空格。
这些设计在数值计算里影响不大,因为Fortran程序很少做大量字符串处理。但如果你要从文件读配置、解析输入,就会频繁遇到,需要适应。
6.4 文件IO的格式化输出陷阱
Fortran的print *是列表导向输出,格式由编译器决定,不同编译器输出可能不一样。要精确控制输出格式,得用write配合格式说明符:
write(*, '(A, I5, F10.4)') "结果:", count, value这里A是字符串,I5是宽度5的整数,F10.4是宽度10、小数4位的浮点数。格式说明符是Fortran的一个独立子系统,功能强大但语法繁琐。
写数据文件的时候,我通常用F格式或者科学计数法E格式。注意F格式宽度不够时会输出星号****而不是截断,这是Fortran的一个保护机制,但如果你没注意,数据文件里就会出现一堆星号。
write(*, '(E15.7)') x ! 科学计数法,宽度15,7位小数读文件的时候,格式要跟写的时候匹配,否则会读错。我建议数据文件用简单的列表导向读写,格式文件用格式说明符,分开处理。
6.5 编译优化与调试的冲突
开了-O2或-O3之后,编译器会重排指令、内联函数、向量化循环,这会导致调试的时候单步执行跟源码对不上,变量值也可能被优化掉看不到。更麻烦的是,有些在-O0下正常的代码,开了优化之后因为未定义行为而表现异常。
开发流程应该是:-O0 -g -fcheck=all写代码和调试,确认正确之后再用-O2或-O3编译发布版本,并且发布版本要重新跑一遍测试。不要用优化版本调试,也不要用调试版本做性能测试。
如果优化版本出了问题而调试版本正常,八成是代码里有未定义行为,比如数组越界、未初始化变量、别名问题。这时候回头用-fcheck=all仔细查。
7. 从能用到好用:我的个人经验
写了这些年Fortran,有几个体会是越来越深的。
第一,Fortran的价值在数值计算,不在通用编程。不要试图用它写Web服务、做文本处理、搞GUI,那些不是它的战场。它的战场是数组、循环、浮点运算,在这些地方它依然是顶尖的。认清这一点,你的技术选型就不会拧巴。
第二,现代Fortran跟老Fortran是两回事。Fortran 90引入了数组操作、模块、动态内存,Fortran 2003加了面向对象,Fortran 2008和2018继续补强。你现在写的Fortran可以很现代,有模块、有类型、有接口,不是那种满屏goto和common块的老古董。不要被老代码吓到,新写的代码可以很干净。
第三,工具链的熟练度决定开发效率。编译命令、Makefile、调试器、性能分析工具,这些花时间练熟,收益是长期的。我见过很多人卡在"改了代码不知道怎么重新编译"这种问题上,其实就几条命令的事。
第四,跟其他语言配合是常态。纯Fortran项目越来越少,混合编程越来越多。f2py、iso_c_binding这些桥接技术值得花时间学,它们能让Fortran的能力嵌入到更大的系统里。
第五,性能优化要有数据支撑。不要凭感觉猜哪里慢,用gprof或者perf做性能分析,找到真正的热点再优化。我见过太多人优化了不重要的地方,真正耗时的循环反而没动。
最后说一个具体的技巧。如果你要从零开始一个Fortran项目,我的建议是先用Python或者甚至手算把算法验证一遍,确认数学是对的,再用Fortran实现。Fortran的调试成本比Python高,把算法错误和实现错误分开排查,能省很多时间。算法对了,Fortran实现只是翻译,出问题基本就是语法和内存的坑,范围小很多。
这个语言不新,不酷,学它的人越来越少。但在它擅长的领域里,它依然是那个最能打的。如果你手头有计算密集的活儿,花几天时间把Fortran捡起来,长期看是划算的。