27

これは、今日まで気が付かなかったことです。どうやら、よく使用されるタプル クラス ( など) の .NET 実装により、Tuple<T>等価ベースの操作が実行されるときに、値の型に対してTuple<T1, T2>ボクシング ペナルティが発生するようです。

クラスがフレームワークにどのように実装されているかを次に示します (ソースは 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;
    }
}

私が見る問題は、2 段階のボックス化とボックス化解除が発生することです。1つはアイテムをどのボックスEqualsで呼び出すか、2 つは非ジェネリック呼び出しで、内部的にアイテムを元のタイプにボックス化解除する必要があります。comparer.EqualsEqualityComparer<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 タプル クラスでこのように等価性が実装されているのを見て驚きました。辞書の 1 つでタプル型をキーとして使用していました。

最初のコードに示すようにこれを実装する必要がある理由はありますか? その場合、このクラスを使用するのは少しお勧めできません。

コードのリファクタリングと重複しないデータが大きな懸念事項になるべきではなかったと思います。同じ非ジェネリック/ボクシングの実装も遅れていIStructuralComparableますが、IStructuralComparable.CompareToあまり使用されていないため、頻繁に問題になることはありません。


上記の 2 つのアプローチを、次のように負担の少ない 3 番目のアプローチでベンチマークしました (要点のみ)。

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>フィールドでは、1000000Equalsが呼び出されます。結果は次のとおりです。

最初のアプローチ (元の .NET 実装) - 310 ミリ秒

2 番目のアプローチ - 60 ミリ秒

3 番目のアプローチ - 130 ミリ秒

デフォルトの実装は、最適なソリューションよりも約 4 ~ 5 倍遅くなります。

4

1 に答える 1