3.3 高级数组特性与性能契约


3.3 高级数组特性与性能契约

本节摘要:数组写法和形态都会了,还剩最后一层:把数组当参数传进过程时,怎么保证编译器不"保守处理"。假定形状数组用显式接口换来接口灵活性,延迟形状与自动数组分别住在堆和栈上,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

contiguous:跟编译器签一份连续性的约

假定形状数组作为实参时,可能指向连续内存,也可能只是某个大数组的切片。编译器无法确定,只能假设最坏情况,于是放弃 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 章性能分析一节会再回到这个模式。

学习目标

阅读完本节,你应当能够:

  1. 区分假定形状、延迟形状、自动数组三种声明及其内存位置
  2. 说明为什么假定形状数组必须配显式接口
  3. 解释 contiguous 属性如何解锁 SIMD,以及它的代价是什么
  4. 为一个高性能过程选择正确的数组声明方式

本节要点回顾

  • 三种形状声明各有地盘:假定形状灵活、延迟形状上堆、自动数组上栈
  • 假定形状必须配显式接口:这是"过程进模块"工程铁律的技术原因
  • contiguous 是连续性承诺书:解锁 SIMD 的前提,代价是挡住不连续实参
  • 切片实参天然不连续:热路径上要么调整布局,要么 pack 后喂给内核
  • 性能调优先想布局再谈属性:数据结构合理,属性只是锦上添花
  • 本节的布局直觉直通第 6 章并行:分布式数组与 GPU 显存拷贝都建立在连续布局上

数组引擎加满油了,下一站是它的"变速箱"——模块与过程设计。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U