8.2 现代测试框架与性能调优


8.2 现代测试框架与性能调优

本节摘要:怎么证明数值代码"对",怎么量化它"快"?本节给出两个工程化答案:测试上,用容差断言(而不是等于)验证浮点结果,把断言、计数、汇总装进一个可自动运行的小测试框架;性能上,先用剖析工具定位瓶颈,再区分内存带宽受限与计算受限,对症下药。读完你能为任何一段 Fortran 代码建立"对错有据、快慢有数"的度量。

数值断言为什么不能用等于

if (result == expected) then ——这段代码在整数世界里没问题,在浮点世界里是定时炸弹。浮点运算的舍入误差导致同样算法在编译器优化级别不同时,末位可能差一个 ulp(unit in the last place)。测试浮点结果必须用容差:绝对容差适合量级已知的值,相对容差适合量级跨度的值,科学软件通常两者结合。

! 容差比较:绝对与相对容差结合,返回是否"足够接近" function approx_eq(a, b, rtol, atol) result(ok) use iso_fortran_env, only: real64 implicit none real(real64), intent(in) :: a, b, rtol, atol logical :: ok ok = abs(a - b) <= atol + rtol * max(abs(a), abs(b)) end function approx_eq

abs(a-b) <= atol + rtol*max(|a|,|b|) 是标准的近似相等判据:a、b 都接近 0 时用绝对容差,量级大时用相对容差。rtol 取 1e-10 到 1e-12 之间通常合适(双精度有效位 15 位,留 2–3 位余量),atol 按问题量纲设。

一个自带断言的小测试框架

Fortran 有第三方测试框架(如社区维护的测试工具),但手写一个微型框架并不难,而且能让你理解测试的本质:执行、断言、计数、汇总、按结果给退出码。

module test_util_mod use iso_fortran_env, only: real64, output_unit implicit none integer :: test_count = 0 integer :: fail_count = 0 contains subroutine check(cond, msg) logical, intent(in) :: cond character(*), intent(in) :: msg test_count = test_count + 1 if (cond) then write(output_unit, '(a)') 'PASS: ' // msg else fail_count = fail_count + 1 write(output_unit, '(a)') 'FAIL: ' // msg end if end subroutine check function approx_eq(a, b, rtol, atol) result(ok) real(real64), intent(in) :: a, b, rtol, atol logical :: ok ok = abs(a - b) <= atol + rtol * max(abs(a), abs(b)) end function approx_eq end module test_util_mod program test_solver use test_util_mod use iso_fortran_env, only: real64 implicit none call check(approx_eq(1.0_real64, 1.0_real64, 1.0e-10_real64, 1.0e-12_real64), 'trivial pass') ! 一个真实用例:axpy 结果验证 block real(real64) :: y(3) y = [1.0_real64, 2.0_real64, 3.0_real64] y = 2.0_real64 * y + 1.0_real64 call check(approx_eq(sum(y), 15.0_real64, 1.0e-10_real64, 1.0e-12_real64), 'axpy sum') end block write(*,*) 'tests:', test_count, 'failed:', fail_count if (fail_count > 0) error stop 1 end program test_solver

这个骨架可以直接扩成你的测试基建:check 收集结果,结束时按失败数给非零退出码,CI 脚本就能据此判定红绿。测试的粒度对准"函数"而非"整个程序":纯函数、elemental 过程、数组变换这类单元最好测,这也是第 4 章强调 pure 的又一回报。

把测试接进日常

测试的价值在反复跑。三条习惯:一、每改一次代码就全量跑一遍测试,回归错误当天暴露;二、测试用例要覆盖边界——空数组、单个元素、极大极小值、正好等于容差的临界;三、数值测试要固定"参考实现"与"参考数据",防止"测试跟着代码一起错"。fpm 自带 test 目录约定,fpm test 一条命令即可,配合 CI 脚本(第 1 章的工程化环境)能在每次提交自动验证。

性能剖析:先量再改

"感觉这循环慢"不是优化依据。剖析的流程是:先测基线(记录现在多快),再用工具定位热点(哪个过程占时间最多),改完再测对比。Linux 上用 gprof(编译加 -pg,运行后看调用图报告)或性能剖析器(perf 等)抓热点函数;Windows 与跨平台场景可用编译器自带的计时工具加手动计时点。关注两个指标:每个过程的花费占比(谁在烧时间)与调用次数(是不是被反复调了几百万次)。

program time_demo use iso_fortran_env, only: real64 implicit none integer :: i real(real64) :: t1, t2, s real(real64) :: x(1000000) x = 1.0_real64 call cpu_time(t1) do i = 1, 1000 s = sum(x * x) end do call cpu_time(t2) write(*,*) 'elapsed =', t2 - t1, 's' end program time_demo

cpu_time 是粗粒度计时,够用于对比相对快慢;要求更高用 system_clock 取墙钟时间。计时本身会引入开销,热点测量用剖析器,只有"改前改后对比"这种小样本测量才用手动计时。

内存带宽受限与计算受限

剖析之后要判断瓶颈类型。内存带宽受限:程序受限于数据搬运速度,特征是大数组反复遍历、每次元素只做简单运算——优化方向是减少数据量(单精度换双精度要慎重、pack 收拢稀疏数据、提高数据复用)、改善访问局部性(第 3 章的连续布局)。计算受限:程序受限于算术吞吐,特征是复杂浮点运算密集——优化方向是向量化(第 6 章)、算法降阶(少算一次乘加)、必要时交给 GPU。区分方法很直接:把一个耗时循环里的计算改成最小(比如只赋值),如果时间没明显下降,就是带宽受限——它在搬数据而不是算数据。

学习目标

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

  1. 解释数值断言为什么必须用容差,并写一个容差比较函数
  2. 写一个可自动运行的测试框架:断言、计数、汇总、退出码
  3. 说出剖析的基本流程与两个关注指标
  4. 区分内存带宽受限与计算受限,并各给出一个优化方向

本节要点回顾

  • 浮点断言用容差:绝对加相对容差的判据,别用等于
  • 微型测试框架四件套:执行、断言、计数、汇总,按失败数给退出码
  • 测试要覆盖边界与参考实现:回归当天暴露,参考数据固定不动
  • 先量再改:基线、剖析、对比三步走,别靠感觉优化
  • 瓶颈分两类:带宽受限少搬数据,计算受限提向量化与降阶
  • 测试与调优是工程化的两条腿:证明它对,再让它快

对错快慢都能量化了,最后一节看怎么把资产传下去——规范与迁移。


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