【问题标题】:Optimize complex objects comparison优化复杂对象比较
【发布时间】:2019-08-30 14:49:35
【问题描述】:

我有一个模型类 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 秒。

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

此外,obj1obj2 中的 Class2Class3Class4 对象的数量将始终相同

【问题讨论】:

  • 尝试以下操作:(Id + "^" + Name + string.Join("^",Class4s.Select(x => x.Id))).ToHash();如果 Id 是唯一的,则不需要 Name 和 Id。
  • 这不是您问题的答案,它可能只是您的代码示例中的一个错字,但以防万一您应该知道您对 Equals(object) 的覆盖都是无限递归的。
  • @jdweng 你能详细说明一下吗?是的,我确实需要 IdName 都等于说对象是平等的。
  • Ids 通常是唯一的。你需要两者吗?如果 ID(如果需要,名称是唯一的)Joshua 是错误的。你不需要递归。旁边的哈希不需要是唯一的。哈希是 EQUAL 的第一步,最小化重复哈希会加速比较。在执行散列后,将执行相等方法作为第二步,以消除散列可能重复的情况。在每种情况下,您都可以只让哈希返回 1。如果您有一个来自“a”、“ab”、“bcd”、“cd”等词的哈希。并结合任意两者。你可以得到“abcd”。添加“^”你会得到“a^bcd”和“ab^cd”。
  • 我认为这里没有太大的优化空间。瓶颈显然是频繁的OrderBy 调用,使用IList 时您无法真正避免。我会去审查模型,并检查是否可以移动到预排序或键索引的数据结构,如 IDictionary,或缓存以前相等性测试的结果。

标签: c# list performance linq comparison


【解决方案1】:

我已经对你的代码和我必须优化你的代码的想法做了一些BenchmarkDotNet 基准测试。

对于每个测试,我创建了 Class1 的 1 个实例,它有 150 个 Class2 类型的孩子,每个孩子有 150 个 Class3 类型的孩子,每个孩子都有 150 个 @987654327 类型的孩子@。

我已经测量了将一个对象与其自身进行比较,因为比较不同的对象会快得多,因为任何返回错误快捷方式的比较都会影响整个事情。此外,没有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

【讨论】:

    【解决方案2】:

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

    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,所有对象的结构都相似,因为它没有内部列表。

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

    【讨论】:

      【解决方案3】:

      对列表进行排序只是为了比较它们对我来说似乎效率很低。您可以尝试使用其他方法来比较列表

      代替

      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 方法执行相同的操作。

      【讨论】:

      • 我尝试了这种方法,但速度较慢。与SequenceEqual 的比较需要 12 秒,而这种方法需要 45 秒以上。我可以以某种方式改进我的 GetHashCode 方法以使其更快吗?
      猜你喜欢
      • 1970-01-01
      • 2011-04-01
      • 2020-11-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-22
      相关资源
      最近更新 更多