F# 性能类型(fsharp-performance-types)是一个面向 F# 代码性能优化的技能,重点解决集合与类型选择带来的运行时和内存分配问题。它适用于 F# 代码运行缓慢、分配过多内存,或被审查者、任务明确标记为热点路径的场景。技能强调:只有在测量证明某处是热点,或被明确指认为热点时才使用;非热点路径应优先保持可读写法。
主要能力与规则:
集合使用:使用 for、List.fold 或模式匹配遍历 F# 列表,不要用索引,也不要为循环而复制成数组;不要多次枚举 seq,应物化一次或单遍计算全部结果;不要在循环中使用 @ 或 List.append,可向头部 cons 后调用一次 List.rev,或填充 ResizeArray;热点循环中更新表时使用局部 Dictionary 或 HashSet,Map.add 每次更新会分配新树路径,Map.tryFind 每次命中分配 Some,若契约需要不可变结果可在最后返回,Dictionary 使用 .NET 相等性,键需要 F# 结构相等时(如数组键)传入 HashIdentity.Structural;保持契约定义的顺序,List.sort、Array.sort、Map、Set 按序数排序字符串,Array.Sort、List<T>.Sort、SortedDictionary 默认使用当前区域性,除非传入 StringComparer.Ordinal;构建一次、多次读取的查找表使用 FrozenDictionary 或 FrozenSet。
单遍处理:将大输入上的 filter |> map |> length 链替换为一次 fold 或一次循环;在循环中跟踪前一个值,替代 List.pairwise 或 Seq.windowed(它们为每个元素分配元组或数组);仅在每个元素工作量远大于任务开销时使用 Array.Parallel,并进行测量。
值类型:热点循环中返回 voption 而非 option,并为部分活动模式标记 [<return: Struct>];非内联函数在热点循环中返回的元组和 Dictionary 键使用 struct (a, b),引用元组键每次查找都会分配;不要将 struct 元组用作 sortBy 键,F# 泛型比较会在每次比较时装箱,应使用 sortWith 配合 String.CompareOrdinal 或逐字段 compare;小型记录或含少量基元字段的联合使用 [<Struct>],大型值、以 obj 或接口存储的值、通过副本修改的值不要使用 struct。
内存分配:热点循环中不要为每个元素分配 Some、元组、闭包、列表单元、子字符串或装箱值;不要在热点循环中调用 sprintf,改用 StringBuilder.Append 或 String.Concat 配合 ToString(format, CultureInfo.InvariantCulture)(sprintf 和 %.3f 使用固定区域性,ToString("F3") 和 {x:F3} 使用当前区域性,逗号小数分隔符机器上输出会变);不要用 + 在循环或 fold 中构建字符串,使用一个 StringBuilder;使用 Array.sumBy 等内联集合函数替代 Array.filter |> Array.length,仅当测量显示闭包成本时才将自写小型高阶辅助函数标记为 inline。
测量方法:在 Release 构建中围绕一次预热调用使用 GC.GetAllocatedBytesForCurrentThread() 测量分配字节数;时间取多次运行的中位数;在与生产相同的输入规模上对比前后,报告数字而非断言。
使用步骤:1) 找到热点函数,构建生产规模输入并测量每个函数一次调用的分配字节数与中位时间;2) 列出每次额外遍历、重复 seq 枚举、循环中的 @ 或索引、循环中的 Map/Set 更新、每个元素的分配;3) 按上述规则重写,保持公共类型与签名不变;4) 在相同输入、逗号小数分隔符区域性(如 de-DE)、仅大小写不同的字符串以及单个 -0.0 值下检查输出与之前一致;5) 用同一测量工具再次测量,报告前后数字,若某项未改变数字则恢复为可读形式。
验证:从技能根目录运行 pwsh ./scripts/run.ps1,脚本在 dotnet fsi --optimize+ 下运行,使用 GC.GetAllocatedBytesForCurrentThread()、运行计数或时间比来测量技能关于分配、顺序和区域性的声明,测量结果与声明不符时失败;FrozenDictionary、ImmutableArray 和 Array.Parallel 的建议来自 .NET 文档,脚本不测量。
适用场景:F# 代码性能调优、热点路径集合与类型选择、内存分配优化、代码审查反馈处理。限制:仅用于测量证明的热点或被指认的热点,非热点路径应保持可读写法;部分建议未由脚本测量验证。