使用Any()时优化OrderBy()

时间:2012-01-24 00:56:36

标签: linq optimization linq-to-objects

所以我有一个相当标准的LINQ-to-Object设置。

var query = expensiveSrc.Where(x=> x.HasFoo)
                        .OrderBy(y => y.Bar.Count())
                        .Select(z => z.FrobberName);    

// ...

if (!condition && !query.Any())
 return; // seems to enumerate and sort entire enumerable 

// ...

foreach (var item in query)
   // ...

这列举了两次。这很糟糕。

var queryFiltered = expensiveSrc.Where(x=> x.HasFoo);

var query = queryFiltered.OrderBy(y => y.Bar.Count())
                         .Select(z => z.FrobberName); 

if (!condition && !queryFiltered.Any())
   return;

// ...

foreach (var item in query)
   // ...

有效,但还有更好的方法吗?

是否有任何非疯狂的方式来“启发”Any()绕过非必需的操作?我想我记得这种优化进入了EduLinq。

7 个答案:

答案 0 :(得分:9)

为什么不摆脱冗余:

if (!query.Any())
 return; 

它似乎没有任何目的 - 即使没有它,如果查询没有产生任何结果,foreach的主体也不会执行。因此,通过Any()签入,您可以在快速路径中保存任何内容,并在慢速路径中枚举两次。

另一方面,如果你必须知道在循环结束后是否找到了的结果,你也可以只使用一个标志:

bool itemFound = false;

foreach (var item in query)
{
    itemFound = true;
    ... // Rest of the loop body goes here.
}

if(itemFound)
{
   // ...
}

如果你真的担心循环体中的冗余标志设置,你可以直接使用枚举器:

using(var erator = query.GetEnumerator())
{
    bool itemFound = erator.MoveNext();

    if(itemFound)
    {
       do
       {
           // Do something with erator.Current;
       } while(erator.MoveNext())
    }

   // Do something with itemFound
}

答案 1 :(得分:2)

编辑(修订):此答案回顾了执行两次查询的问题,我认为这是关键问题。见下文原因:

Any()更聪明是只有Linq实施者可以做的事情,IMO ...或者使用反射会有一些肮脏的冒险。

使用如下所示的类,您可以缓存原始可枚举的输出,并将其枚举两次:

public class CachedEnumerable<T> 
{
    public CachedEnumerable(IEnumerable<T> enumerable)
    {
        _source = enumerable.GetEnumerator();
    }

    public IEnumerable<T> Enumerate()
    {
        int itemIndex = 0;
        while (true)
        {
            if (itemIndex < _cache.Count)
            {
                yield return _cache[itemIndex];
                itemIndex++;
                continue;
            }

            if (!_source.MoveNext())
                yield break;

            var current = _source.Current;
            _cache.Add(current);
            yield return current;
            itemIndex++;                 
        }

    }

    private List<T> _cache = new List<T>();
    private IEnumerator<T> _source;
}

这样你就可以保持LINQ的惰性,保持代码的可读性和通用性。直接使用IEnumerator<>会慢一些。有很多机会可以扩展和优化这个类,例如丢弃旧项目的政策,摆脱协程等。但是我认为这超出了这个问题的范围。

哦,这个类现在不是线程安全的。这并没有被问到,但我可以想象有人在尝试。如果源枚举没有线程关联,我认为可以很容易地添加它。

为什么这会是最佳的?

让我们考虑两个可能性:枚举可能包含元素,也可能不包含元素。

  • 如果它包含元素,则此方法最适合查询 只运行一次。
  • 如果它不包含任何元素,那么你会受到诱惑 在添加时消除OrderBy和Select部分查询 没有价值。但是..如果在Where()子句之后有零个项目,则排序零项,这将花费零时间(好吧,差不多)。 Select()子句也是如此。

如果这还不够快怎么办?在这种情况下,我的策略是绕过Linq。现在,我真的很喜欢linq,但它的优雅是有代价的。因此,对于每100次使用Linq,通常会有一两个计算对于执行非常快速非常重要,我使用旧的for循环和列表编写。掌握技术的一部分是认识到不合适的地方。 Linq也不例外。

答案 2 :(得分:2)

可以从可枚举中提取的信息不多,所以将查询转换为IQueryable可能会更好吗?这个Any扩展方法向下遍历其表达式树,跳过所有不相关的操作,然后将重要分支转换为可以调用以获得优化IQueryable的委托。标准Any方法明确应用于它以避免递归。不确定极端情况,也许缓存已编译的查询是有意义的,但是像你这样的简单查询似乎有效。

static class QueryableHelper {
    public static bool Any<T>(this IQueryable<T> source) {
        var e = source.Expression;
        while (e is MethodCallExpression) {
            var mce = e as MethodCallExpression;
            switch (mce.Method.Name) {
                case "Select":
                case "OrderBy":
                case "ThenBy": break;
                default: goto dun;
            }
            e = mce.Arguments.First();
        }
        dun:
        var d = Expression.Lambda<Func<IQueryable<T>>>(e).Compile();
        return Queryable.Any(d());
    }
}

查询本身必须像这样修改:

var query = expensiveSrc.AsQueryable()
                        .Where(x=> x.HasFoo)
                        .OrderBy(y => y.Bar.Count())
                        .Select(z => z.FrobberName); 

答案 3 :(得分:2)

  

是否有任何非疯狂的方式来“启发”Any()绕过非必需的操作?我想我记得这种优化进入了EduLinq。

好吧,我不会忽视提到Edulinq的任何问题:)

在这种情况下,Edulinq可能比LINQ to Objects更快,因为它的OrderBy实现尽可能地懒惰 - 它只需要排序它需要的数量,以便检索它返回的元素

然而,从根本上说,仍然必须在返回任何内容之前读取整个序列。毕竟,序列中的最后一个元素可能是第一个必须返回的元素。

如果您控制整个堆栈,可以Any()检测到它已在您的“已知”IOrderedEnumerable实施中被调用,并直接转到原始来源。请注意,此确实会在观察到的行为中产生更改 - 如果迭代整个序列会引发异常(或者有任何其他副作用),则优化会丢失该副作用。当然,你可以说这没关系 - 在LINQ中算作“有效”的优化是一个非常棘手的领域。

另一种非常可怕但可以解决这一特定问题的可能性是使IOrderedEnumerable返回的迭代器只从源中获取MoveNext()的第一个值。这对于Any的正常实现来说已经足够了,在那时我们需要知道第一个元素是什么。我们可以推迟实际排序,直到第一次使用Current属性。

这是一个非常特殊的案例优化 - 而且我需要谨慎实施。我认为Ani的方法更好 - 只是使用query使用foreach进行迭代,如果查询结果为空,则永远不会进入循环体。

答案 4 :(得分:1)

试试这个:

var items = expensiveSrc.Where(x=> x.HasFoo)
                        .OrderBy(y => y.Bar.Count())
                        .Select(z => z.FrobberName).ToList();   

// ...

if (!condition && items.Count == 0)
 return; // Just check the count

// ...

foreach (var item in items)
   // ...

查询只执行一次。

答案 5 :(得分:1)

  
    

但是我丢失了流量/延迟加载,这是linq的一半

  

延迟加载(延迟执行)和2个具有不同结果的LINQ查询无法优化(减少)为1次查询执行。

答案 6 :(得分:0)

为什么不使用.ToArray()

var query = expensiveSrc.Where(x=> x.HasFoo)
                    .OrderBy(y => y.Bar.Count())
                    .Select(z => z.FrobberName).ToArray();    

如果没有元素,排序和选择不应给予太多开销。如果你正在排序,那么无论如何你都需要一个存储数据的缓存,所以开销.ToArray产生的开销不应该那么多。 如果你反编译OrderedEnumerable类,你会发现有一个包含引用的int []数组,所以你只需要使用.ToArray(或.ToList)创建一个新的引用数组。

BUT 如果expensiveSrc来自数据库,其他策略可能会更好。如果可以在数据库中完成排序,这会给你带来很大的开销,因为数据存储了两次。