1. 为什么在2025年还要聊Fortran第一次在技术社区看到有人讨论Fortran很多人的反应是“这玩意儿不是上世纪的老古董吗”我刚开始接触高性能计算的时候也是这个想法直到被一个做气象数值模拟的同行拉进项目才发现自己错得离谱。Fortran不仅没死反而在科学计算、气象预报、计算流体力学、量子化学、结构仿真这些领域活得相当滋润。全球排名前列的超算中心里大量核心求解器仍然是Fortran写的很多商业仿真软件的内核也保留着Fortran的血统。这篇文章想做的事情很明确给那些因为课程、科研或工作需要不得不接触Fortran的人一条清晰的入门路径。不管你是刚进课题组的研究生还是从Python/Matlab转过来做数值计算的工程师又或者是被 legacy 代码维护任务砸中的开发者我都会从环境搭建、语法核心、数组思维、性能调优到实际踩坑把这条路上该知道的都讲清楚。我不会只给你语法手册上能查到的东西而是把那些真正上手之后才会明白的经验揉进去。Fortran的全称是Formula Translation1957年诞生于某公司是世界上第一个被广泛使用的高级编程语言。它的设计初衷就是让科学家和工程师能用接近数学公式的方式写程序而不是去折腾汇编和机器码。这个基因决定了它到今天为止的核心竞争力数组操作天然贴合数学表达数值计算性能极强编译器优化空间大。最新标准Fortran 2018和正在推进的Fortran 2023已经加入了大量现代语言特性比如面向对象、泛型编程、并行计算支持、C互操作等早就不是很多人印象里那个只有GOTO和固定格式的老古董了。适合读这篇的人我大致分三类。第一类是零基础但必须用Fortran做数值计算的比如物理、力学、大气、海洋这些专业的学生你们需要的是快速能跑起来、能改代码、能看懂别人程序的能力。第二类是有其他语言基础想转过来的你们需要的是理解Fortran的思维差异尤其是数组和内存布局这块。第三类是维护老代码的你们需要的是知道哪些老写法该保留、哪些该重构、怎么和现代工具链接轨。这三类人的需求不一样但底层逻辑是相通的我会尽量兼顾。2. 环境搭建与第一个可运行程序2.1 编译器选择别在工具上浪费时间Fortran的编译器选择其实比很多人想的要简单。主流的就那几个GCC套件里的gfortran、Intel的ifx注意是ifx不是ifort了ifort已经进入维护阶段、以及某些平台上的商业编译器。对于入门和绝大多数日常开发gfortran完全够用免费、跨平台、社区支持好。如果你做的是对性能极度敏感的大规模并行计算Intel编译器在特定硬件上的优化确实有优势但那是后期的事情入门阶段不用纠结。Windows上最省心的方案是装MSYS2或者直接用一个预编译的MinGW-w64包里面自带gfortran。Linux用户更简单一条命令的事。macOS用户装个Homebrew然后brew install gccgfortran就跟着来了。我见过太多新手在环境配置上卡半天其实真没必要选最顺手的就行语言本身才是重点。安装完之后验证一下gfortran --version能输出版本号就说明装好了。我建议入门阶段直接用命令行编译不要一上来就折腾IDE。命令行能让你看清楚编译的每个环节对理解整个工具链有好处。等你熟悉了之后想用VS Code加Modern Fortran插件或者用其他编辑器都是水到渠成的事。2.2 第一个程序从“Hello World”到有意义的计算经典的开场白还是要走一遍的但我想让它稍微有点意义不只是打印一行字。下面这个程序计算了1到100的平方和顺便展示Fortran的基本结构program square_sum implicit none integer :: i, total total 0 do i 1, 100 total total i * i end do print *, Sum of squares from 1 to 100 is: , total end program square_sum保存为square_sum.f90然后编译运行gfortran -o square_sum square_sum.f90 ./square_sum输出应该是338350。这个程序虽然简单但包含了几个关键信息。implicit none是必须加的它强制你声明所有变量避免Fortran历史上那个臭名昭著的隐式类型规则带来的bug。program ... end program是程序单元的标志。integer :: i, total是变量声明双冒号是Fortran 90之后的标准写法。do ... end do是循环结构比老式的带标签循环清晰太多。注意如果你看到老代码里没有implicit none而且变量不声明就能用那是Fortran 77甚至更早的写法。新写的代码一定要加implicit none这是铁律能帮你省下无数调试时间。2.3 编译流程与常见报错处理Fortran的编译流程和C类似但细节上有自己的特点。一个.f90文件从源码到可执行文件大致经过预处理、编译、汇编、链接几个阶段。gfortran默认会根据文件扩展名判断语言标准.f90/.f95/.f03/.f08对应不同版本的Fortran标准.f/.for通常被当作固定格式的Fortran 77处理。这个细节很重要因为固定格式和自由格式的语法差异很大放错扩展名会导致一堆莫名其妙的错误。新手最常遇到的几个报错我列一下报错信息常见原因解决方法Unclassifiable statement固定格式下代码没有正确缩进或者自由格式文件用了.f扩展名检查文件扩展名自由格式用.f90Symbol already has a basic type变量重复声明或者implicit none下变量名拼写不一致检查变量声明和拼写Unexpected end of file缺少end program或end subroutine补全程序单元结束语句Rank mismatch数组维度不匹配检查数组声明和调用时的维度Segmentation fault数组越界或未初始化指针编译时加-fcheckall检查编译时加-Wall -fcheckall -g是个好习惯能把很多隐藏问题提前暴露出来。-Wall打开所有警告-fcheckall做运行时检查-g保留调试信息。这些选项在开发阶段应该常开等代码稳定了再关掉做性能测试。3. 核心语法与思维转变3.1 变量声明与类型系统比你想的严格Fortran的类型系统比很多人印象中要严格得多尤其是加了implicit none之后。基本类型有integer、real、complex、character、logical每种还有不同的精度种类kind。精度种类是Fortran里非常重要的概念它决定了数值计算的精度和范围。integer, parameter :: dp kind(1.0d0) real(dp) :: x, y complex(dp) :: z character(len20) :: name logical :: flag这里dp是双精度实数的kind值通常等于8对应64位。用kind(1.0d0)而不是直接写数字是为了可移植性。不同编译器上kind值可能不同但kind(1.0d0)总是能拿到双精度的正确值。这个技巧在实际项目中非常常用建议养成习惯。Fortran的变量声明还有一个特点声明必须在程序单元的开头执行语句之前。这和C可以在任意位置声明变量不一样。虽然Fortran 2008之后允许在block结构里声明但主流风格还是把声明集中在开头。这个规则一开始可能觉得别扭但它强制你把程序的“数据布局”想清楚再写逻辑长期来看是好事。3.2 数组操作Fortran真正的杀手锏如果说Fortran有什么是其他语言很难替代的数组操作绝对排第一。Fortran的数组是语言内置的不是库实现的编译器能对数组操作做深度优化。更重要的是它的数组语法几乎和数学公式一一对应。real(dp), dimension(1000) :: a, b, c ! 整个数组相加一行搞定 c a b ! 数组切片 c(1:500) a(1:500) * 2.0_dp ! 条件赋值 where (a 0.0_dp) b a elsewhere b 0.0_dp end where这种写法不仅简洁而且编译器能自动向量化生成利用SIMD指令的高效代码。我做过一个测试同样的数组加法Fortran的c a b和C的手写循环相比在开启优化的情况下Fortran版本通常能快10%到30%因为编译器对数组语义的理解更充分能做出更激进的优化。数组的内存布局也是必须理解的点。Fortran是列优先column-majorC是行优先row-major。这意味着二维数组a(i,j)在内存里i方向是连续的。这个差异在写嵌套循环时至关重要! 好的写法内层循环遍历第一个下标 do j 1, n do i 1, m a(i, j) ... end do end do ! 差的写法缓存不友好 do i 1, m do j 1, n a(i, j) ... end do end do这两种写法在功能上完全等价但性能可能差几倍。原因就是缓存局部性第一种写法按内存顺序访问第二种跳着访问。这个坑我踩过不止一次尤其是从C转过来的时候肌肉记忆总是让我写错。3.3 流程控制与程序单元Fortran的流程控制语句和其他语言大同小异但有几个细节值得注意。if语句有单行和多行两种形式多行形式用then/else if/else/end if。select case相当于其他语言的switch但不需要break而且支持范围匹配select case (score) case (90:100) print *, A case (80:89) print *, B case default print *, Below B end select程序单元是Fortran组织代码的基本方式包括program、subroutine、function、module。其中module是最重要的概念之一它相当于其他语言的命名空间加类库。把相关的函数、子程序、全局数据放在一个module里其他程序单元通过use语句引用这是现代Fortran的标准做法。module math_utils implicit none private public :: add, multiply contains function add(a, b) result(c) real(dp), intent(in) :: a, b real(dp) :: c c a b end function add function multiply(a, b) result(c) real(dp), intent(in) :: a, b real(dp) :: c c a * b end function multiply end module math_utilsprivate和public控制模块成员的可见性intent(in)声明参数是输入这些现代特性让Fortran代码的可维护性大幅提升。我强烈建议新项目从一开始就用module组织代码不要像老代码那样到处写全局函数。4. 从能跑到跑得快性能优化实战4.1 编译优化选项免费的午餐Fortran编译器提供了大量优化选项合理使用能带来数倍的性能提升而且几乎不需要改代码。这是Fortran相比很多语言的一大优势。常用的优化级别选项含义适用场景-O0无优化调试阶段-O1基本优化快速验证-O2标准优化日常开发-O3激进优化性能测试-Ofast最激进优化对精度要求不极端的场景-marchnative针对本机CPU优化本地运行-funroll-loops循环展开小循环体-flto链接时优化多文件项目我通常的开发流程是调试用-O0 -g -fcheckall测试用-O2最终性能测试用-O3 -marchnative。-marchnative让编译器针对当前机器的CPU指令集生成代码能充分利用AVX、AVX-512等向量指令。但注意这个选项生成的可执行文件不能跨机器移植如果要在集群上跑得用集群对应的架构选项。注意-Ofast会开启一些可能影响浮点精度的优化比如快速数学函数和重排结合律。如果你的计算对精度极度敏感慎用这个选项。我一般只在确认精度不受影响的情况下才用它。4.2 数组与循环的优化技巧Fortran的性能优化核心就两件事让编译器能向量化让内存访问有局部性。向量化方面编译器需要确认循环迭代之间没有依赖关系。如果你知道某个循环可以安全向量化但编译器不确定可以用!$omp simd或者编译指示来提示。内存局部性方面除了前面说的循环顺序还有几个实用技巧。第一是尽量用连续内存避免指针数组和链表这类结构。第二是分块blocking把大数组切成能放进缓存的小块来处理integer, parameter :: block_size 64 do jj 1, n, block_size do ii 1, m, block_size do j jj, min(jjblock_size-1, n) do i ii, min(iiblock_size-1, m) ! 处理 a(i,j) end do end do end do end do这种分块技术在大矩阵运算中效果显著能把缓存命中率提升一个数量级。具体块大小要根据CPU的缓存大小调一般L1缓存32KB到64KBL2缓存256KB到1MB块大小让工作集能放进L2就行。第三是避免不必要的数组临时变量。Fortran的数组表达式虽然简洁但有时会生成临时数组带来额外的内存分配和拷贝开销。比如a b c d可能生成两个临时数组。可以用-fcheckarray-temps或者编译器的报告功能来检查。如果临时数组成为瓶颈可以手动展开成循环。4.3 并行计算入门OpenMP与CoarrayFortran对并行计算的支持是语言级别的这是它的一大优势。最常用的是OpenMP通过编译指示实现共享内存并行!$omp parallel do do i 1, n result(i) compute(data(i)) end do !$omp end parallel do编译时加-fopenmp运行时用OMP_NUM_THREADS环境变量控制线程数。就这么简单一个循环就并行化了。当然实际项目中要考虑数据竞争、负载均衡、伪共享等问题但入门门槛确实低。Fortran 2008引入的Coarray是更进一步的并行特性它是语言内置的不需要外部库real(dp) :: a[*] integer :: i a this_image() sync all if (this_image() 1) then do i 2, num_images() a a a[i] end do end ifCoarray的语法看起来有点怪但概念很清晰[*]表示这个变量在每个镜像可以理解为进程上都有一份a[i]访问第i个镜像上的a。sync all是同步点。编译时加-fcoarraysingle单镜像模拟或-fcoarraylib多镜像。Coarray适合做分布式内存的并行但生态还不如MPI成熟入门阶段了解概念即可。5. 常见问题与排查技巧实录5.1 数值精度问题浮点数的坑浮点数精度问题是数值计算中最常见也最隐蔽的bug来源。Fortran的real默认是单精度32位有效数字只有7位左右。很多新手写real :: x然后做累加结果发现误差大得离谱。解决办法很简单用双精度。integer, parameter :: dp kind(1.0d0) real(dp) :: x但双精度也不是万能的。浮点数的比较不能用因为0.1 0.2不等于0.3。正确的做法是判断差值是否小于某个容差if (abs(a - b) 1.0e-10_dp) then ! 认为相等 end if另一个常见问题是灾难性抵消两个相近的大数相减有效数字大量丢失。比如计算1.0e10 1.0 - 1.0e10结果不是1.0而是0.0。遇到这种情况需要重新设计算法避免相近数相减。实操心得在开发数值算法时我习惯用单精度快速验证逻辑然后用双精度跑最终结果。如果单精度和双精度结果差异很大说明算法本身可能有问题需要检查数值稳定性。5.2 数组越界与内存错误Fortran默认不做数组越界检查越界访问可能不报错但结果完全错误或者直接段错误。这是最让人头疼的一类bug。解决办法是开发阶段加-fcheckbounds让编译器插入边界检查代码。虽然会降低运行速度但能快速定位问题。另一个常见错误是数组维度顺序搞反。Fortran是列优先a(i,j)中i是行号第一个维度j是列号第二个维度。从C或Python转过来的人经常搞混。我的建议是在声明数组时用dimension属性明确写出维度并且给变量起有意义的名字比如a(row, col)而不是a(i, j)。未初始化的变量也是常见问题。Fortran不会自动把变量初始化为0未初始化的变量值是随机的。加-finit-local-zero可以让编译器把局部变量初始化为0但这只是调试手段正式代码里应该显式初始化所有变量。5.3 编译链接与模块依赖问题当项目变大多个module互相依赖时编译顺序就变得重要。Fortran的module会生成.mod文件其他程序单元编译时需要读取这些文件。如果编译顺序不对会报“Cannot open module file”之类的错误。解决办法是用Makefile或CMake管理编译流程自动处理依赖关系。一个简单的Makefile示例FC gfortran FFLAGS -O2 -Wall OBJS main.o math_utils.o main: $(OBJS) $(FC) -o $ $(OBJS) %.o: %.f90 $(FC) $(FFLAGS) -c $ main.o: math_utils.mod math_utils.o: math_utils.f90 $(FC) $(FFLAGS) -c $CMake对Fortran的支持也很好而且能自动处理module依赖大型项目建议直接用CMake。5.4 常见问题速查表问题现象可能原因排查方法结果完全错误但无报错数组越界、变量未初始化加-fcheckall -finit-local-zero段错误数组越界、指针未分配加-g用gdb调试检查数组大小精度不够用了单精度改用real(dp)性能远低于预期循环顺序错误、未开优化检查循环嵌套顺序加-O3 -marchnative编译报module找不到编译顺序错误用Makefile/CMake管理依赖并行结果不稳定数据竞争检查共享变量用private/reduction6. 工具链与生态现代Fortran开发6.1 编辑器与IDE配置VS Code是目前最流行的选择装个Modern Fortran插件就能获得语法高亮、自动补全、跳转定义、格式化等功能。配合fortls语言服务器体验接近现代语言。配置也不复杂装好插件后在设置里指定fortls路径和编译器路径就行。如果你习惯用Vim或Emacs也有对应的Fortran插件。Vim的fortran.vim提供语法高亮和缩进Emacs的f90-mode功能更全。我个人的习惯是VS Code写代码命令行编译运行这样既能享受编辑器的便利又能保持对编译流程的掌控。调试方面gdb对Fortran的支持很好能查看数组、module变量、派生类型。编译时加-g -O0然后gdb ./program设置断点、单步执行、打印变量和调试C程序差不多。对于并行程序gdb也能调试OpenMP线程但需要一些额外配置。6.2 常用库与包管理Fortran的生态虽然不如Python丰富但科学计算需要的核心库都有。BLAS和LAPACK是线性代数的基础几乎所有数值计算都绕不开。Intel MKL、OpenBLAS、参考实现BLAS都可以用性能依次递减。调用BLAS/LAPACK需要链接对应的库gfortran -o program program.f90 -lblas -llapack现代Fortran包管理用fpmFortran Package Manager类似Rust的cargo或Python的pip。创建一个新项目fpm new myproject cd myproject fpm runfpm会自动处理依赖、编译、测试。虽然生态还在发展中但已经有不少包可以用比如用于字符串处理的fortran-regex、用于JSON解析的json-fortran等。对于新项目我建议直接用fpm能省很多配置工作。6.3 与C/Python的互操作Fortran和C的互操作是标准特性通过iso_c_binding模块实现。这让Fortran代码可以调用C库也可以被C调用。关键是用bind(c)属性声明接口use iso_c_binding interface function c_function(x) bind(c, namec_function) import :: c_double real(c_double), value :: x real(c_double) :: c_function end function end interface和Python的互操作通常通过f2pyNumPy的一部分或者Cython。f2py能自动把Fortran子程序包装成Python模块对于想把现有Fortran代码暴露给Python的项目非常方便。不过f2py对现代Fortran特性的支持有限复杂项目可能需要手写包装层。7. 学习路径与资源推荐7.1 分阶段学习路线入门阶段1-2周目标是能看懂和修改简单的Fortran程序。重点掌握变量声明、基本类型、if/do/select case、数组基本操作、subroutine和function。这个阶段不用追求写复杂程序能把现成的代码跑起来、改对就行。进阶阶段1-2个月目标是能独立写中等规模的数值计算程序。重点掌握module组织、派生类型、指针、动态数组、文件IO、BLAS/LAPACK调用。这个阶段要多写代码把学到的语法用起来。高级阶段持续目标是能优化性能、做并行计算、维护大型项目。重点掌握OpenMP/MPI/Coarray、性能分析工具、编译优化选项、混合语言编程。这个阶段更多是在实际项目中积累经验。7.2 值得看的书和资料入门书我推荐《Fortran 95/2003 for Scientists and Engineers》讲得细例子多适合自学。进阶可以看《Modern Fortran Explained》覆盖到Fortran 2018是标准级别的参考书。在线资源方面Fortran-lang.org是官方社区有教程、库列表、编译器对比。编译器文档也值得翻gfortran的man page和Intel的开发者文档都有大量实用信息。实操心得不要试图把语法书从头到尾看完再写代码。我的经验是看一章就动手写对应的程序遇到问题再回去查。Fortran的很多特性看的时候觉得懂了写的时候才发现细节没记住。边学边练效率最高。7.3 从老代码中学习的技巧很多人的Fortran之旅是从接手老代码开始的。老代码可能用固定格式、没有module、变量命名随意、注释稀少。读这种代码需要耐心和技巧。我的做法是先找到程序的入口program理清主流程然后逐个理解subroutine和function。遇到不认识的语法先查标准确认是哪个版本的特性。固定格式的代码读起来费劲可以先用工具转成自由格式。gfortran有-ffree-form选项但更彻底的做法是用fprettify或类似工具做格式转换。转换后代码可读性会好很多也方便用现代编辑器处理。重构老代码要谨慎。我的原则是先加测试确保重构前后结果一致再逐步替换一次只改一个模块最后才是优化。不要一上来就大改很容易引入难以发现的bug。8. 一些个人体会Fortran这门语言用久了会发现它的设计哲学其实很一致为数值计算服务。数组操作、精度控制、并行支持都是围绕这个目标。它的“老派”之处比如声明必须在开头、没有异常处理在数值计算场景下反而成了优点——代码结构清晰执行路径可预测。我见过不少人因为Fortran“看起来老”就排斥它转而去用Python或Julia。这些语言当然有它们的优势但在大规模数值计算的核心环节Fortran的性能和稳定性仍然是标杆。而且现代Fortran的语法已经相当友好写起来并不比Python复杂多少。如果你正在学Fortran我的建议是别把它当成一门“老语言”来学把它当成一门“专为数值计算设计的语言”来学。理解它的设计意图顺着它的思路写代码你会发现它其实很优雅。遇到问题多查标准文档少依赖搜索引擎的碎片信息。多写多练尤其是数组操作和module组织这两块掌握了Fortran就算入门了。最后分享一个我常用的调试技巧在程序关键位置插入print *, checkpoint: , variable虽然原始但极其有效。配合-fcheckall编译选项能解决大部分逻辑错误。性能问题则用gprof或perf做 profiling找到热点再针对性优化。这些工具都不难用花半小时学一下收益很大。