使用嵌套的Where(LINQ)子句优化数组迭代

时间:2014-12-14 21:49:41

标签: c# .net performance linq linq-to-objects

我正在创建一个具有搜索功能的(C#)工具。搜索类似于“去任何地方”搜索(如ReSharper或VS2013)。

搜索上下文是一个字符串数组,包含前面的所有项目:

private string[] context; // contains thousands of elements

搜索是增量搜索,与用户提供的每个新输入(字符)一起发生。

我使用LINQ Where扩展方法实现了搜索:

// User searched for "c"
var input = "c";
var results = context.Where(s => s.Contains(input));

当用户搜索“ca”时,我试图将之前的结果用作搜索上下文,但是这会导致(我认为?)嵌套的Where迭代,并且运行得不好。想想这样的代码:

// Cache these results.
var results = var results = context.Where(s => s.Contains(input));

// Next search uses the previous search results
var newResults = results.Where(s => s.Contains(input));

有没有办法优化这种情况?

每次搜索都会将IEnumerable转换为数组,导致内存分配过高,运行效果不佳。

3 个答案:

答案 0 :(得分:1)

向用户呈现数千个搜索结果是非常没用的。在将结果呈现给用户之前,您应该在查询中添加“top”(linq Take)语句。

var results = context.Where(s => s.Contains(input)).Take(100);

如果您想向用户展示接下来的100个结果:

var results = context.Where(s => s.Contains(input)).Skip(100).Take(100);

同样只使用原始数组进行所有搜索,不使用嵌套Where,因为除非您实现查询,否则它没有任何好处。

答案 1 :(得分:1)

我有几个有用的要点要添加,太多的评论。

首先,我同意您应该从.take(100)开始的其他评论,减少加载时间。更好的是,当时添加一个结果:

var results = context.Where(s => s.Contains(input));
var resultEnumerator = result.GetEnumerator()

遍历resultEnumerator以显示一个结果,当屏幕已满或启动新搜索时停止。

其次,限制你的输入。如果用户撰写Hello,则您不想针对HHeHelHellHello进行5次搜索,你想只搜索Hello。当用户稍后添加world时,可能值得采用旧结果并将Hello world添加到where子句中。

results = results.Where(s => s.Contains(input));
resultEnumerator = result.GetEnumerator()

当然,当用户添加新文本时,取消当前正在进行的结果。

使用Rx,节流部分很简单,你可以得到这样的结果:

var result = context.AsEnumerable();
var oldStr = "";
var resultEnumerator = result.GetEnumerator();
Observable.FromEventPattern(h => txt.TextChanged += h, h => txt.TextChanged -= h)
         .Select(s => txt.Text)
         .DistinctUntilChanged().Throttle(TimeSpan.FromMilliseconds(300))
         .Subscribe(s =>
         {
             if (s.Contains(oldStr))
                 result = result.Where(t => t.Contains(s));
             else
                 result = context.Where(t => t.Contains(s));
             resultEnumerator = result.GetEnumerator();
             oldStr = s;
             // and probably start iterating resultEnumerator again,
             // but perhaps not on this thread.
         });

答案 2 :(得分:0)

如果您关注的是alloc并且您不想编写trie实现或使用第三方代码,那么您应该先连续分区您的上下文数组,以便在前面聚集匹配的条目。不是LINQ-ish,但速度快,内存成本为零。

分区扩展方法,基于C ++' std::partition

/// <summary>
/// All elements for which predicate is true are moved to the front of the array.
/// </summary>
/// <param name="start">Index to start with</param>
/// <param name="end">Index to end with</param>
/// <param name="predicate"></param>
/// <returns>Index of the first element for which predicate returns false</returns>
static int Partition<T>(this T[] array, int start, int end, Predicate<T> predicate)
{
    while (start != end)
    {
        // move start to the first not-matching element
        while ( predicate(array[start]) )
        {
            if ( ++start == end )
            {
                return start;
            }
        }

        // move end to the last matching element
        do
        {
            if (--end == start)
            {
                return start;
            }
        }
        while (!predicate(array[end]));

        // swap the two
        var temp = array[start];
        array[start] = array[end];
        array[end] = temp;

        ++start;
    }
    return start;
}

所以现在你需要存储最后一个分区索引,它应该用context长度初始化:

private int resultsCount = context.Length;

然后,对于输入的每个更改,您可以运行增量:

resultsCount = context.Partition(0, resultsCount, s => s.Contains(input));

每次这只会检查以前没有被过滤掉的元素,这正是你所追求的。

对于每次非增量更改,您需要将resultsCount重置为原始值。

您可以使用方便的调试器和LINQ友好方式公开结果:

public IEnumerable<string> Matches
{
    get { return context.Take(resultsCount); }
}