F#流式计算场景避免上下文复制开销用可变结构体还是类?
问题描述
假设我们有如下递归函数:
let rec doSomething dataList b c d e f g h i = // 遍历dataList并用更新后的参数递归
为了让代码更简洁,我们可以做如下优化:
type Context = { b:..; c:..; ... } let rec doSomething dataList context = // 用更新后的上下文对象递归
但该实现实际运行性能更差,因为每次递归都要重新创建上下文对象,在计算逻辑简单的紧凑循环中,这部分开销十分明显。
我已经用该模式实现了多个数学计算循环,现在遇到了新需求:需要将相同逻辑应用在流式输入数据上,随着数据输入逐块分步执行计算。
因此我计划将函数改写成如下形式:
let doOneStepOfSomething oneData context : result * Context = // 处理单条数据,返回下一轮迭代用的上下文对象
但我仍然面临对象重复创建的开销问题,同时还需要保留多份不同的上下文实例。
请问这是适合使用实例上下文变量可变的class的有效场景吗?还是说为Context定义带可变字段的可变类型更合理?
技术解答
两种方案都可以解决不可变对象重复创建的开销问题,具体选型可以根据你的使用场景判断:
- 如果你大部分上下文操作都在单步执行的逻辑内部完成,对外暴露的上下文实例不需要做额外的不可变保证,*带可变字段的结构体(值类型)*是性能最优的选择,比引用类型的class或者可变记录开销更低:
- 需要多份上下文副本时,显式调用结构体复制即可,开销远低于堆分配的不可变记录
- 示例写法:
[<Struct>] type MutableContext = val mutable b: int val mutable c: float // 其余字段定义 new(b, c) = { b = b; c = c } - 如果你需要上下文对外提供部分不可变接口,或者需要在上下文里附带一些方法逻辑,使用带私有可变字段的class更合适:
- 你可以只对外暴露字段的getter,只允许
doOneStepOfSomething内部修改字段,避免外部意外修改上下文状态 - 需要多份实例时实现
Clone方法即可,灵活控制复制粒度
- 你可以只对外暴露字段的getter,只允许
- 带可变字段的记录类型(用
mutable关键字修饰字段)适合中间场景:写法比class简洁,同时保持了记录类型的语义,性能和普通class基本持平,不需要额外写构造函数的模板代码:type MutableContext = { mutable b: int; mutable c: float; (*其余字段定义*) }
额外优化建议:如果你的计算逻辑是CPU密集的紧凑循环,可以考虑给doOneStepOfSomething加上[<MethodImpl(MethodImplOptions.AggressiveInlining)>]特性,减少函数调用开销,进一步提升性能。
内容的提问来源于stack exchange,提问作者Thomas
相关产品推荐
相关产品推荐

