С# - метод 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
не является ключевым словом языка, а средством выполнения... Компилятор полностью не знает об этом методе. Таким образом, здесь возникает смысл использовать отражение.
Ответы
Ответ 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/