【问题标题】:LINQ Performance for Large Collections大型集合的 LINQ 性能
【发布时间】:2009-03-25 17:18:29
【问题描述】:

我有大量按字母顺序排序的字符串(最多 1M)。我已经使用 HashSet、SortedDictionary 和 Dictionary 对这个集合进行了 LINQ 查询试验。我正在静态缓存集合,它的大小高达 50MB,并且我总是针对缓存的集合调用 LINQ 查询。我的问题如下:

无论集合类型如何,性能都比 SQL 差很多(最多 200 毫秒)。对底层 SQL 表执行类似查询时,性能要快得多(5-10 毫秒)。我已按如下方式实现了我的 LINQ 查询:

public static string ReturnSomething(string query, int limit)
{
  StringBuilder sb = new StringBuilder();
  foreach (var stringitem in MyCollection.Where(
      x => x.StartsWith(query) && x.Length > q.Length).Take(limit))
  {
      sb.Append(stringitem);
  }

  return sb.ToString();
}

据我了解,HashSet、Dictionary 等使用二叉树搜索而不是标准枚举来实现查找。对于高级集合类型的高性能 LINQ 查询,我有哪些选择?

【问题讨论】:

    标签: c# .net linq performance


    【解决方案1】:

    在您当前的代码中,您没有使用 Dictionary / SortedDictionary / HashSet 集合的任何特殊功能,您使用它们的方式与使用 List 的方式相同。这就是为什么您看不到任何性能差异的原因。

    如果您使用字典作为索引,其中字符串的前几个字符是键,字符串列表是值,您可以从搜索字符串中挑选出整个字符串集合中可能存在的一小部分匹配。

    我编写了下面的课程来测试这一点。如果我用一百万个字符串填充它并使用八个字符的字符串进行搜索,它会在大约 3 毫秒内遍历所有可能的匹配项。使用一个字符串进行搜索是最坏的情况,但它会在大约 4 毫秒内找到前 1000 个匹配项。查找一个字符串的所有匹配项大约需要 25 毫秒。

    该类为 1、2、4 和 8 个字符键创建索引。如果您查看您的特定数据和搜索内容,您应该能够选择创建哪些索引来针对您的条件进行优化。

    public class IndexedList {
    
        private class Index : Dictionary<string, List<string>> {
    
            private int _indexLength;
    
            public Index(int indexLength) {
                _indexLength = indexLength;
            }
    
            public void Add(string value) {
                if (value.Length >= _indexLength) {
                    string key = value.Substring(0, _indexLength);
                    List<string> list;
                    if (!this.TryGetValue(key, out list)) {
                        Add(key, list = new List<string>());
                    }
                    list.Add(value);
                }
            }
    
            public IEnumerable<string> Find(string query, int limit) {
                return
                    this[query.Substring(0, _indexLength)]
                    .Where(s => s.Length > query.Length && s.StartsWith(query))
                    .Take(limit);
            }
    
        }
    
        private Index _index1;
        private Index _index2;
        private Index _index4;
        private Index _index8;
    
        public IndexedList(IEnumerable<string> values) {
            _index1 = new Index(1);
            _index2 = new Index(2);
            _index4 = new Index(4);
            _index8 = new Index(8);
            foreach (string value in values) {
                _index1.Add(value);
                _index2.Add(value);
                _index4.Add(value);
                _index8.Add(value);
            }
        }
    
        public IEnumerable<string> Find(string query, int limit) {
            if (query.Length >= 8) return _index8.Find(query, limit);
            if (query.Length >= 4) return _index4.Find(query,limit);
            if (query.Length >= 2) return _index2.Find(query,limit);
            return _index1.Find(query, limit);
        }
    
    }
    

    【讨论】:

    • 太棒了!高性能,正是我想要的。你会推荐这种方法(当然是修改过的)来查询非字符串对象集合的属性吗?
    • 是的,您可以使 Index 类通用并使用 HashSet 而不是 List,然后您可以为不同的属性创建索引并与 HashSet 相交以缩小要搜索的项目。
    • 比 indexLength 更短的字符串怎么办 - Add() 不会存储它们并且 Find() 不会找到它们?
    • @Sam:没错。 n 个字符的索引将仅包含至少为 n 个字符的字符串。这就是为什么最好像示例中那样创建不同长度的索引。
    • 为什么投反对票?如果你不解释你认为错的地方是什么,它就无法改进答案。
    【解决方案2】:

    我敢打赌,您在列上有一个索引,因此 SQL Server 可以在 O(log(n)) 操作而不是 O(n) 中进行比较。要模仿 SQL 服务器的行为,请使用排序集合并查找所有字符串 s,使得 s >= 查询,然后查看值,直到找到不以 s 开头的值,然后对这些值进行额外的过滤。这就是所谓的范围扫描 (Oracle) 或索引查找 (SQL server)。

    这是一些示例代码,由于我没有测试它,很可能会进入无限循环或出现一次性错误,但你应该明白了。

    // Note, list must be sorted before being passed to this function
    IEnumerable<string> FindStringsThatStartWith(List<string> list, string query) {
        int low = 0, high = list.Count - 1;
        while (high > low) {
            int mid = (low + high) / 2;
            if (list[mid] < query)
                low = mid + 1;
            else
                high = mid - 1;
        }
    
        while (low < list.Count && list[low].StartsWith(query) && list[low].Length > query.Length)
            yield return list[low];
            low++;
        }
    }
    

    【讨论】:

      【解决方案3】:

      如果您正在做“开始于”,您只关心序数比较,并且您可以对集合进行排序(再次按序数顺序),那么我建议您将值放在列表中。然后,您可以二分查找以找到以正确前缀开头的第一个值,然后沿着列表线性向下生成结果,直到 以正确前缀开头的第一个值。

      事实上,您可能可以对不以前缀开头的第一个值进行另一次二进制搜索,因此您将有一个起点和一个终点。然后,您只需将长度标准应用于该匹配部分。 (我希望如果它是明智的数据,前缀匹配将摆脱大多数候选值。)找到不以前缀开头的第一个值的方法是搜索字典顺序的第一个值不 - 例如以“ABC”为前缀,搜索“ABD”。

      这些都不使用 LINQ,而且都非常适合您的特定情况,但它应该可以工作。如果有任何不妥之处,请告诉我。

      【讨论】:

        【解决方案4】:

        如果您尝试优化查找具有给定前缀的字符串列表,您可能想看看在 C# 中实现 Trie(不要误认为是常规的 tree)数据结构。

        Tries 提供了非常快速的前缀查找,并且与用于此类操作的其他数据结构相比,内存开销非常小。

        关于一般的 LINQ to Objects。与 SQL 相比,速度降低并不罕见。网络是littered with articles 分析其性能。

        【讨论】:

          【解决方案5】:

          仅查看您的代码,我会说您应该重新排序比较以在使用布尔运算符时利用短路:

          foreach (var stringitem in MyCollection.Where(
              x => x.Length > q.Length && x.StartsWith(query)).Take(limit))
          

          长度的比较总是一个 O(1) 操作(因为长度被存储为字符串的一部分,它不会每次都计算每个字符),而对 StartsWith 的调用将是一个 O(N) 操作,其中 N 是查询的长度(或字符串的长度,以较小者为准)。

          通过在调用 StartsWith 之前进行长度比较,如果比较失败,您可以节省一些额外的周期,这些周期在处理大量项目时可能会加起来。

          我认为查找表在这里不会对您有所帮助,因为当您比较整个键而不是键的部分时,查找表是很好的,就像您对 StartsWith 的调用一样。

          相反,您最好使用根据列表中单词中的字母拆分的树结构。

          但是,此时,您实际上只是在重新创建 SQL Server 正在执行的操作(在索引的情况下),而这只是您的重复工作。

          【讨论】:

            【解决方案6】:

            我认为问题在于 Linq 无法使用您的序列已经排序的事实。尤其是它不知道,应用StartsWith函数会保留顺序。

            我建议将List.BinarySearch 方法与仅比较第一个查询字符的IComparer&lt;string&gt; 一起使用(这可能很棘手,因为不清楚,查询字符串是始终是第一个还是()) 的第二个参数。

            您甚至可以使用标准字符串比较,因为 BinarySearch 返回一个负数,您可以对它进行补码(使用 ~)以获得大于查询的第一个元素的索引。

            然后,您必须从返回的索引开始(双向!)以查找与您的查询字符串匹配的所有元素。

            【讨论】:

              猜你喜欢
              • 2019-09-17
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-08-05
              • 1970-01-01
              • 1970-01-01
              • 2015-02-20
              • 1970-01-01
              相关资源
              最近更新 更多