【问题标题】:C# Performance is slow on list comparisons using LINQ使用 LINQ 进行列表比较时,C# 性能很慢
【发布时间】:2020-12-19 17:55:53
【问题描述】:

我有这个型号的列表

public Class Contact
{
   public string MobileNumber {get;set;}
   public string PhoneNumber2 {get;set;}
}

我有一个方法可以将此列表与电话号码列表进行比较并返回不匹配的值

private List<ContactDto> GetNewContactsNotFoundInCrm(ContactPostModel model) 
{
    var duplicates = GetAllNumbers(); // Returns a List<string> of 5 million numbers
    var mobile = duplicates.Select(x => x.MobilePhone).ToList();
    var telephone2 = duplicates.Select(x => x.Telephone2).ToList();

    // I'm trying to compare Telephone2 and MobilePhone against the
    // duplicates list of 5 million numbers. It works, but it's slow
    // and can take over a minute searching for around 5000 numbers.

    return model.Contacts
        .Where(y => 
            !mobile.Contains(y.Phonenumber.ToPhoneNumber()) &&
            !telephone2.Contains(y.Phonenumber.ToPhoneNumber()) && 
            !mobile.Contains(y.Phonenumber2.ToPhoneNumber()) && 
            !telephone2.Contains(y.Phonenumber2.ToPhoneNumber()))
        .ToList();
}

// Extension method used
public static string ToPhoneNumber(this string phoneNumber)
{
    if (phoneNumber == null || phoneNumber == string.Empty)
        return string.Empty;

    return phoneNumber.Replace("(", "").Replace(")", "")
        .Replace(" ", "").Replace("-", "");
}

我可以使用什么数据结构来比较 MobileTelephone2 与 500 万个数字的列表以获得更好的性能?

【问题讨论】:

  • HashSet
  • 1) 尝试使用HashSet&lt;T&gt; 而不是List&lt;T&gt; 2) 缓存ToPhoneNumber() 结果,您对所有元素都调用了很多次。
  • 您从哪里获得电话号码(GetAllNumbers 的内部信息)?为了快速获得它,理想的情况是源已经准备好进行此类操作,例如在那里有一个索引或类似的东西。
  • @RufusL:那不是字符串。包含,那是集合。包含。

标签: c# .net linq search optimization


【解决方案1】:

创建一个 HashSet 可能会解决您的问题:

    var mobile = new HashSet<string>(duplicates.Select(x => x.MobilePhone));
    var telephone2 = new HashSet<string>(duplicates.Select(x => x.Telephone2));

您还可以进行其他性能改进,但与避免为您检查的每个数字重复超过 500 万个项目相比,它们将是微优化。

【讨论】:

    【解决方案2】:

    您可以使用Enumerable.Except
    Enumerable.Except 在内部使用HashSet 来提高查找性能。您想使用允许传入自定义 IEqualityComparer 作为参数的重载。

    另请注意,ToList() 是一个 LINQ 查询终结器。意味着ToList() 立即执行 LINQ 表达式 - 这会导致对集合的完整迭代。 LINQ 的强大之处在于查询被延迟执行,从而显着提高了性能。所有子查询(无论是链接还是拆分为单独的语句)都在内部使用yield return 合并到一个迭代中:

    表现不错:

    // Instead of two iterations, LINQ will defer both 
    // iterations and merge them into a single iteration
    var filtered = collection.Where(); // Deferred iteration #1
    var projected = filtered.Select(); // Deferred iteration #2
    var results = projected.ToList(); // Results in one single iteration
    

    表现不佳:

    // LINQ will execute each iteration immediately
    // resulting in two complete iterations
    var filtered = collection.Where().ToList(); // Executed iteration #1
    var projected = filtered.Select().ToList(); // Executed iteration #2
    

    您应该避免在真正想要执行查询之前调用终结器以显着提高性能:

    // Executes deferred
    var mobile = duplicates.Select(x => x.MobilePhone);
    

    代替:

    // Executes immediately
    var mobile = duplicates.Select(x => x.MobilePhone).ToList();
    

    还要注意每个Enumerable.Contains 执行一个单独的迭代。 Contains 是一个终结器,将立即执行:

    return model.Contacts
      .Where(y => 
        !mobile.Contains(y.Phonenumber.ToPhoneNumber()) // Iteration #1
          && !telephone2.Contains(y.Phonenumber.ToPhoneNumber()) // Iteration #2
          && !mobile.Contains(y.Phonenumber2.ToPhoneNumber()) // Iteration #3
          && !telephone2.Contains(y.Phonenumber2.ToPhoneNumber())) // Iteration #4
      .ToList(); // Iteraion #5
    

    最坏情况迭代n elements * 4 Enumerable.Contains * 5*10^6 reference elements in mobile and telephone2 - 仅用于比较!

    Enumerable.Except

    ContactEqualityComparer.cs

    class ContactEqualityComparer : IEqualityComparer<Contact>
    {
      public bool Equals(Contact contact1, Contact contact2)
      {
        if (ReferenceEquals(contact1, contact2))      
          return true;
        else if (ReferenceEquals(contact1, null) || ReferenceEquals(contact2, null))
          return false;
        else if (contact1.MobileNumber.Equals(contact2.MobileNumber, StringComparoison.OrdinalIgnoreCase) 
          && contact1.PhoneNumber2.Equals(contact2.PhoneNumber2, StringComparer.OrdinalIgnoreCase))
          return true;
        else
          return false;
      }
    
      // Will be used by Enumerable.Except to generate item keys 
      // for the lookup table
      public int GetHashCode(Contact contact)
      {
        unchecked
        {
          return ((contact.MobileNumber != null 
            ? contact.MobileNumber.GetHashCode() 
            : 0) * 397) ^ (contact.PhoneNumber2 != null 
              ? contact.PhoneNumber2.GetHashCode() 
              : 0);
        }
      }
    }
    

    Contact.cs
    考虑为每个数据使用两个属性:一个用于计算,一个用于显示,例如MobileNumberMobileNumberDisplay。计算属性应该是数字。

    public class Contact : IEqualityComparer<Contact>
    {
      private string mobileNumber;
      public string MobileNumber
      {
        get => this.mobileNumber;
        set => this.mobileNumber = value.ToPhoneNumber();
      }
    
      private string phoneNumber2;
      public string PhoneNumber2 
      { 
        get => this.phoneNumber2; 
        set => this.phoneNumber2 = value.ToPhoneNumber();
      }
        
      public string ToPhoneNumber(string phoneNumber)
      {
        if (phoneNumber == null || phoneNumber == string.Empty)
            return string.Empty;
    
        return phoneNumber.Replace("(", "").Replace(")", "")
            .Replace(" ", "").Replace("-", "");
      }
    }
    

    示例

    private List<Contact> GetNewContactsNotFoundInCrm(ContactPostModel model) 
    {    
      List<Contact> duplicates = GetAllNumbers();
    
      return model.Contacts
        .Except(duplicates, new ContactEqualityComparer())
        .ToList(); 
    }
    

    【讨论】:

    • 这行不通。它甚至不会编译。 union 是一个IEnumerable&lt;string&gt;,所以它不能作为参数传递给Except(IEnumerable&lt;Contact&gt;)
    • @StriplingWarrior 谢谢。查看原始代码后,我注意到 OP 添加了误导性注释。根据Select 查询,局部变量duplicates 似乎是List&lt;Contact&gt; 而不是List&lt;string&gt;。这将进一步简化代码:与其将Contact 转换为字符串,不如直接对Contact 进行操作以确保转换成本。
    • @StriplingWarrior 无论如何,目标必须是删除 四个! Enumerable.Contains 查询,这将迭代大约 500 万个参考元素每个元素 在源集合中 (n * 4 * 5*10^6)。
    • 我理解您最初的困惑。我同意避免Enumerable.Contains 很重要,我认为可以通过将其更改为HashSet&lt;&gt;.Contains 来实现。一旦我们将该操作转换为O(1) 操作,我们执行 5000 次还是 20000 次这些调用可能并不重要。
    【解决方案3】:

    这里的一个不错的选择是尝试在每个迭代步骤中消除对电话号码转换的需要(调用ToPhoneNumber 方法),方法是同时制作常规号码(您转换 ToPhoneNumber 的那个)、电话号码和手机号码以及telephone2 以相同格式比较的数字。

    对查询进行改进的另一件事是缓存手机和电话号码的呼叫。您可以将他们的计算移到GetNewContactsNotFoundInCrm 方法之外,并且仅在数据有新变化时才获取。

    最后,考虑使用HashSet 来消除重复和快速比较的需要。


    旁注:

    如果您正在处理数据库元素,请考虑将此逻辑移至 SQL Stored Procedure

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-02-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-23
      • 1970-01-01
      相关资源
      最近更新 更多