C# foreach&x2B;中断与linq FirstOrDefault性能差异

C# foreach&x2B;中断与linq FirstOrDefault性能差异,c#,performance,linq,.net-4.0,foreach,C#,Performance,Linq,.net 4.0,Foreach,我有两个类执行特定日期的日期范围数据获取 public class IterationLookup<TItem> { private IList<Item> items = null; public IterationLookup(IEnumerable<TItem> items, Func<TItem, TKey> keySelector) { this.items = items.OrderByDesc

我有两个类执行特定日期的日期范围数据获取

public class IterationLookup<TItem>
{
    private IList<Item> items = null;

    public IterationLookup(IEnumerable<TItem> items, Func<TItem, TKey> keySelector)
    {
        this.items = items.OrderByDescending(keySelector).ToList();
    }

    public TItem GetItem(DateTime day)
    {
        foreach(TItem i in this.items)
        {
           if (i.IsWithinRange(day))
           {
               return i;
           }
        }
        return null;
    }
}


public class LinqLookup<TItem>
{
    private IList<Item> items = null;

    public IterationLookup(IEnumerable<TItem> items, Func<TItem, TKey> keySelector)
    {
        this.items = items.OrderByDescending(keySelector).ToList();
    }

    public TItem GetItem(DateTime day)
    {
        return this.items.FirstOrDefault(i => i.IsWithinRange(day));
    }
}
为什么我知道它应该表现得更好?因为当我在不使用这些查找类的情况下编写非常类似的代码时,Linq的执行与
foreach
迭代非常类似

// continue from previous code block

// items used by both order as they do in classes as well
IList<RangeItem> items = ranges.OrderByDescending(r => r.Id).ToList();

timer.Restart();
for(int i = 0; i < 1000000; i++)
{
    DateTime day = GetRandomDay();
    foreach(RangeItem r in items)
    {
        if (r.IsWithinRange(day))
        {
            // RangeItem result = r;
            break;
        }
    }
}    
timer.Stop();
// display elapsed time

timer.Restart();
for(int i = 0; i < 1000000; i++)
{
   DateTime day = GetRandomDay();
   items.FirstOrDefault(i => i.IsWithinRange(day));
}
timer.Stop();
// display elapsed time
附录二:GitHub:供您自己测试的Gist代码 我已经提出了一个要点,这样你就可以自己得到完整的代码,看看发生了什么。创建一个控制台应用程序并将Program.cs复制到其中,并添加本要点中的其他文件

抓住它

附录三:最终想法和测量测试 当然,最有问题的是LINQ实现非常慢。事实证明,这一切都与委托编译器优化有关。这实际上让我尝试了不同的方法。我在
GetItem
方法中尝试了各种不同的方法(或者在Gist中称为
GetPointData
):

  • 大多数开发人员通常会这样做(也是在Gist中实现的,并且在结果显示这不是最好的方式后没有更新):

  • 通过定义局部谓词变量:

    Func<TItem, bool> predicate = item => item.IsWithinRange(day);
    return this.items.FirstOrDefault(predicate);
    
    Func<DateTime, Func<TItem, bool>> builder = d => item => item.IsWithinRange(d);
    Func<TItem, bool> predicate = builder(day);
    return this.items.FirstOrDefault(predicate);
    
  • 外部定义的谓词,并作为方法参数提供

    public TItem GetItem(Func<TItem, bool> predicate)
    {
        return this.items.FirstOrDefault(predicate);
    }
    
  • 在循环的
    外部定义的谓词生成器:

    for (int i = 0; i < 1000000; i++)
    {
        linqLookup.GetItem(item => item.IsWithinRange(GetRandomDay()));
    }
    
    Func<DateTime, Func<Ranger, bool>> builder = d => r => r.IsWithinRange(d);
    for (int i = 0; i < 1000000; i++)
    {
        linqLookup.GetItem(builder(GetRandomDay()));
    }
    
    Func builder=d=>r=>r.IsWithinRange(d);
    对于(int i=0;i<1000000;i++)
    {
    GetItem(生成器(GetRandomDay());
    }
    
  • 结果-什么表现最好 为了在使用迭代类时进行比较,对随机生成的范围执行100万次查找大约需要770ms

  • 3本地谓词生成器被证明是最好的编译器优化,所以它的执行速度几乎和通常的迭代一样快800ms
  • 6.2在
    外为
    循环定义的谓词生成器:885ms
  • 6.1在
    中为
    循环定义的谓词:1525ms
  • 所有其他时间介于4200ms到4360ms之间,因此被视为不可用
  • 所以,每当您在外部频繁调用的方法中使用谓词时,请定义一个生成器并执行它。这将产生最好的结果


    关于这一点,我最惊讶的是委托(或谓词)可能会耗费这么多时间。

    有时LINQ看起来比较慢,因为循环中委托的生成(特别是不明显的循环方法调用)会增加时间。相反,您可能需要考虑将查找器移出类以使其更通用(如您的密钥选择器在构造中):

    公共类LinqLookup
    {
    私有IList项=null;
    公共迭代查找(IEnumerable项,Func键选择器)
    {
    this.items=items.OrderByDescending(keySelector.ToList();
    }
    公共TItem GetItem(函数选择器)
    {
    返回此.items.FirstOrDefault(选择器);
    }
    }
    
    由于在迭代代码中不使用lambda,这可能有点不同,因为它必须在循环的每个过程中创建委托。通常,这段时间对于每天的编码来说是无关紧要的,调用委托的时间并不比其他方法调用花费更多,只是在一个紧密循环中创建委托会增加一点点额外的时间

    在这种情况下,由于类的委托从不更改,因此可以在循环代码之外创建它,这样会更有效

    更新

    实际上,即使没有任何优化,在我的机器上以发布模式编译,我也看不到5倍的差异。我刚刚对一个只有
    DateTime
    字段的
    项目执行了1000000次查找,列表中有5000个项目。当然,我的数据等是不同的,但你可以看到,当你提取出代理时,时间实际上非常接近:

    迭代:14279毫秒,0.014279毫秒/次呼叫

    linq w opt:17400毫秒,0.0174毫秒/通话


    这些时间差异非常小,值得使用LINQ提高可读性和可维护性。但是我没有看到5倍的差异,这让我相信在您的测试线束中有一些我们没有看到的东西。

    假设您有这样一个循环:

    for (int counter = 0; counter < 1000000; counter++)
    {
        // execute this 1M times and time it 
        DateTime day = GetRandomDay(); 
        items.FirstOrDefault(i => i.IsWithinRange(day)); 
    }
    
    for(int计数器=0;计数器<1000000;计数器++)
    {
    //执行此命令1M次并计时
    DateTime day=GetRandomDay();
    items.first或default(i=>i.IsWithinRange(day));
    }
    
    此循环将创建1000000个lambda对象,以便调用
    i.IsWithinRange
    访问
    day
    。在每次创建lambda之后,调用
    i.IsWithinRange
    的委托平均被调用1000000个*
    项.Length
    /2次。这两个因素都不存在于
    foreach
    循环中,这就是显式循环更快的原因。

    此外,我可以确认,差异似乎是由于每次调用
    GetPointData
    都要重新构造委托的成本造成的

    如果我在
    IterationRangeLookupSingle
    类中的
    GetPointData
    方法中添加一行,那么它的爬行速度将与
    LinqRangeLookupSingle
    相同。试试看:

    // in IterationRangeLookupSingle<TItem, TKey>
    public TItem GetPointData(DateTime point)
    {
        // just a single line, this delegate is never used
        Func<TItem, bool> dummy = i => i.IsWithinRange(point);
    
        // the rest of the method remains exactly the same as before
        // ...
    }
    

    您是否在IDE之外的发布版本上对此进行计时?我认为性能差异来自BCL执行的强制转换操作,以确定给定集合是否实现了
    IList
    。这种优化是有问题的,因为您总是希望获得第一项(而不是执行
    LastOrDefault
    )。@JamesMichaelHare:这些都处于调试模式,没有附加调试程序。让我也检查一下发布模式。。。好啊以下是结果:Release执行得更快,但异常仍然存在于5倍因子中。@tahir:FirstOrDefault()如果未找到,则不会抛出。只扔
    public TItem GetItem(Func<TItem, bool> predicate)
    {
        return this.items.FirstOrDefault(predicate);
    }
    
    for (int i = 0; i < 1000000; i++)
    {
        linqLookup.GetItem(item => item.IsWithinRange(GetRandomDay()));
    }
    
    Func<DateTime, Func<Ranger, bool>> builder = d => r => r.IsWithinRange(d);
    for (int i = 0; i < 1000000; i++)
    {
        linqLookup.GetItem(builder(GetRandomDay()));
    }
    
    public class LinqLookup<TItem, TKey>
    {
        private IList<Item> items = null;
    
        public IterationLookup(IEnumerable<TItem> items, Func<TItem, TKey> keySelector)
        {
            this.items = items.OrderByDescending(keySelector).ToList();
        }
    
        public TItem GetItem(Func<TItem, TKey> selector)
        {
            return this.items.FirstOrDefault(selector);
        }
    }
    
    for (int counter = 0; counter < 1000000; counter++)
    {
        // execute this 1M times and time it 
        DateTime day = GetRandomDay(); 
        items.FirstOrDefault(i => i.IsWithinRange(day)); 
    }
    
    // in IterationRangeLookupSingle<TItem, TKey>
    public TItem GetPointData(DateTime point)
    {
        // just a single line, this delegate is never used
        Func<TItem, bool> dummy = i => i.IsWithinRange(point);
    
        // the rest of the method remains exactly the same as before
        // ...
    }
    
    // in LinqRangeLookupSingle<TItem, TKey>
    public TItem GetPointData(DateTime point)
    {
        Func<DateTime, Func<TItem, bool>> builder = x => y => y.IsWithinRange(x);
        Func<TItem, bool> predicate = builder(point);
    
        return this.items.FirstOrDefault(predicate);
    }