本节摘要:数组写法和形态都会了,还剩最后一层:把数组当参数传进过程时,怎么保证编译器不"保守处理"。假定形状数组用显式接口换来接口灵活性,延迟形状与自动数组分别住在堆和栈上,contiguous 属性则是一份"我的实参内存连续"的承诺书。本节把三种形状声明、显式接口与连续性的性能契约讲透。
老代码里常见这种过程:subroutine solve(a, n),数组大小靠整数参数 n 传进去,数组声明写成 real a(n)。某天网格从一维变成二维,这个子程序的接口就得改,所有调用点跟着改,编译不过的地方一片红。这就是把"维度信息"硬编码进接口的代价。假定形状数组 real :: a(:) 的初衷就是消灭这种连锁重构——接口只承诺"这是一维数组",形状由调用方决定。
| 声明方式 | 语法 | 内存位置 | 特点 |
|---|---|---|---|
| 假定形状 | real :: a(:) |
实参所在处 | 接口灵活,须显式接口,实参可为切片 |
| 延迟形状 | real, allocatable :: a(:) |
堆 | 运行时分配,作用域结束自动释放 |
| 自动数组 | real :: a(n)(n 来自参数) |
栈 | 过程进入分配、退出释放,快但栈有上限 |
subroutine fill_default(d) use iso_fortran_env, only: real64 implicit none real(real64), intent(inout) :: d(:) ! 假定形状:任何一维实数组都能传进来 d = 1.0_real64 end subroutine fill_default program call_fill use iso_fortran_env, only: real64 implicit none real(real64) :: m(10, 10) call fill_default(m(:, 1)) ! 传一列切片,合法 call fill_default(m(1, :)) ! 传一行切片,也合法 end program call_fill
假定形状的代价:编译器需要数组描述符(秩、边界、步长)才能生成正确的访问代码,这要求显式接口——过程定义在模块里,或写成内部过程,或手工写 interface 块。裸的外部子程序没有显式接口,用假定形状会编译报错。自动数组的代价相反:栈空间有限,大数组会栈溢出;但它分配快、无泄漏风险,适合中小规模的临时工作区。
为什么必须显式接口?因为编译器要检查实参的秩、类型与 intent 是否匹配,这需要完整的过程签名。放在模块里的过程自带显式接口,这也是"一切过程都该进模块"这条工程铁律的技术原因。没有显式接口,你连参数个数错了都要等到运行时才炸。
module math_ops_mod use iso_fortran_env, only: real64 implicit none contains real(real64) function norm2_v(v) result(n) real(real64), intent(in) :: v(:) ! 模块内过程,自动获得显式接口 n = sqrt(sum(v * v)) end function norm2_v end module math_ops_mod program use_norm use math_ops_mod, only: norm2_v use iso_fortran_env, only: real64 implicit none real(real64) :: x(4) x = [3.0_real64, 0.0_real64, 4.0_real64, 0.0_real64] write(*,*) norm2_v(x) ! 5.0 end program use_norm
假定形状数组作为实参时,可能指向连续内存,也可能只是某个大数组的切片。编译器无法确定,只能假设最坏情况,于是放弃 SIMD。contiguous 属性就是这份承诺书:声明实参内存连续,编译器据此生成向量指令。
subroutine saxpy_c(v, x, y) use iso_fortran_env, only: real64 implicit none real(real64), intent(in), contiguous :: v(:), x(:) real(real64), intent(inout), contiguous :: y(:) y = y + v * x ! 编译器可安全向量化 end subroutine saxpy_c
代价也明确:如果调用方真传了一个不连续的切片,编译器要么生成拷贝,要么报错。所以 contiguous 适用于"我保证传连续实参"的核心内核;在通用接口层别滥用,否则会把合法调用挡在门外。经验法则:性能敏感、形状规则的数值内核加 contiguous;面向用户的通用过程保持宽松。

有一类性能问题几乎每个 Fortran 项目都会撞上:同样的代码,传整块数组快,传某列切片慢。原因正是连续性——切片实参在声明层就丢了连续性承诺,优化层只能保守。解决办法不是加 contiguous 让编译器硬着头皮优化,而是调整数据结构或接口:要么按列存成连续块,要么在入口处 pack 一次。先想清楚数据该长什么样,再谈属性,这是数组性能调优的正序。第 8 章性能分析一节会再回到这个模式。
阅读完本节,你应当能够:
数组引擎加满油了,下一站是它的"变速箱"——模块与过程设计。