F#中的IEquatable,=运算符性能和结构相等

IEquatable in F#, = operator performance and structural equality

我想知道在哪些情况下 F# 中的相等性测试会导致装箱,以及是否存在覆盖 EqualsGetHashCode 并实现 IEquatable<> 比使用StructuralEqualityAttribute。如果是这样,是否可以在不降低 = 运算符性能的情况下完成?

对于包含一个整数的简单结构,我 运行 一个重复相同相等性检查 1M 次的循环。我使用...

为循环计时

据我了解,IEquatable<> 接口可以用作性能优化,以防止在检查相等性时装箱。这在 C# 中似乎很常见,但我很难在 F# 中找到它的提及。此外,F# 编译器在尝试覆盖给定类型的 = 运算符时会报错。

StructuralEquality 属性 documented in the MSDN 仅覆盖 EqualsGetHashCode。不过,它确实阻止了 IEquatable<> 的显式实现。但是,生成的类型与 IEquatable<MyType> 不兼容。这对我来说似乎不合逻辑,如果结构上等同的类型不实现 IEquatable<>?

在 F# 规范(3.0 规范中的 8.15.6.2)中有关于 = 性能的说明,但我不知道该怎么做:

Note: In practice, fast (but semantically equivalent) code is emitted for direct calls to (=), compare, and hash for all base types, and faster paths are used for comparing most arrays

之前给出的"base types"的定义对阅读这篇笔记似乎没什么用。这是指基本类型吗?

我很困惑。到底是怎么回事?如果类型可以用作集合键或用于频繁的相等性测试,那么正确的相等性实现会是什么样子?

以下是我根据有限的经验收集的内容:

However, the resulting type is incompatible with IEquatable<MyType>.

这是不正确的,生成的类型确实实现了 IEquatable<MyType>。您可以在 ILDasm 中验证。示例:

[<StructuralEquality;StructuralComparison>]    
type SomeType = {
    Value : int
}

let someTypeAsIEquatable = { Value = 3 } :> System.IEquatable<SomeType>
someTypeAsIEquatable.Equals({Value = 3}) |> ignore // calls Equals(SomeType) directly

也许您对 F# 不像 C# 那样进行隐式向上转换的方式感到困惑,所以如果您只是这样做:

{ Value = 3 }.Equals({Value = 4})

这实际上会调用 Equals(obj) 而不是接口成员,这与 C# 的预期相反。

I'm wondering in which cases equality tests in F# cause boxing

一个常见且令人烦恼的情况是对于定义在例如中的任何结构。 C# 和实现 IEquatable<T>,例如:

public struct Vector2f : IEquatable<Vector2f>

或类似地,在 F# 中定义的具有自定义实现 IEquatable<T> 的任何结构,例如:

[<Struct;CustomEquality;NoComparison>]
type MyVal =
    val X : int
    new(x) = { X = x }
    override this.Equals(yobj) =
        match yobj with
        | :? MyVal as y -> y.X = this.X
        | _ -> false
    interface System.IEquatable<MyVal> with
        member this.Equals(other) =
            other.X = this.X

将此结构的两个实例与 = 运算符进行比较实际上调用 Equals(obj) 而不是 Equals(MyVal),导致装箱发生在 两个被比较的值上 (然后铸造和拆箱)。注意:我将此报告为 bug on the Visualfsharp Github。 (2022 年更新:尽管社区做出了英勇的努力,但这个问题从未得到解决)。

如果您认为明确转换为 IEquatable<T> 会有所帮助,那么它会有所帮助,但这本身就是一个装箱操作。但至少你可以通过这种方式节省自己的两个拳击之一。

I'm confused. What is going on? What would a proper equality implementation look like, if the type might be used as a collection key or in a frequent equality test?

我和你一样困惑。 F# 似乎非常支持 GC。即使是默认行为:

[<Struct>]
type MyVal =
    val X : int
    new(x) = { X = x }

for i in 0 .. 1000000 do
      (MyVal(i) = MyVal(i + 1)) |> ignore;;
Réel : 00:00:00.008, Processeur : 00:00:00.015, GC gén0: 4, gén1: 1, gén2: 0

仍然会导致装箱和过度的 GC 压力!请参阅下面的解决方法。

如果必须将类型用作键,例如一本字典?好吧,如果它是 System.Collections.Generics.Dictionary 你没问题,那不使用 F# 相等运算符。但是在 F# 中定义的任何使用此运算符的集合显然 运行 会出现装箱问题。

I'm wondering (...) whether there are cases in which overriding Equals and GetHashCode and implementing IEquatable<> is preferable to using the StructuralEqualityAttribute.

重点是定义您自己的自定义相等性,在这种情况下,您使用 CustomEqualityAttribute 而不是 StructuralEqualityAttribute

If so, can it be done without reducing the performance of the = operator?

更新: 我建议避免默认 (=) 并直接使用 IEquatable(T).Equals。您可以为此定义一个内联运算符,或者您甚至可以根据它重新定义 (=)。这对 F# 中的几乎所有类型都是正确的,对于其余类型,它不会编译,因此您不会 运行 陷入细微的错误。 (2022 年更新:我不确定这是个好主意。)

原文: 从 F# 4.0 开始,您可以执行以下操作 (thanks latkin):

[<Struct>]
type MyVal =
    val X : int
    new(x) = { X = x }
    static member op_Equality(this : MyVal, other : MyVal) =
        this.X = other.X

module NonStructural =
    open NonStructuralComparison
    let test () =
        for i in 0 .. 10000000 do
              (MyVal(i) = MyVal(i + 1)) |> ignore

// Real: 00:00:00.003, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
NonStructural.test()

NonStructuralComparison 模块使用仅调用 op_Equality 的版本覆盖默认 =。我会向结构添加 NoEqualityNoComparison 属性,以确保您不会意外使用性能不佳的默认值 =.