为什么在 F# 中使用引用大值的字段创建记录这么慢?
Why is it so slow to create records with a field that references a big value in F#?
在下面作为 .fsx 脚本执行的代码中,最后一行大约需要 30 秒才能完成。我假设由于记录是引用类型,所以最后一行只创建带有引用(不可变)大值的字段的记录,因此它应该非常快。为什么它很慢,我该如何解决?
type BigType = { Big: Set<int> }
type CollectionType = { BigVal: BigType; SmallVal: int }
let b = { Big = set [ 0 .. 999999 ] }
let mySet = set [ 0 .. 50 ]
#time
mySet |> Set.map (fun x -> { BigVal = b; SmallVal = x })
谢谢。
欢迎来到 F# 社区。
我猜每条新记录都在复制 b
,尽管正如您所说,默认情况下记录是引用类型,但我不确定为什么会这样。
这种方法并不快:
let y = { BigVal = b; SmallVal = 0 }
mySet |> Set.map (fun x -> { y with SmallVal = x })
改用 class 会快得多:
type BigType = { Big: Set<int> }
let b = { Big = set [ 0 .. 999999 ] }
type CollectionType(smallVal: int) =
interface System.IComparable with
member __.CompareTo other =
compare __.SmallVal (other :?> CollectionType).SmallVal
member __.BigVal = b
member __.SmallVal = smallVal
let mySet = set [ 0 .. 50 ]
#time
mySet |> Set.map (fun x -> CollectionType(x))
这不是一个完整的解决方案,因为有警告 FS0343: The type 'CollectionType' implements 'System.IComparable' explicitly but provides no corresponding override for 'Object.Equals'. An implementation of 'Object.Equals' has been automatically provided, implemented via 'System.IComparable'. Consider implementing the override 'Object.Equals' explicitly
。
如果先转换为数组,则完全不需要时间:
mySet |> Set.toList |> List.map (fun x -> { BigVal = b; SmallVal = x })
所以创建记录所花费的时间是微不足道的。它慢的原因是记录在一个集合中。作为实现的一部分,集合需要将它们的项目相互比较,以确保没有重复值。 F# 通过比较内容来比较记录的结构。因此,正在比较的这些记录包含非常大的集合,需要很长时间才能进行比较。它们实际上是不同的记录,但在内存中的对象方面是相同的集合,但 F# 记录比较不知道,也不检查。
原因是 Set
是作为搜索树实现的,因此为了将一个项目插入到集合中,它会与一些现有项目进行比较。所以确实只创建了小记录,但是正在比较整套。
如果不知道您正在解决的确切问题,很难说出解决问题的最佳方法。但是,如果要求每个 CollectionType
值具有不同的 SmallVal
,那么您可以按照 Scott 的建议进行操作,并实现仅查看 SmallVal
的自定义比较。不过你不需要 class,你可以用一条记录来做到这一点:
(* For records, you need to put CustomComparison in addition to implementing IComparable.
And CustomComparison requires CustomEquality and its corresponding overrides. *)
[<CustomComparison; CustomEquality>]
type CollectionType =
{ BigVal: BigType; SmallVal: int }
interface System.IComparable with
member this.CompareTo(that) =
match that with
| :? CollectionType as that -> compare this.SmallVal that.SmallVal
| _ -> -1
override this.Equals(that) =
match that with
| :? CollectionType as that -> this.SmallVal = that.SmallVal
| _ -> false
override this.GetHashCode() = this.SmallVal
这里要注意的一件事是,您在 type CollectionType = { BigVal: BigType; SmallVal: int }
中定义字段的顺序会有所不同。如果你尝试:
type BigType = { Big: Set<int> }
type CollectionType = { SmallVal: int; BigVal: BigType; }
let b = { Big = set [ 0 .. 999999 ] }
let mySet = set [ 0 .. 50 ]
#time
mySet |> Set.map (fun x -> { BigVal = b; SmallVal = x })
取而代之的是实际时间:00:00:34.385 到实际时间:00:00:00.002.
注意:我最初担心这种行为不可靠,可能会在没有警告的情况下发生变化;然而,正如 nasosev 所发现的,此行为在 F# language specification 文档 4.1 版的第 8.15.3 节中有所描述。
在下面作为 .fsx 脚本执行的代码中,最后一行大约需要 30 秒才能完成。我假设由于记录是引用类型,所以最后一行只创建带有引用(不可变)大值的字段的记录,因此它应该非常快。为什么它很慢,我该如何解决?
type BigType = { Big: Set<int> }
type CollectionType = { BigVal: BigType; SmallVal: int }
let b = { Big = set [ 0 .. 999999 ] }
let mySet = set [ 0 .. 50 ]
#time
mySet |> Set.map (fun x -> { BigVal = b; SmallVal = x })
谢谢。
欢迎来到 F# 社区。
我猜每条新记录都在复制 b
,尽管正如您所说,默认情况下记录是引用类型,但我不确定为什么会这样。
这种方法并不快:
let y = { BigVal = b; SmallVal = 0 }
mySet |> Set.map (fun x -> { y with SmallVal = x })
改用 class 会快得多:
type BigType = { Big: Set<int> }
let b = { Big = set [ 0 .. 999999 ] }
type CollectionType(smallVal: int) =
interface System.IComparable with
member __.CompareTo other =
compare __.SmallVal (other :?> CollectionType).SmallVal
member __.BigVal = b
member __.SmallVal = smallVal
let mySet = set [ 0 .. 50 ]
#time
mySet |> Set.map (fun x -> CollectionType(x))
这不是一个完整的解决方案,因为有警告 FS0343: The type 'CollectionType' implements 'System.IComparable' explicitly but provides no corresponding override for 'Object.Equals'. An implementation of 'Object.Equals' has been automatically provided, implemented via 'System.IComparable'. Consider implementing the override 'Object.Equals' explicitly
。
如果先转换为数组,则完全不需要时间:
mySet |> Set.toList |> List.map (fun x -> { BigVal = b; SmallVal = x })
所以创建记录所花费的时间是微不足道的。它慢的原因是记录在一个集合中。作为实现的一部分,集合需要将它们的项目相互比较,以确保没有重复值。 F# 通过比较内容来比较记录的结构。因此,正在比较的这些记录包含非常大的集合,需要很长时间才能进行比较。它们实际上是不同的记录,但在内存中的对象方面是相同的集合,但 F# 记录比较不知道,也不检查。
原因是 Set
是作为搜索树实现的,因此为了将一个项目插入到集合中,它会与一些现有项目进行比较。所以确实只创建了小记录,但是正在比较整套。
如果不知道您正在解决的确切问题,很难说出解决问题的最佳方法。但是,如果要求每个 CollectionType
值具有不同的 SmallVal
,那么您可以按照 Scott 的建议进行操作,并实现仅查看 SmallVal
的自定义比较。不过你不需要 class,你可以用一条记录来做到这一点:
(* For records, you need to put CustomComparison in addition to implementing IComparable.
And CustomComparison requires CustomEquality and its corresponding overrides. *)
[<CustomComparison; CustomEquality>]
type CollectionType =
{ BigVal: BigType; SmallVal: int }
interface System.IComparable with
member this.CompareTo(that) =
match that with
| :? CollectionType as that -> compare this.SmallVal that.SmallVal
| _ -> -1
override this.Equals(that) =
match that with
| :? CollectionType as that -> this.SmallVal = that.SmallVal
| _ -> false
override this.GetHashCode() = this.SmallVal
这里要注意的一件事是,您在 type CollectionType = { BigVal: BigType; SmallVal: int }
中定义字段的顺序会有所不同。如果你尝试:
type BigType = { Big: Set<int> }
type CollectionType = { SmallVal: int; BigVal: BigType; }
let b = { Big = set [ 0 .. 999999 ] }
let mySet = set [ 0 .. 50 ]
#time
mySet |> Set.map (fun x -> { BigVal = b; SmallVal = x })
取而代之的是实际时间:00:00:34.385 到实际时间:00:00:00.002.
注意:我最初担心这种行为不可靠,可能会在没有警告的情况下发生变化;然而,正如 nasosev 所发现的,此行为在 F# language specification 文档 4.1 版的第 8.15.3 节中有所描述。