.NET Tuple 和 Equals 性能

Posted

技术标签:

【中文标题】.NET Tuple 和 Equals 性能【英文标题】:.NET Tuple and Equals performance 【发布时间】:2014-01-31 19:01:49 【问题描述】:

这是我直到今天才注意到的。显然,当执行基于等式的操作时,经常使用的元组类(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&lt;object&gt; 调用非通用 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&lt;DateTime, DateTime&gt; 字段,1000000 Equals 调用。结果如下:

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

第二次接近 - 60 毫秒

第三种方法 - 130 毫秒

默认实现比最优方案慢约 4-5 倍。

【问题讨论】:

我看不出使用IEqualityComparer&lt;object&gt; 的任何原因,但我并不是说没有。但你仍然可以让它变得更好一点:pastebin.com/tNA2FYjq @MarcinJuraszek 怎么样更好? Tuple&lt;,&gt; 实现 IStructuralEquatable 具有这些定义 bool Equals(object other, IEqualityComparer comparer); int GetHashCode(IEqualityComparer comparer); Tuple 类型不同的第二个示例进行测试。就个人而言,如果您使用元组作为键,它可能是创建正确命名的类的候选者,具有确定的相等实现。这将提高可读性。另外,你能提供你用来测试性能的代码吗?过去我遇到过类似的与哈希码相关的性能问题,尤其是在 struct 键上,提供了实现相等的自定义类型,或者自定义相等比较器极大地提高了性能,如您所见。 @nawfal 我们在内部使用大量字典作为服务器上的穷人缓存,我们最近清除了定义不明确的复合键(目前是我们自己在 .NET 2.0 上实现的 Tuple)和审查了所有使用复合词典的案例。我们采用了两种方法。 1) 字典中的复合键,由自定义类或结构组成,在类型本身或字典上具有自定义相等性。 2) 嵌套字典的复合字典结构。根据键的宽度创建很多字典,但访问速度非常快。性能跃升。 @nawfal 类和结构自定义键之间的差异可以忽略不计,并且取决于结构大小和最终用途等某些因素,但通常重要的是提供良好的平等实现。在回答您的问题时,Tuple 默认代码可能不是最佳代码,但除非编写/批准代码的人出现,否则您不太可能理解最终实现背后的驱动力。 【参考方案1】:

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

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

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

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

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

【讨论】:

以上是关于.NET Tuple 和 Equals 性能的主要内容,如果未能解决你的问题,请参考以下文章

性能优化

Java的hashCode和equals方法

提高 ASP.NET Web 应用性能的 24 种方法和技巧

提高 ASP.NET Web 应用性能的 24 种方法和技巧(转载)

.NET内存性能分析宝典

是什么让.NET7的Min和Max方法性能暴增了45倍?