【问题标题】:C# - Value Type Equals method - why does the compiler use reflection?C# - 值类型等于方法 - 为什么编译器使用反射?
【发布时间】:2009-06-17 20:35:52
【问题描述】:

我刚刚遇到了一些对我来说很奇怪的事情:当你对一个值类型使用 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 的字段?

对我来说很奇怪。

谢谢:)

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

【问题讨论】:

  • "要使用 Equals 的标准实现,您的值类型必须装箱并作为引用类型 System.ValueType 的实例传递。然后 Equals 方法使用反射来执行比较。" - msdn.microsoft.com/en-us/library/ff647790.aspx

标签: c# compiler-construction struct


【解决方案1】:

以下是从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 可能会在此处注入适当的代码。至于为什么它这么慢,我不能不告诉你。

【讨论】:

  • 我想知道如果运行时为任何尚未定义的结构自动生成Equals 覆盖是否会出现兼容性问题:bool Equals(object other) { return StructComparer&lt;thisType&gt;.EqualsProc(ref this, other); },其中EqualsProc 是静态类StructComparer&lt;thisType&gt; 中的静态委托字段?这种方法可以避免每次比较对象时都必须使用反射,并且还可以避免装箱步骤。
【解决方案2】:

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

它慢的原因是Equals 的参数是object 类型,并且值类型必须装箱才能被视为object。装箱涉及在堆上分配内存以及将值类型复制到该位置的内存。

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

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

【讨论】:

  • 它在某些情况下使用反射。如果它检测到它可以只对结果进行 blit,它就会这样做 - 但如果字段中有引用类型(或包含引用类型的类型),它必须做一个更痛苦的过程。
  • 在我写这篇文章时,我翻阅了一下,发现一些 .Net 框架作者(Cwalina、Abrams)只是确认 Equals 正在对值类型使用反射。但也许只是在 Framework 2.0 中?
  • Sylvain:他们是对的。正如 Jon 所说,如果结构包含引用类型作为成员,它必须在该字段上调用 ​​Equals。我更新了答案以反映这一点。我试图说明的一点是,它在不需要时不使用反射(如您的示例)。
  • 我不得不说,我不明白为什么有引用时不能进行逐位比较。如果两个引用指向同一个对象,什么时候指针不完全相等?
  • @Snarfblam:从 Mehrdad 发布的代码来看,它似乎在结构中包含的每个引用对象上调用 Equals - 几乎是递归的。如果两个对象相等但它们的引用不相等,则按位比较将失败。
【解决方案3】:

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

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

现在,.Equals() 的概念很简单:

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

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

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

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

将有效的 struct Equals 留给用户并保持概念简单,这需要标准的默认 Equals 方法,这是一个不错的决定。

也就是说,性能关键的结构应该覆盖 Equals。下面的代码显示

360653 毫秒 在 .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)
    {
        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/

【讨论】:

  • 值得注意的是,编译器不使用反射;它只是使用虚拟方法调度到方法ValueType.Equals;因为该方法期望this 是一个类类型[ValueType 尽管它的名字是一个类],所以需要将值装箱。从概念上讲,如果ValueType 定义了一个静态方法ValueTypeEquals&lt;T&gt;(ref T it, Object other) { ValueTypeComparer&lt;T&gt;.Compare(ref it, other); 并建议编译器在可能的情况下调用它而不是虚拟Equals 方法,那可能会很好。这种方法可能...
  • ...使运行时仅在第一次使用每种类型时为其生成一个比较器,然后在后续调用时直接访问该比较器。
  • Nitpick:ReferenceEquals(null, obj) 调用在技术上与the is expression evaluates to false if the provided expression (obj) is null 一样是多余的。我确信它不会以任何有用的方式影响基准测试的结果。不过,如果有关系的话,我不会依赖编译器来优化它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-05-21
  • 2018-05-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多