本节摘要:上一节解决"怎么表达数组计算",本节解决"数据形态怎么流动"。数组构造器与隐含 do 一行生成数据序列;reshape 改视图不搬数据;pack 与 unpack 在稠密与稀疏之间搬运;merge 做无分支选择。这套工具组合起来,就是一条让数据以最小阻力流向计算单元的处理管线。
物理仿真里,数据很少从头到尾保持同一个形状:文件里读进来的是一维字节流,计算需要三维场;有效计算区域往往只占网格的一部分,全尺寸数组里塞着大量不参与计算的格子。如果每次形态变换都靠手写循环,代码既长又容易把索引搞错。Fortran 提供一组"形态搬运"的内置工具,让数据流动像管道一样清晰。
数组构造器把一组值合并成一个一维数组。配合隐含 do,可以在一行里生成带规律的序列。
program constructor_demo use iso_fortran_env, only: real64 implicit none real(real64) :: r(10), g(5) integer :: i r = [1.0_real64, 2.0_real64, 3.0_real64] ! 直接列举 r = [(real(i*i, real64), i = 1, 10)] ! 隐含 do:前 10 个平方数 g = [ (real(i, real64) * 0.1_real64, i = 1, 5) ] ! 等差序列 write(*,*) 'r =', r write(*,*) 'g =', g end program constructor_demo
[(expr, i = 1, n)] 是隐含 do 构造器,比手写循环填数简洁得多,也让编译器更容易内联展开。注意构造器总是一维的;想得到多维数组,配合 reshape 使用。
reshape 把一个线性序列按指定形状重新解释。关键认知:它更新的是数组描述符,不是复制数据——一维数据流"弯曲"成矩阵,物理内存不变,逻辑视图变了。列优先规则下,reshape([1,2,3,4,5,6], [2,3]) 的填充顺序是先填满第一列:结果为 (1,2)、(3,4)、(5,6) 三列。
program reshape_demo use iso_fortran_env, only: real64 implicit none real(real64) :: flat(6), mat(2, 3) flat = [1.0_real64, 2.0_real64, 3.0_real64, 4.0_real64, 5.0_real64, 6.0_real64] mat = reshape(flat, [2, 3]) ! 1、2 填第一列,3、4 填第二列,5、6 填第三列 write(*,*) mat(:, 1) ! 1 2 write(*,*) mat(:, 2) ! 3 4 end program reshape_demo
reshape 的零拷贝特性在切换场视角时特别值钱:三维网格要取某个截面,与其复制出一块新数组,不如用 reshape 或切片换一个视图。警惕的是"视图变了、邻接没变":重塑出的数组在内存里仍按原顺序排列,若你的算法假设了新布局的连续性,缓存表现可能和想象不同。
有效计算区域只占小部分时,继续在全尺寸数组上循环是浪费。pack 按掩码把元素压缩成一维连续向量,unpack 按同一掩码回填——这一压一回,计算复杂度从全数组降到有效子集。
program pack_demo use iso_fortran_env, only: real64 implicit none real(real64) :: field(10), active(10) logical :: mask(10) real(real64), allocatable :: dense(:) integer :: n field = [1.0_real64, 2.0_real64, 3.0_real64, 4.0_real64, 5.0_real64, & 6.0_real64, 7.0_real64, 8.0_real64, 9.0_real64, 10.0_real64] mask = field > 5.0_real64 ! 只关心大于 5 的格子 dense = pack(field, mask) ! 压缩成 [6,7,8,9,10] active = 0.0_real64 active = unpack(dense, mask, active) ! 按掩码回填回原形状 write(*,*) 'dense =', dense write(*,*) 'count =', count(mask) end program pack_demo
pack 生成连续向量后,可以放心喂给 BLAS 类例程或做向量化处理;在 MPI 通信里,pack 还能把分散的边界数据收进连续缓冲区,减少通信次数。代价是压缩与回填本身要移动数据——如果有效数据占比接近 100%,这套操作就是纯开销,别用。判断依据:有效子集能否显著缩小工作集,让它装进 L2/L3 缓存。
merge 是函数式的无分支选择:result = merge(tval, fval, mask) 对每个位置按掩码取值。它写在表达式内部,不像 where 是语句块。
program merge_demo use iso_fortran_env, only: real64 implicit none real(real64) :: x(5), absx(5) x = [-2.0_real64, -1.0_real64, 0.0_real64, 1.0_real64, 2.0_real64] absx = merge(x, -x, x >= 0.0_real64) ! 无分支求绝对值 write(*,*) 'abs =', absx end program merge_demo
merge 适合简单的二选一;where/elsewhere 适合带块级逻辑的多步赋值。两者都在向编译器表达"这里没有控制流跳转,可以向量化"。区别是粒度和可读性:一条表达式用 merge,一组条件赋值用 where。
| 工具 | 形态 | 适用 |
|---|---|---|
| 构造器 + 隐含 do | 表达式 | 生成有规律的序列 |
| reshape | 函数 | 改视图,不搬数据 |
| pack/unpack | 函数对 | 稠密稀疏互转,收拢有效计算 |
| merge | 函数 | 表达式内的无分支二选一 |
| where/elsewhere | 构造 | 带块逻辑的条件赋值 |
阅读完本节,你应当能够:
[(expr, i=1,n)] 比手写循环简洁且可内联语法与形态都齐了,最后一节解决性能契约:怎么声明数组才能让编译器放开手脚。