【发布时间】:2012-01-03 02:01:28
【问题描述】:
我有两个类执行特定日期的日期日期范围数据获取。
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 版本慢了大约 5 倍。当我在本地存储项目而不使用ToList 枚举它们时,这将是有意义的。这会使它变得更慢,因为每次调用FirstOrDefault,linq 也会执行OrderByDescending。但事实并非如此,所以我真的不知道发生了什么。 Linq 的执行应该与迭代非常相似。
这是衡量我的时间的代码摘录
IList<RangeItem> ranges = GenerateRanges(); // returns List<T>
var iterLookup = new IterationLookup<RangeItems>(ranges, r => r.Id);
var linqLookup = new LinqLookup<RangeItems>(ranges, r => r.Id);
Stopwatch timer = new Stopwatch();
timer.Start();
for(int i = 0; i < 1000000; i++)
{
iterLookup.GetItem(GetRandomDay());
}
timer.Stop();
// display elapsed time
timer.Restart();
for(int i = 0; i < 1000000; i++)
{
linqLookup.GetItem(GetRandomDay());
}
timer.Stop();
// display elapsed time
为什么我知道它应该表现更好?因为当我在不使用这些查找类的情况下编写非常相似的代码时,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
我认为这是非常相似的代码。 FirstOrDefault 据我所知,它也只迭代直到它到达一个有效的项目或直到它到达末尾。这在某种程度上与foreach 和break 相同。
但即使是迭代类的性能也比我简单的 foreach 迭代循环更差,这也是一个谜,因为与直接访问相比,它的所有开销都是对类中方法的调用。
问题
我在我的 LINQ 类中做错了什么,导致它执行得非常慢?
我在迭代类中做错了什么,所以它的执行速度是直接foreach 循环的两倍?
正在测量哪些时间?
我执行以下步骤:
- 生成范围(如下结果所示)
- 为 IterationLookup、LinqLookup(以及我优化的日期范围类 BitCountLookup,这里不讨论)创建对象实例
- 使用先前实例化的 IterationLookup 类启动计时器并在最大日期范围内(如结果中所示)内的随机天执行 100 万次查找。
- 使用先前实例化的 LinqLookup 类启动计时器并在最大日期范围内(如结果中所示)内的随机天执行 100 万次查找。
- 使用手动 foreach+break 循环和 Linq 调用启动计时器并执行 100 万次查找(6 次)。
如您所见,没有测量对象实例化。
附录 I:超过百万次查找的结果
这些结果中显示的范围不重叠,这应该使两种方法更加相似,以防 LINQ 版本在成功匹配时不会中断循环(它很可能会这样做)。
生成范围: ID 范围 000000000111111111122222222223300000000011111111112222222222 123456789012345678901234567890112345678901234567890123456789 09 22.01.-30.01。 |-------| 08 14.01.-16.01。 |-| 07 16.02.-19.02。 |--| 06 15.01.-17.01。 |-| 05 19.02.-23.02。 |---| 04 01.01.-07.01.|-----| 03 02.01.-10.01。 |-------| 02 11.01.-13.01。 |-| 01 16.01.-20.01。 |---| 00 29.01.-06.02。 |-------| 查找类... - 迭代:1028ms - Linq: 4517ms !!!这就是问题!!! - 位计数器:401 毫秒 手动循环... - 迭代:786ms - Linq:981ms - 迭代:787ms - Linq:996ms - 迭代:787ms - Linq:977ms - 迭代:783ms - Linq:979ms附录 II:GitHub:自己测试的 Gist 代码
我已经提出了一个要点,因此您可以自己获取完整的代码并查看发生了什么。创建一个 Console 应用程序并将 Program.cs 复制到其中并添加属于此要点的其他文件。
抓住它here。
附录 III:最终想法和测量测试
最有问题的当然是 LINQ implementationatino,它非常慢。事实证明,这与委托编译器优化有关。 LukeH provided the best and most usable solution 这实际上让我尝试了不同的方法。我在 GetItem 方法(或在 Gist 中称为 GetPointData)尝试了各种不同的方法:
-
大多数开发人员会做的通常方式(并且也在 Gist 中实现,并且在结果显示这不是最好的方式后没有更新):
return this.items.FirstOrDefault(item => item.IsWithinRange(day)); -
通过定义一个局部谓词变量:
Func<TItem, bool> predicate = item => item.IsWithinRange(day); return this.items.FirstOrDefault(predicate); -
本地谓词构建器:
Func<DateTime, Func<TItem, bool>> builder = d => item => item.IsWithinRange(d); return this.items.FirstOrDefault(builder(day)); -
本地谓词构建器和本地谓词变量:
Func<DateTime, Func<TItem, bool>> builder = d => item => item.IsWithinRange(d); Func<TItem, bool> predicate = builder(day); return this.items.FirstOrDefault(predicate); -
类级别(静态或实例)谓词构建器:
return this.items.FirstOrDefault(classLevelBuilder(day)); -
外部定义的谓词并作为方法参数提供
public TItem GetItem(Func<TItem, bool> predicate) { return this.items.FirstOrDefault(predicate); }在执行这个方法时,我也采取了两种方法:
-
直接在
for循环内的方法调用处提供谓词:for (int i = 0; i < 1000000; i++) { linqLookup.GetItem(item => item.IsWithinRange(GetRandomDay())); } -
在
for循环之外定义的谓词生成器:Func<DateTime, Func<Ranger, bool>> builder = d => r => r.IsWithinRange(d); for (int i = 0; i < 1000000; i++) { linqLookup.GetItem(builder(GetRandomDay())); }
-
结果 - 什么表现最好
为了在使用迭代类时进行比较,它大约需要。 770ms 对随机生成的范围执行 100 万次查找。
3 本地谓词生成器被证明是经过最佳编译器优化的,因此它的执行速度几乎与通常的迭代一样快。 800 毫秒。
6.2 谓词构建器在
for循环之外定义:885msfor循环中定义的 6.1 谓词:1525ms- 所有其他时间都在 4200ms - 4360ms 之间,因此被视为不可用。
因此,每当您在外部频繁调用的方法中使用谓词时,请定义一个构建器并执行它。这将产生最好的结果。
对此我最大的惊喜是委托(或谓词)可能会耗费这么多时间。
【问题讨论】:
-
您是在 IDE 之外的 Release 构建中计时吗?
-
我认为性能差异来自 BCL 确定给定集合是否实现
IList<T>的强制转换操作。这种优化是有问题的,因为你总是想得到第一个项目(而不是在做LastOrDefault时)。 -
@JamesMichaelHare:这些都处于没有附加调试器的调试模式。让我也检查一下发布模式……好吧。结果如下:Release 执行速度更快,但在 5 倍因子方面仍然存在异常情况。
-
@tahir:FirstOrDefault() 如果未找到则不会抛出。仅抛出空参数。
-
@RobertKoritnik:如果你可以发布整个代码 sn-p 包括数据生成。我们没有看到 5 倍的差异,因此生成数据或运行测试的方式可能存在一些问题。
标签: c# performance linq .net-4.0 foreach