.NET 元组和等于性能

标签 .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>字段 a 1000000 Equals调用。这是结果:

1st approach (original .NET implementation) - 310 ms

2nd approach - 60 ms

3rd approach - 130 ms


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

最佳答案

您想知道它是否“必须”以这种方式实现。简而言之,我会说不:有许多功能等效的实现。

但是为什么现有的实现会如此明确地使用 EqualityComparer<object>.Default ?可能只是写这篇文章的人在心理上针对“错误”进行了优化,或者至少与您在内部循环中的速度场景不同。根据他们的基准,这似乎是“正确”的事情。

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

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

When the CLR needs to create an instance of a closed generic type, such as List, it creates a method table and EEClass based on the open type. As always, the method table contains method pointers, which are compiled on the fly by the JIT compiler. However, there is a crucial optimization here: compiled method bodies on closed generic types that have reference type parameters can be shared. [...] The same idea does not work for value types. For example, when T is long, the assignment statement items[size] = item requires a different instruction, because 8 bytes must be copied instead of 4. Even larger value types may even require more than one instruction; and so on.

关于.NET 元组和等于性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21084412/

相关文章:

css - 重置 CSS box-sizing : universal selector vs . 框类

ios - 在元组中追加值

python - 为什么期望的字符串变成元组

.net - 有哪些不同的 CLR 句柄类型?

.net - 跨 DMZ 的代理调用

.net - Union、Intersect 和 Except 的非 LINQ 实现

python - 如果要计数的值位于另一只 Pandas 数据框中(以一种更快的方式),如何计算值的出现?

.net - null 在 .NET 中如何表示

android - 如何在android中添加r8规则?

c# - 如何命名元组属性?