IEquatable в F #, = производительность оператора и структурное равенство

Мне интересно, в каких случаях тесты равенства в F # вызывают бокс, и есть ли случаи, когда переопределение Equals и GetHashCode и реализация IEquatable<> предпочтительнее использовать StructuralEqualityAttribute. Если да, можно ли это сделать без снижения производительности оператора =?

Для простых структур, содержащих одно целое число, я запустил цикл, который повторяет ту же проверку равенства 1M раз. Я назначил цикл, используя...

  • = с пользовательским (тип и значение) равенство: около 110 мс
  • = со структурным равенством: от 20 мс до 25 мс
  • Пользовательский == оператор, который перенаправляет на IEquatable: от 1 мс до 3 мс
  • Пользовательский == оператор, который сравнивает значения напрямую: 0ms (стирается оптимизатором)

Из того, что я понимаю, интерфейс IEquatable<> может использоваться как оптимизация производительности для предотвращения бокса при проверке равенства. Это похоже на С#, но я вряд ли могу найти упоминания об этом в F #. Кроме того, компилятор F # жалуется при попытке переопределить оператор = для заданного типа.

Атрибут StructuralEquality задокументирован в MSDN для переопределения только Equals и GetHashCode. Тем не менее, это предотвращает явную реализацию IEquatable<>. Однако полученный тип несовместим с IEquatable<MyType>. Мне это не кажется логичным, если структурно приравненный тип не реализует IEquatable<>?

Есть заметка о производительности = в спецификации F # (8.15.6.2 в спецификации 3.0), но я не знаю, что с ней делать:

Примечание. На практике быстрый (но семантически эквивалентный) код испускается для прямых вызовов (=), сравнения, и хэш для всех базовых типов, а более быстрые пути используются для сравнения большинства массивов

Определение "базовых типов", приведенное выше, не представляется полезным читать эту заметку. Это относится к основным типам?

Я в замешательстве. Что происходит? Как выглядит правильная реализация равенства, если тип может использоваться в качестве ключа коллекции или в частых тестах на равенство?

Ответ 1

Вот то, что я собрал, основываясь на моем ограниченном опыте:

Однако результирующий тип несовместим с 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 # не делает неявные восходящие потоки, такие как С#, поэтому, если вам нужно просто:

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

Это фактически вызовет Equals (obj) вместо члена интерфейса, что противоречит ожиданиям, исходящим из С#.

Мне интересно, в каких случаях тесты равенства в F # вызывают бокс

Один общий и неприятный случай для любой структуры, определенной, например, С# и реализации 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), что приводит к возникновению бокса в , причем оба значения сравниваются (а затем литье и распаковка). Примечание. Я сообщил об этом как об ошибке в Visualfsharp Github, и, очевидно, этот случай должен быть исправлен раньше, чем позже.

И если вы считаете, что приведение к IEquatable<T> явно поможет, ну это будет, но это операция по боксу сама по себе. Но по крайней мере вы можете спасти себе один из двух боксов таким образом.

Я в замешательстве. Что происходит? Какое бы правильное равенство реализация выглядит так, если тип может использоваться как коллекция ключа или в частых тестах на равенство?

Я так же смущен, как и ты. F # кажется очень GC-happy (неважно, что это GCs Tuples и не поддерживает записи структуры или DU). Даже поведение по умолчанию:

[<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

Все еще вызывает бокс и чрезмерное давление в ГК! Ниже приведено обходное решение.

Что делать, если тип должен использоваться в качестве ключа, например. словарь? Ну, если он System.Collections.Generics.Dictionary, вы в порядке, это не использует оператор равенства F #. Но любая коллекция, определенная в F #, которая использует этот оператор, будет явно сталкиваться с проблемами бокса.

Мне интересно (...) есть ли случаи, когда переопределение равен и GetHashCode и реализация IEquatable < > предпочтительнее использовать атрибут StructuralEqualityAttribute.

Суть заключается в том, чтобы определить ваше собственное пользовательское равенство, и в этом случае вы используете CustomEqualityAttribute вместо StructuralEqualityAttribute.

Если да, можно ли это сделать, не уменьшая производительность оператора =?

Обновление: Я предлагаю избегать использования по умолчанию (=) и напрямую использовать IEquatable (T).Equals. Вы можете определить для этого встроенный оператор, или вы можете даже переопределить (=) в терминах этого. Это делает практически все типы в F #, а для остальных он не будет компилироваться, поэтому вы не столкнетесь с небольшими ошибками. Я подробно описываю этот подход здесь.

Оригинал: Начиная с F # 4.0, вы можете сделать следующее (спасибо 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. Я бы добавил атрибуты NoEquality и NoComparison в структуру, чтобы убедиться, что вы случайно не используете невысокую производительность по умолчанию =.