.NET Tuple和Equals性能

naw*_*fal 26 .net performance boxing tuples 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我看到的问题是它会导致一个两阶段的装箱 - 取消装箱,比如用于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;
}
Run Code Online (Sandbox Code Playgroud)

我很惊讶地看到在.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);
} 
Run Code Online (Sandbox Code Playgroud)

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

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

第二种方法 - 60 ms

第3种方法 - 130毫秒

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

Tim*_*ith 12

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

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

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

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

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