3.2 数组构造、重构与掩码运算


3.2 数组构造、重构与掩码运算

本节摘要:上一节解决"怎么表达数组计算",本节解决"数据形态怎么流动"。数组构造器与隐含 do 一行生成数据序列;reshape 改视图不搬数据;pack 与 unpack 在稠密与稀疏之间搬运;merge 做无分支选择。这套工具组合起来,就是一条让数据以最小阻力流向计算单元的处理管线。

数据形态为什么需要流动

物理仿真里,数据很少从头到尾保持同一个形状:文件里读进来的是一维字节流,计算需要三维场;有效计算区域往往只占网格的一部分,全尺寸数组里塞着大量不参与计算的格子。如果每次形态变换都靠手写循环,代码既长又容易把索引搞错。Fortran 提供一组"形态搬运"的内置工具,让数据流动像管道一样清晰。

构造器与隐含 do:一行生成数据序列

数组构造器把一组值合并成一个一维数组。配合隐含 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 把一个线性序列按指定形状重新解释。关键认知:它更新的是数组描述符,不是复制数据——一维数据流"弯曲"成矩阵,物理内存不变,逻辑视图变了。列优先规则下,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:稠密与稀疏之间的搬运

有效计算区域只占小部分时,继续在全尺寸数组上循环是浪费。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 与 where 的边界

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 构造 带块逻辑的条件赋值

学习目标

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

  1. 用数组构造器与隐含 do 生成序列与网格
  2. 解释 reshape 为何是零拷贝操作,并说明列优先下的填充顺序
  3. 用 pack/unpack 完成按掩码压缩与回填
  4. 用 merge 写无分支的条件选择,并说明其向量化含义

本节要点回顾

  • 构造器配隐含 do 一行生成序列[(expr, i=1,n)] 比手写循环简洁且可内联
  • reshape 是零拷贝改视图:物理内存不变,逻辑形状变;列优先决定填充顺序
  • pack/unpack 收拢有效计算:稀疏处理变稠密向量,复杂度从全数组降到子集
  • pack 的代价是数据移动:有效占比接近 1 时别用
  • merge 是无分支选择:适合表达式内的二选一,where 适合块级逻辑
  • 形态搬运工具是管线基础:读文件→reshape 成场→where 屏蔽→pack 精算→unpack 回填,一条顺畅的数据流

语法与形态都齐了,最后一节解决性能契约:怎么声明数组才能让编译器放开手脚。


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