C# - 值类型等于方法 - 为什么编译器使用反射?

时间:2009-06-17 20:35:52

标签: c# compiler-construction struct

我只是遇到了一些非常奇怪的东西:当你在值类型上使用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. */
      (...)

我的问题:为什么C#编译器不生成比较值类型的简单方法?像(在MyStruct的定义中):

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

编译器在编译时知道MyStruct的字段是什么,为什么它要等到运行时才能枚举MyStruct字段?

对我来说很奇怪。

谢谢:)

ADDED :很抱歉,我只是意识到,Equals当然不是语言关键字,而是运行时方法......编译器完全不知道这种方法。所以在这里使用反射是有意义的。

3 个答案:

答案 0 :(得分:10)

当它不需要时,它不使用反射。它只是在struct的情况下逐位比较值,如果它可以这样做的话。但是,如果任何struct成员(或成员的成员,任何后代)覆盖object.Equals并提供其自己的实现,显然,它不能依赖于逐位比较来计算返回值。

速度慢的原因是Equals的参数属于object类型,且值类型必须加框以视为object。拳击涉及在堆上分配内存和将值类型复制到该位置的内存。

您可以手动为Equals方法提供重载,该方法将您自己的struct作为参数来阻止装箱:

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

答案 1 :(得分:10)

以下是来自mscorlib的反编译ValueType.Equals方法:

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 :(得分:3)

编译器生成函数的想法是合理的。

考虑到效果,但我认为语言设计团队做得对。对于初学者来说,从C ++中已知的Compilergenerated方法很难理解。让我们看一下使用自动生成的struct.Equals在C#中会发生什么:

就像现在一样,.Equals()的概念很简单:

  • 每个结构都从ValueType继承Equals。
  • 如果覆盖,则应用自定义Equals方法。

如果编译器总是创建Equals方法,我们可以:

  • 每个结构都从Object继承Equals。 (ValueType将不再实现自己的版本)
  • Object.Equals现在总是(!)覆盖,无论是由编译器生成的Equals方法还是由用户实现

现在我们的struct有一个自动生成的覆盖方法,代码阅读器看不到!那你怎么知道基础方法Object.Equals不适用于你的struct?通过学习所有自动编译生成方法的案例。这正是学习C ++的负担之一。

考虑将高效的struct Equals留给用户并保持概念简单,需要标准的默认Equals方法是一个很好的决定。

尽管如此,性能关键结构应该重写Equals。以下代码显示

在.Net 4.5.1上测量

3606 vs 53毫秒

这种性能提升肯定是因为避免虚拟等于,但无论如何,所以如果调用虚拟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)
    {
        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/