.NET Tuple和Equals性能

时间:2014-01-13 05:25:57

标签: .net performance tuples boxing design-decisions

这是我直到今天才注意到的事情。显然,当执行基于相等的操作时,大量使用的元组类(Tuple<T>Tuple<T1, T2>等)的.NET实现会导致值类型的装箱惩罚

以下是该类在框架中的实现方式(来自ILSpy的源代码):

public class Tuple<T1, T2> : IStructuralEquatable 
{
    public T1 Item1 { get; private set; }
    public T2 Item2 { get; private set; }

    public Tuple(T1 item1, T2 item2)
    {
        this.Item1 = item1;
        this.Item2 = item2;
    }

    public override bool Equals(object obj)
    {
        return this.Equals(obj, EqualityComparer<object>.Default);
    }

    public override int GetHashCode()
    {
        return this.GetHashCode(EqualityComparer<object>.Default);
    }

    public bool Equals(object obj, IEqualityComparer comparer)
    {
        if (obj == null)
        {
            return false;
        }

        var tuple = obj as Tuple<T1, T2>;
        return tuple != null 
            && comparer.Equals(this.Item1, tuple.Item1) 
            && comparer.Equals(this.Item2, tuple.Item2);
    }

    public int GetHashCode(IEqualityComparer comparer)
    {
        int h1 = comparer.GetHashCode(this.Item1);
        int h2 = comparer.GetHashCode(this.Item2);

        return (h1 << 5) + h1 ^ h2;
    }
}

我看到的问题是它会导致两阶段装箱 - 取消装箱,例如Equals个来电,一个,comparer.Equals装箱物品,两个,EqualityComparer<object>来电非通用 Equals,而内部必须将项目拆分为原始类型。

相反,他们为什么不这样做:

public override bool Equals(object obj)
{
    var tuple = obj as Tuple<T1, T2>;
    return tuple != null
        && EqualityComparer<T1>.Default.Equals(this.Item1, tuple.Item1)
        && EqualityComparer<T2>.Default.Equals(this.Item2, tuple.Item2);
}

public override int GetHashCode()
{
    int h1 = EqualityComparer<T1>.Default.GetHashCode(this.Item1);
    int h2 = EqualityComparer<T2>.Default.GetHashCode(this.Item2);

    return (h1 << 5) + h1 ^ h2;
}

public bool Equals(object obj, IEqualityComparer comparer)
{
    var tuple = obj as Tuple<T1, T2>;
    return tuple != null
        && comparer.Equals(this.Item1, tuple.Item1)
        && comparer.Equals(this.Item2, tuple.Item2);
}

public int GetHashCode(IEqualityComparer comparer)
{
    int h1 = comparer.GetHashCode(this.Item1);
    int h2 = comparer.GetHashCode(this.Item2);

    return (h1 << 5) + h1 ^ h2;
}

我很惊讶地看到在.NET元组类中以这种方式实现了相等性。我在其中一个词典中使用元组类型作为键。

是否有任何理由必须如第一段代码所示实现?在这种情况下使用此类有点令人沮丧。

我认为代码重构和非重复数据应该是主要问题。同样的非通用/装箱实现也落后于IStructuralComparable,但由于IStructuralComparable.CompareTo使用较少,因此通常不会出现问题。


我使用第三种方法对上述两种方法进行了基准测试,这种方法仍然不那么费力,像这样(只有必需品):

public override bool Equals(object obj)
{
    return this.Equals(obj, EqualityComparer<T1>.Default, EqualityComparer<T2>.Default);
}

public bool Equals(object obj, IEqualityComparer comparer)
{
    return this.Equals(obj, comparer, comparer);
}

private bool Equals(object obj, IEqualityComparer comparer1, IEqualityComparer comparer2)
{
    var tuple = obj as Tuple<T1, T2>;
    return tuple != null
        && comparer1.Equals(this.Item1, tuple.Item1)
        && comparer2.Equals(this.Item2, tuple.Item2);
} 

对于几个Tuple<DateTime, DateTime>字段进行1000000次Equals次呼叫。这是结果:

  

第一种方法(原始.NET实现) - 310 ms

     

第二种方法 - 60 ms

     

第3种方法 - 130 ms

默认实施比最佳解决方案慢约4-5倍。

1 个答案:

答案 0 :(得分:10)

你想知道是否必须以这种方式实施。简而言之,我会说不:有许多功能相同的实现。

但是为什么现有的实现明确使用EqualityComparer<object>.Default?这可能只是一个人写了这个心理优化的'错误'的情况,或者至少不同于你在内循环中的速度情景。根据他们的基准,它可能看起来是“正确的”。

但是什么基准测试场景可能导致他们做出这样的选择呢?他们所针对的优化似乎是针对最小数量的EqualityComparer类模板实例进行优化。他们可能会选择这个,因为模板实例化会带来内存或加载时间成本。如果是这样,我们可以猜测他们的基准测试场景可能是基于app-startup-time或内存使用而不是一些紧凑的循环场景。

这是支持该理论的一个知识点(通过使用确认偏差找到:) - 如果T是结构,则无法共享EqualityComparer实现方法体。摘自http://blogs.microsoft.co.il/sasha/2012/09/18/runtime-representation-of-genericspart-2/

  

当CLR需要创建一个封闭泛型类型的实例时,   比如List,它创建了一个方法表和基于的EEClass   开放式。与往常一样,方法表包含方法指针,其中   由JIT编译器即时编译。但是,有一个   这里的关键优化:关闭泛型的编译方法体   可以共享具有引用类型参数的类型。   [...]   相同   想法不适用于价值类型。例如,当T很长时,   赋值语句项[size] = item需要不同的   指令,因为必须复制8个字节而不是4个。甚至更大   值类型甚至可能需要多个指令;等等。