【问题标题】:Poor performance comparing collections of objects using reflections and Linq Except/Intersect使用反射和 Linq except/Intersect 比较对象集合的性能不佳
【发布时间】:2018-02-04 19:54:00
【问题描述】:

我编写了一个应用程序,它比较两个对象集合(相同类型),并通过使用它们的属性值(或它们的属性组合)比较对象来找出相似之处和不同之处。这个应用程序从未打算在任何一个集合中扩展超过 10000 个对象,并且人们认为这是一个长期运行的操作。业务需求现在发生了变化,我们需要能够比较任一集合中最多 50000 个(拉伸目标最多为 100000 个)的对象。

以下是要比较的类型的最小示例。

    internal class Employee
    {
        public string ReferenceCode { get; set; }
    }

为此,我为此类型编写了一个自定义相等比较器,它将属性名称作为构造函数参数。对其进行参数化的原因是避免为每种类型的每个属性编写不同的相等比较器(这是相当数量的,而且这听起来像是一个巧妙的解决方案)。

   public class EmployeeComparerDynamic : IEqualityComparer<Employee>
    {
        string PropertyNameToCompare { get; set; }
        public EmployeeComparerDynamic(string propertyNameToCompare)
        {
            PropertyNameToCompare = propertyNameToCompare;
        }

        public bool Equals(Employee x, Employee y)
        {
            return y.GetType().GetProperty(PropertyNameToCompare).GetValue(y) != null 
                && x.GetType().GetProperty(PropertyNameToCompare).GetValue(x)
                .Equals(y.GetType().GetProperty(PropertyNameToCompare).GetValue(y));
        }

        public int GetHashCode(Employee x) 
        {
            unchecked
            {
                int hash = 17;
                hash = hash * 23 + x.GetType().GetProperty(PropertyNameToCompare).GetHashCode();
                return hash;
            }
        }
    }

使用这个相等比较器,我一直在使用 LINQ IntersectExcept 函数比较对象集合。

        var intersectingEmployeesLinq = firstEmployeeList
            .Intersect(secondEmployeeList, new EmployeeComparerDynamic("ReferenceCode")).ToList();

        var deltaEmployeesLinq = firstEmployeeList
            .Except(secondEmployeeList, new EmployeeComparerDynamic("ReferenceCode")).ToList();

这一切都很好,直到缩放限制要求增加,我注意到我的应用程序在处理大量对象时性能很差。

最初,我认为这很正常,完成的总时间可能会显着增加,但是,当我尝试手动循环遍历一个列表并比较该项目以检查该项目是否存在时在另一个列表中 - 我注意到我自己的 LINQ ExceptIntersect 在我的应用程序上下文中实现的实现产生了相同的结果,但性能要好得多。

        var intersectingEmployeesManual = new List<Employee>();           

        foreach (var employee in firstEmployeeList)
        {
            if (secondEmployeeList.Any(x => x.ReferenceCode == employee.ReferenceCode))
                intersectingEmployeesManual.Add(employee);
        }

与早期 sn-p 中的实现相比,它的性能明显更好(大约 30 倍)。当然,前面的sn-p使用反射来获取属性的值,所以我也尝试过。

        var intersectingEmployeesManual = new List<Employee>();

        foreach (var employee in firstEmployeeList)
        {
            if (secondEmployeeList.Any(x => x.GetType()
                .GetProperty("ReferenceCode").GetValue(x)
                .Equals(employee.GetType().GetProperty("ReferenceCode").GetValue(employee))))
                intersectingEmployeesManual.Add(employee);
        }

这仍然表现好大约 2-3 倍。最后,我编写了另一个相等比较器,但不是参数化属性,而是与类型的预定义属性进行比较。

    public class EmployeeComparerManual : IEqualityComparer<Employee>
    {
        public bool Equals(Employee x, Employee y)
        {
            return y.ReferenceCode != null
                   && x.ReferenceCode.Equals(y.ReferenceCode);
        }

        public int GetHashCode(Employee x)
        {
            unchecked
            {
                int hash = 17;
                hash = hash * 23 + x.ReferenceCode.GetHashCode();
                return hash;
            }
        }
    }

以及相应的代码来计算交集和增量对象。

        var intersectingEmployeesLinqManual = firstEmployeeList
            .Intersect(secondEmployeeList, new EmployeeComparerManual()).ToList();

        var deltaEmployeesLinqManual = firstEmployeeList
            .Except(secondEmployeeList, new EmployeeComparerManual()).ToList();

最后,我开始通过这个实现获得我正在寻找的扩展,但另外我还使用 10 台不同的机器进行了一些基准测试。结果如下(平均,以毫秒为单位,四舍五入到最接近的毫秒)。

    +-------+-------------+-----------+-------------------+--------+----------------+----------------+------------------------+-------------+---------------------+
    |       | List Items  | Intersect | Intersect Dynamic | Except | Except Dynamic | Intersect Linq | Intersect Linq Dynamic | Except Linq | Except Linq Dynamic |
    +-------+-------------+-----------+-------------------+--------+----------------+----------------+------------------------+-------------+---------------------+
    | Run 1 | 5000/4000   |       479 |              7440 |    340 |           7439 |              1 |                  14583 |           2 |               15257 |
    | Run 2 | 10000/8000  |      2177 |             32489 |   1282 |          29290 |              1 |                  59154 |           2 |               74170 |
    | Run 3 | 20000/16000 |      6758 |            116266 |   4578 |         116720 |              5 |                 225960 |           3 |              295146 |
    | Run 4 | 50000/40000 |     34457 |            720023 |  30693 |         731690 |             14 |                1483084 |          14 |             1657832 |
    +-------+-------------+-----------+-------------------+--------+----------------+----------------+------------------------+-------------+---------------------+

所以,到目前为止我的总结是:

  • 使用反射获取属性值会增加 15-20 倍的开销
  • 在相等比较器和 LINQ ExceptIntersect 中使用反射会增加 2-3 倍的开销

我的未决问题是:

  • 使用反射来获取属性值真的会增加这么多开销还是我在这里遗漏了一块拼图?
  • 为什么在将 LINQ 与不使用反射的相等比较器一起使用时,我只能获得承诺的 O(n+m) 整体工作量?
  • 我是否有希望找到并接近可以让每个类型都有一个相等比较器并以某种方式参数化我正在比较的属性而不是每个属性每个类型的相等比较器?
  • 附带的问题 - 为什么在相等比较器中使用反射与 LINQ ExceptIntersect 相比,我自己的基本实现只是遍历列表,将所有内容与所有内容进行比较?

最后,下面是一个完整的可重现示例:

class Program
{
    static void Main(string[] args)
    {
        StackOverflow();
    }

    private static void StackOverflow()
    {
        var firstEmployeeList = CreateEmployeeList(5000);
        var secondEmployeeList = CreateEmployeeList(4000);

        var intersectingEmployeesManual = new List<Employee>();
        var sw = new Stopwatch();

        //Intersecting employees - comparing predefined property
        sw.Start();
        foreach (var employee in firstEmployeeList)
        {
            if (secondEmployeeList.Any(x => x.ReferenceCode == employee.ReferenceCode))
                intersectingEmployeesManual.Add(employee);
        }
        sw.Stop();
        Console.WriteLine("Intersecting Employees Manual: " + sw.ElapsedMilliseconds);
        intersectingEmployeesManual.Clear();
        sw.Reset();

        //Intersecting employees - comparing dynamic property
        sw.Start();
        foreach (var employee in firstEmployeeList)
        {
            if (secondEmployeeList.Any(x => x.GetType()
                .GetProperty("ReferenceCode").GetValue(x)
                .Equals(employee.GetType().GetProperty("ReferenceCode").GetValue(employee))))
                intersectingEmployeesManual.Add(employee);
        }
        sw.Stop();
        Console.WriteLine("Intersecting Employees Manual (dynamic property): " + sw.ElapsedMilliseconds);
        sw.Reset();

        //Delta Employees - comparing predefined property
        var deltaEmployeesManual = new List<Employee>();
        sw.Start();
        foreach (var employee in firstEmployeeList)
        {
            if (secondEmployeeList.All(x => x.ReferenceCode != employee.ReferenceCode))
                deltaEmployeesManual.Add(employee);
        }
        sw.Stop();
        Console.WriteLine("Delta Employees Manual: " + sw.ElapsedMilliseconds);
        sw.Reset();
        deltaEmployeesManual.Clear();

        //Delta Employees - comparing dynamic property
        sw.Start();
        foreach (var employee in firstEmployeeList)
        {
            if (secondEmployeeList
                .All(x => !x.GetType().GetProperty("ReferenceCode").GetValue(x)
                .Equals(employee.GetType().GetProperty("ReferenceCode").GetValue(employee))))
                deltaEmployeesManual.Add(employee);
        }
        sw.Stop();
        Console.WriteLine("Delta Employees Manual (dynamic property): " + sw.ElapsedMilliseconds);
        sw.Reset();

        //Intersecting employees Linq - dynamic property
        sw.Start();
        var intersectingEmployeesLinq = firstEmployeeList
            .Intersect(secondEmployeeList, new EmployeeComparerDynamic("ReferenceCode")).ToList();
        sw.Stop();
        Console.WriteLine("Intersecting Employees Linq (dynamic property): " + sw.ElapsedMilliseconds);
        sw.Reset();

        //Intersecting employees Linq - manual property
        sw.Start();
        var intersectingEmployeesLinqManual = firstEmployeeList
            .Intersect(secondEmployeeList, new EmployeeComparerManual()).ToList();
        sw.Stop();
        Console.WriteLine("Intersecting Employees Linq (manual property): " + sw.ElapsedMilliseconds);
        sw.Reset();

        //Delta employees Linq - dynamic property
        sw.Start();
        var deltaEmployeesLinq = firstEmployeeList
            .Except(secondEmployeeList, new EmployeeComparerDynamic("ReferenceCode")).ToList();
        sw.Stop();
        Console.WriteLine("Delta Employees Linq (dynamic property): " + sw.ElapsedMilliseconds);
        sw.Reset();

        //Delta employees Linq - manual property
        sw.Start();
        var deltaEmployeesLinqManual = firstEmployeeList
            .Except(secondEmployeeList, new EmployeeComparerManual()).ToList();
        sw.Stop();
        Console.WriteLine("Delta Employees Linq (manual property): " + sw.ElapsedMilliseconds);
        sw.Reset();

        Console.WriteLine("Finished");
        Console.ReadLine();

    }

    private static List<Employee> CreateEmployeeList(int numberToCreate)
    {
        var employeList = new List<Employee>();
        for (var i = 0; i < numberToCreate; i++)
        {
            employeList.Add(new Employee
            {
                ReferenceCode = i.ToString()
            });
        }
        return employeList;
    }

    internal class Employee
    {
        public string ReferenceCode { get; set; }
    }

    public class EmployeeComparerDynamic : IEqualityComparer<Employee>
    {
        string PropertyNameToCompare { get; set; }
        public EmployeeComparerDynamic(string propertyNameToCompare)
        {
            PropertyNameToCompare = propertyNameToCompare;
        }

        public bool Equals(Employee x, Employee y)
        {
            return y.GetType().GetProperty(PropertyNameToCompare).GetValue(y) != null 
                && x.GetType().GetProperty(PropertyNameToCompare).GetValue(x)
                .Equals(y.GetType().GetProperty(PropertyNameToCompare).GetValue(y));
        }

        public int GetHashCode(Employee x) 
        {
            unchecked
            {
                int hash = 17;
                hash = hash * 23 + x.GetType().GetProperty(PropertyNameToCompare).GetValue(x).GetHashCode();
                return hash;
            }
        }
    }

    public class EmployeeComparerManual : IEqualityComparer<Employee>
    {
        public bool Equals(Employee x, Employee y)
        {
            return y.ReferenceCode != null
                   && x.ReferenceCode.Equals(y.ReferenceCode);
        }

        public int GetHashCode(Employee x)
        {
            unchecked
            {
                int hash = 17;
                hash = hash * 23 + x.ReferenceCode.GetHashCode();
                return hash;
            }
        }
    }
}

编辑:

因此,借助在相等比较器中使用委托的建议以及我在动态相等比较器中没有正确计算哈希码这一点,我能够得出以下结论:

  • 反射确实增加了开销,但我对 LINQ ExceptIntersect 表现不佳的问题是因为动态相等比较器以及我使用属性而不是属性值的 GetHasCode() 计算哈希码的事实.
  • 使用委托相等确实可以恢复性能,并且使用语法保持简洁。

我现在实现了以下相等比较器:

public static class Compare
{
    public static IEqualityComparer<TSource> By<TSource, TIdentity>(Func<TSource, TIdentity> identitySelector)
    {
        return new DelegateComparer<TSource, TIdentity>(identitySelector);
    }

    public static IEnumerable<T> IntersectBy<T, TIdentity>(this IEnumerable<T> source, IEnumerable<T> second, Func<T, TIdentity> identitySelector)
    {
        return source.Intersect(second, By(identitySelector));
    }

    private class DelegateComparer<T, TIdentity> : IEqualityComparer<T>
    {
        private readonly Func<T, TIdentity> identitySelector;

        public DelegateComparer(Func<T, TIdentity> identitySelector)
        {
            this.identitySelector = identitySelector;
        }

        public bool Equals(T x, T y)
        {
            return Equals(identitySelector(x), identitySelector(y));
        }

        public int GetHashCode(T obj)
        {
            return identitySelector(obj).GetHashCode();
        }
    }
}

这很好地适用于以下用法语法:

        var intersectingEmployeesDelegate = firstEmployeeList
            .IntersectBy(secondEmployeeList, x => x.ReferenceCode).ToList();

我剩下的唯一悬而未决的问题是,是否有一种简洁的方法可以对给定类型的所有属性调用此比较。

我最初的实现类似于下面:

        foreach (var pInfo in typeof(Employee).GetProperties())
        {
            var intersectingEmployees = firstEmployeeList
                .Intersect(secondEmployeeList, 
                new EmployeeComparerDynamic(pInfo.Name)).ToList();
        }

如果使用委托比较器可以实现类似的任何想法?

【问题讨论】:

  • 那个比较器是一个性能杀手,每次您将一个员工与另一个员工进行比较时,您都在检索TypePropertyInfo。缓存该数据,反射很重,因此您缓存/减少反射操作的次数越多,它的效果就越好。
  • 使用反射获取值很慢。没有办法解决这个问题。 ValueType.Equals (msdn.microsoft.com/en-us/library/2dts52z7.aspx) 的默认行为是进行“逐字节”比较或 refeleciton 以遍历字段。这两种方法都非常缓慢,您应该提供适当的重载。反射只是不适合批量工作。
  • 老实说,我很困惑你为什么在这里使用反射。如果您想有多种方式对任何类进行排序,只需提供多个排序器。大多数类型在 IComparabile 实现中只有一个“默认顺序”。但不是全部。 usr 还指出了提供 Comperer 作为代表的选项。如果你走这条路,比较器甚至可以是一个匿名函数。
  • @Christopher 这并不是真正的排序,这些对象列表实际上是从不同的数据库中获取的,然后标准化为具有各种属性的相同模型,然后进行比较以找出差异和相似之处。
  • 一般来说强类型化是你最大的朋友。永远不要逃避它,永远拥抱它。如果您从弱类型源获取数据,ExpandoObject 可以为您服务。基本上它只是一个带有一些语法糖的Dictionary&lt;string, object&gt;,让它看起来你正在使用属性。一旦在 ExpandoObjects 中获得数据,就可以将其转换为一些强类型的自定义类。然后就用这些来工作。

标签: c# linq reflection


【解决方案1】:

也许您可以将委托传递给比较器?

new DelegateEqualityComparer<Employee>(e => e.ReferenceCode)

如果你想以字符串为基础,你可以从一个表达式树编译一个委托。在构造函数中执行一次。

【讨论】:

  • 我认为能够比较 int 属性也很好,而无需转换为字符串。
  • @usr 谢谢你的建议,我已经用 DelegateEqualityComparer 的实现编辑了我的问题,也许你知道如何迭代地评估给定类型的属性?
【解决方案2】:

当您使用反射来获取所有属性时,您必须使用 usr 提出的解决方案。您必须将表达式树构造并编译为委托,并将其用作比较器构造函数的参数。代码可能如下所示:

public static IEqualityComparer<T> GetComparer<T>(PropertyInfo propertyInfo)
{
    Type tT = typeof(T);
    ParameterExpression paramExpr = Expression.Parameter(tT);
    MemberExpression memberExpr = Expression.Property(paramExpr, propertyInfo);
    LambdaExpression lambdaExpr = Expression.Lambda(memberExpr, paramExpr);

    Type tQ = memberExpr.Type;
    Type te = typeof(DelegateEqualityComparer<,>);
    Type te2 = te.MakeGenericType(new Type[] { tT, tQ });
    ConstructorInfo ci = te2.GetConstructors()[0];

    Object i = ci.Invoke(new object[] { lambdaExpr.Compile() });

    return (IEqualityComparer<T>)i;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-26
    • 2015-03-30
    • 1970-01-01
    相关资源
    最近更新 更多