优化复杂对象比较

时间:2019-08-30 14:49:35

标签: c# list performance linq comparison

我有一个模型类Class1,我想比较Class1的两个实例是否相同(结构相等)。

public class Class1 : IEquatable<Class1>
{
    public string Id { get; set; }
    public string Name { get; set; }
    public IList<Class2> Class2s { get; set; }

    public bool Equals(Class1 other)
    {
       return QuestName.Equals(other.QuestName)
            && Class2s.OrderBy(c => c.Id).SequenceEqual(other.Class2s.OrderBy(c => c.Id));
                    //Below method is very fast but not so accurate
                    //because 2 objects with the same hash code may or may not be equal
        //return GetHashCode() == other.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        return obj is Class1
            && this.Equals(obj as Class1);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 13;
            hash = (hash * 7) + Name.GetHashCode();
            foreach (var c2 in Class2s.OrderBy(c => c.Id))
            {
                hash = (hash * 7) + c2.GetHashCode();
            }
            return hash;
        }
    }
}

public class Class2 : IEquatable<Class2>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public IList<Class3> Class3s { get; set; }

    public bool Equals(Class2 other)
    {
        return Id == other.Id
             && Name.Equals(other.Name)
             && Class3s.OrderBy(c => c.Id).SequenceEqual(other.Class3s.OrderBy(c => c.Id));
    }

    public override bool Equals(object obj)
    {
        return obj is Class2
            && this.Equals(obj as Class2 );
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 13;
            hash = (hash * 7) + Id.GetHashCode();
            hash = (hash * 7) + Name.GetHashCode();
            foreach (var c3 in Class3s.OrderBy(c => c.Id))
            {
                hash = (hash * 7) + c3.GetHashCode();
            }
            return hash;
        }
    }
}

public class Class3 : IEquatable<Class3>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public IList<Class4> Class4s { get; set; }

    public bool Equals(Class3 other)
    {
        return Id == other.Id
            && Name.Equals(other.Name)
            && Class4s.OrderBy(c => c.Id).SequenceEqual(other.Class4s.OrderBy(c => c.Id));
    }

    public override bool Equals(object obj)
    {
        return obj is Class3
            && this.Equals(obj as Class3);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 13;
            hash = (hash * 7) + Id.GetHashCode();
            hash = (hash * 7) + Name.GetHashCode();
            foreach (var c in Class4s.OrderBy(c => c.Id))   
            {
                hash = (hash * 7) + c.GetHashCode();
            }                
            return hash;
        }
    }
}

public class Class4 : IEquatable<Class4>
{
    public int Id { get; set; }
    public string Name { get; set; }

    public bool Equals(Class4 other)
    {
        return Id.Equals(other.Id)
            && Name.Equals(other.Name);
    }

    public override bool Equals(object obj)
    {
        return obj is Class4
            && this.Equals(obj as Class4);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 13;
            hash = (hash * 7) + Id.GetHashCode();
            hash = (hash * 7) + Name.GetHashCode();
            return hash;
        }
    }
}

我说,在以下情况下,两个Class1对象相等:
1.它们具有相同的Name
2.它们具有相同的Class2对象(它们的顺序无关紧要)

两个Class2对象相等:
1.它们具有相同的ID
2.它们具有相同的名称
3.它们具有相同的Class3对象(它们的顺序无关紧要)

两个Class3对象相等:
1.它们具有相同的ID
2.它们具有相同的名称
3.它们具有相同的Class4对象(它们的顺序无关紧要)

两个Class4对象相等:
1.它们具有相同的ID
2.它们具有相同的名称

我使用Equals方法比较它们,并像这样测量运行时间:

Class1 obj1 = GetFirstClass1Object();
Class1 obj2 = GetSecondClass1Object();
var startTime = DateTime.Now;
bool equals = obj1.Equals(obj2);
var elaspedTime = DateTime.Now.Substract(startTime)

上述解决方案效果很好,但是速度很慢。 我知道,如果我们将obj1obj2展平,它们每个包含3500个Class4对象,并且比较obj1obj2大约需要12秒钟。

有没有更快的方法可以做到这一点?我可以以某种方式利用散列来使其更快吗?

此外,Class2Class3内的Class4obj1obj2对象的数量将始终相同

3 个答案:

答案 0 :(得分:4)

我已经针对您的代码做了一些BenchmarkDotNet基准测试,并提出了一些优化代码的想法。

对于每个测试,我创建了一个Class1实例,其中有150个类型为Class2的子代,每个子实体都有150个类型为Class3的子代,每个子代都有150个Class4类型的子代。

我已经测量过将对象与自身进行比较的原因,因为比较不同的对象会更快,因为任何返回假快捷方式的比较都会使整个过程变得更短。另外,没有ReferenceEquals()快捷方式,因此我不必费心克隆对象。

测量

|                                                                 Method |        Mean | Error | Ratio |
|----------------------------------------------------------------------- |------------:|------:|------:|
|                                                        'Original code' |   535.46 ms |    NA |  1.00 |
|                               'Custom dictionary-based SequenceEquals' | 6,606.23 ms |    NA | 12.34 |
| 'Custom dictionary-based SequenceEquals, classes cache their HashCode' | 1,136.91 ms |    NA |  2.12 |
|                                 'Custom Except()-based SequenceEquals' | 2,281.12 ms |    NA |  4.26 |
|   'Custom Except()-based SequenceEquals, classes cache their HashCode' |   257.46 ms |    NA |  0.48 |
|                                                         'No OrderBy()' |    76.31 ms |    NA |  0.14 |
  • Original code:这是您的代码。我将它用作比较的基准。
  • Custom dictionary-based SequenceEquals:然后,我尝试优化列表相等性比较。首先,我尝试了受this answer启发的Dictionary解决方案。原来,它慢了12倍,因为Dictionary必须频繁地计算哈希码,而在我们的案例中,哈希码意味着遍历子代和嵌套子代。
  • Custom dictionary-based SequenceEquals, classes cache their HashCode:我认为如果开始缓存哈希码,则可以做得更好。现在,基于Dictionary的解决方案的速度仅为原始解决方案的两倍。
  • Custom Except()-based SequenceEquals:然后是Except()方法。在幕后,它创建了类似HashSet的东西。据我了解,它只需要为两个可枚举的每个元素计算一次哈希码。该解决方案所需的时间是原始解决方案的4.26倍。
  • Custom Except()-based SequenceEquals, classes cache their HashCode:与以前一样,我开始缓存哈希码,因此实际上只为每个对象计算一次。生成的解决方案花费原始时间的0.48倍。还不错。
  • No OrderBy():然后,我不再使用OrderBy(),仅使用SequenceEquals(),并且鉴于我正在将对象与其自身进行比较,您可以说数据已经被排序了,因此比较安全::)。最终的解决方案是极大的加速,其速度是原始速度的0.14倍。

总结:

您最好的选择是查看模型和需求,您真的需要比较像这样的巨大对象图吗? 如果确实需要:

  • 使您的对象不可变,缓存哈希码,并使用基于Except()的比较。请注意,由于基于集合的解决方案假定您不关心重复项,因此必须在Count之前比较列表Except()
  • 或者,代替列表,使用某种排序列表,以避免必须使用OrderBy()和简单的SequenceEquals()比较。这是一个折衷,因为刀片将变得更加昂贵。请参阅,这是否适用于您的方案。

将我的代码和度量值上传到this repo

答案 1 :(得分:1)

以提供的类为例,考虑以下结构。没有基于您的示例的示例数据可以对其进行测试,因此您将必须使用自己的示例进行测试。

public class Class1 : IEquatable<Class1> {
    public int Id { get; set; }
    public string Name { get; set; }
    public IList<Class2> Class2s { get; set; }

    public static bool operator ==(Class1 left, Class1 right) {
        return Equals(left, right);
    }

    public static bool operator !=(Class1 left, Class1 right) {
        return !(left == right);
    }

    public bool Equals(Class1 other) {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return string.Equals(this.ToString(), other.ToString());
    }

    public override bool Equals(object obj) {
        return obj is Class1 other && this.Equals(other);
    }

    public override int GetHashCode() {
        return ToString().GetHashCode();
    }

    public override string ToString() {
        var cs = Class2s == null ? "" : string.Join("", Class2s.OrderBy(_ => _.Id).Select(_ => _.ToString()));
        return string.Join("", Id, Name, cs);
    }
}

public class Class2 : IEquatable<Class2> {
    public int Id { get; set; }
    public string Name { get; set; }
    public IList<Class3> Class3s { get; set; }

    public static bool operator ==(Class2 left, Class2 right) {
        return Equals(left, right);
    }

    public static bool operator !=(Class2 left, Class2 right) {
        return !(left == right);
    }

    public bool Equals(Class2 other) {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return string.Equals(this.ToString(), other.ToString());
    }

    public override bool Equals(object obj) {
        return obj is Class2 other && this.Equals(other);
    }

    public override int GetHashCode() {
        return ToString().GetHashCode();
    }

    public override string ToString() {
        var cs = Class3s == null ? "" : string.Join("", Class3s.OrderBy(_ => _.Id).Select(_ => _.ToString()));
        return string.Join("", Id, Name, cs);
    }
}

public class Class3 : IEquatable<Class3> {
    public int Id { get; set; }
    public string Name { get; set; }
    public IList<Class4> Class4s { get; set; }

    public static bool operator ==(Class3 left, Class3 right) {
        return Equals(left, right);
    }

    public static bool operator !=(Class3 left, Class3 right) {
        return !(left == right);
    }

    public bool Equals(Class3 other) {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return string.Equals(this.ToString(), other.ToString());
    }

    public override bool Equals(object obj) {
        return obj is Class3 other && this.Equals(other);
    }

    public override int GetHashCode() {
        return ToString().GetHashCode();
    }

    public override string ToString() {
        var cs = Class4s == null ? "" : string.Join("", Class4s.OrderBy(_ => _.Id).Select(_ => _.ToString()));
        return string.Join("", Id, Name, cs);
    }
}

public class Class4 : IEquatable<Class4> {
    public int Id { get; set; }
    public string Name { get; set; }

    public static bool operator ==(Class4 left, Class4 right) {
        return Equals(left, right);
    }

    public static bool operator !=(Class4 left, Class4 right) {
        return !(left == right);
    }

    public bool Equals(Class4 other) {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return string.Equals(this.ToString(), other.ToString());
    }

    public override bool Equals(object obj) {
        return obj is Class4 other && Equals(other);
    }

    public override int GetHashCode() {
        return ToString().GetHashCode();
    }

    public override string ToString() {
        return string.Format("{0}{1}", Id, Name);
    }
}

Class4之外,所有对象的结构都相似,因为它没有内部列表。

尽管只是一个示例,但是很多重复的代码都可以重构为一个通用的基类。

答案 2 :(得分:0)

对列表进行排序以比较它们对我而言似乎效率很低。您可以尝试使用其他方法比较列表

代替

Class2s.OrderBy(c => c.Id).SequenceEqual(other.Class2s.OrderBy(c => c.Id)

您可以尝试类似

!Class2s.Except(other.Class2s).Any()

如果大多数对象不相等,则还可以添加一个额外的测试,以确保列表的大小不相同时不会循环播放这些列表:

Class2s.Count == other.Class2s.Count && !Class2s.Except(other.Class2s).Any()

当然,您也可以对Class2.Equals()和Class3.Equals方法执行相同的操作。