Подтвердить что ты не робот

С# - метод Value Type Equals - почему компилятор использует отражение?

Я просто наткнулся на что-то довольно странное: когда вы используете метод Equals() для типа значения (и если этот метод не был переоценен, конечно), вы получаете что-то очень очень медленное - поля сравниваются к одному, используя отражение! Как в:

public struct MyStruct{
   int i;
}

   (...)

   MyStruct s, t;
   s.i = 0;
   t.i = 1;
   if ( s.Equals( t ))   /*  s.i will be compared to t.i via reflection here. */
      (...)

Мой вопрос: почему компилятор С# не генерирует простой метод сравнения типов значений? Что-то вроде (в определении MyStruct):

   public override bool Equals( Object o ){
      if ( this.i == o.i )
         return true;
      else
         return false;
   }

Компилятор знает, каковы поля MyStruct во время компиляции, почему он ожидает, пока среда выполнения перечислит поля MyStruct?

Очень странно для меня.

Спасибо:)

ADDED: Извините, я просто понимаю, что, конечно, Equals не является ключевым словом языка, а средством выполнения... Компилятор полностью не знает об этом методе. Таким образом, здесь возникает смысл использовать отражение.

4b9b3361

Ответ 1

Ниже представлен декомпилированный метод ValueType.Equals из mscorlib:

public override bool Equals(object obj)
{
    if (obj == null)
    {
        return false;
    }
    RuntimeType type = (RuntimeType) base.GetType();
    RuntimeType type2 = (RuntimeType) obj.GetType();
    if (type2 != type)
    {
        return false;
    }
    object a = this;
    if (CanCompareBits(this))
    {
        return FastEqualsCheck(a, obj);
    }
    FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
    for (int i = 0; i < fields.Length; i++)
    {
        object obj3 = ((RtFieldInfo) fields[i]).InternalGetValue(a, false);
        object obj4 = ((RtFieldInfo) fields[i]).InternalGetValue(obj, false);
        if (obj3 == null)
        {
            if (obj4 != null)
            {
                return false;
            }
        }
        else if (!obj3.Equals(obj4))
        {
            return false;
        }
    }
    return true;
}

По возможности, будет проведено побитное сравнение (обратите внимание на CanCompareBits и FastEqualsCheck, оба из которых определены как InternalCall. JIT предположительно вводит соответствующий код здесь. Что касается того, почему это так медленно, я не мог " скажите вам.

Ответ 2

Он не использует отражение, когда ему это не нужно. Он просто сравнивает значения по биту в случае struct, если он может это сделать. Однако, если какой-либо из членов struct (или членов членов, любых потомков) переопределяет object.Equals и предоставляет свою собственную реализацию, очевидно, что он не может полагаться на побитовое сравнение для вычисления возвращаемого значения.

Причина, по которой она замедляется, заключается в том, что параметр Equals имеет тип object, а типы значений должны быть помещены в бокс, чтобы обрабатываться как object. Бокс включает выделение памяти в куче и памяти, копируя тип значения в это место.

В ручном режиме вы можете перегрузить метод Equals, который принимает ваш собственный параметр struct как запрещающий бокс:

public bool Equals(MyStruct obj) {
     return obj.i == i;
}

Ответ 3

Идея генерируемой компилятором функции оправдана.

Размышляя о последствиях, я думаю, что команда разработчиков языка сделала все правильно. Сопоставимые методы, известные из С++, трудно понять для новичков. Давайте посмотрим, что произойдет в С# с автогенерированными struct.Equals:

Как и сейчас, концепция .Equals() проста:

  • Каждая структура наследует Equals из ValueType.
  • При переопределении применяется специальный метод Equals.

Если компилятор всегда создавал метод Equals, мы могли бы иметь:

  • Каждая структура наследует Equals из Object. (ValueType больше не будет реализовывать свою собственную версию)
  • Object.Equals теперь всегда (!) переопределяется либо сгенерированным компилятором методом Equals, либо с помощью реализации пользователя

Теперь наша структура имеет автогенерированный метод переопределения, который читатель кода не видит! Итак, как вы знаете, что базовый метод Object.Equals не применяется к вашей структуре? Изучая все случаи автоматических методов генерирования компилятора. И это точно одно из нагрузок, изучающих С++.

Будет считаться правильным решением оставить эффективную структуру Equals для пользователя и поддерживать простые концепции, требуя стандартного метода Equals по умолчанию.

Тем не менее, критические структуры производительности должны переопределять Equals. В приведенном ниже коде показано

3606 vs 53 миллисекунды, измеренные на .Net 4.5.1

Это увеличение производительности, конечно, связано с тем, что вы избегаете виртуальных Equals, но в любом случае, так что если виртуальный Object.Equals будет называться, коэффициент усиления будет намного ниже. Критические случаи производительности не будут вызывать Object.Equals, поэтому коэффициент усиления здесь будет применяться.

using System;
using System.Diagnostics;

struct A
{
    public int X;
    public int Y;
}

struct B : IEquatable<B>
{
    public bool Equals(B other)
    {
        return this.X == other.X && this.Y == other.Y;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        return obj is B && Equals((B)obj);
    }

    public int X;
    public int Y;
}


class Program
{
    static void Main(string[] args)
    {
        var N = 100000000;

        A a = new A();
        a.X = 73;
        a.Y = 42;
        A aa = new A();
        a.X = 173;
        a.Y = 142;

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < N; i++)
        {
            if (a.Equals(aa))
            {
                Console.WriteLine("never ever");
            }
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds);

        B b = new B();
        b.X = 73;
        b.Y = 42;
        B bb = new B();
        b.X = 173;
        b.Y = 142;

        sw = Stopwatch.StartNew();
        for (int i = 0; i < N; i++)
        {
            if (b.Equals(bb))
            {
                Console.WriteLine("never ever");
            }
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds);
    }
}

см. также http://blog.martindoms.com/2011/01/03/c-tip-override-equals-on-value-types-for-better-performance/