最近有件事引起了我的好奇心。
当他们做同样的事情时,为什么该Enumerable.Any(Func<TSource, bool> predicate)方法比手动 foreach慢得多?
我一直在搞乱一些基准并想到了这一点。我正在检查List<int>大约位于列表一半的内容和项目。
以下是我对几种不同大小的列表的测试结果:
项目:1 000,搜索项目:543
| 方法 | 意思是 | 比率 | 已分配 | 分配比例 |
|---|---|---|---|---|
| 福里奇 | 838.3纳秒 | 1.00 | - | 不适用 |
| 任何 | 3,348.8 纳秒 | 4.05 | 40乙 | 不适用 |
项目:10 000,搜索项目:5 432
| 方法 | 意思是 | 比率 | 已分配 | 分配比例 |
|---|---|---|---|---|
| 福里奇 | 7.988 我们 | 1.00 | - | 不适用 |
| 任何 | 30.991 我们 | 3.88 | 40乙 | 不适用 |
项目:100 000,搜索项目:54 321
| 方法 | 意思是 | 比率 | 已分配 | 分配比例 |
|---|---|---|---|---|
| 福里奇 | 82.35 我们 | 1.00 | - | 不适用 |
| 任何 | 328.86 我们 | 4.00 | 40乙 | 不适用 |
有两个基准:
foreach带有if语句的手册Any方法(变成Enumerable.Any)这是我的基准测试代码(使用 BenchmarkDotNet,在发布模式下运行的 .NET 6.0 控制台应用程序):
[MemoryDiagnoser(displayGenColumns: false)]
[HideColumns("Error", "StdDev", "RatioSD")]
public class Benchmarks
{
private readonly List<int> _items;
private readonly Func<int, bool> _filter;
public Benchmarks()
{
_items = Enumerable.Range(1, 10_000).ToList();
_filter = x => x == 5432;
}
[Benchmark(Baseline = true)]
public bool Foreach()
{
if (_items is null)
{
throw new ArgumentNullException(nameof(_items));
}
if (_filter is null)
{
throw new ArgumentNullException(nameof(_filter));
}
foreach (var item in _items)
{
if (_filter(item))
{
return true;
}
}
return false;
}
[Benchmark]
public bool Any()
{
return _items.Any(_filter);
}
}
Run Code Online (Sandbox Code Playgroud)
Any方法慢了 4 倍,并且分配了一点内存,尽管我尽了最大努力来优化它。
我尝试通过将谓词 ( ) 缓存在变量 ( ) 中来使Any方法更快。然而,它仍然分配了40B,我不知道为什么......Func<int, bool>_filter
反编译后,Any方法变成Enumerable.Any(Func<TSource, bool> predicate)方法:
public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
if (source == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
if (predicate == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.predicate);
}
foreach (TSource element in source)
{
if (predicate(element))
{
return true;
}
}
return false;
}
Run Code Online (Sandbox Code Playgroud)
Any方法与Foreach方法有何不同?只是好奇...
编译器可以优化内部 foreach 的工作List<T>(与 相比IEnumerable<T>)。我无法详细解释,但如果您检查生成的 IL(例如在Sharplab.io),您已经会看到差异 - 编译器可以通过(Call 和 CallvirtList<T>.Enumerator )调用具体方法而不是多态调用。不确定这一点(以及由于使用接口而进行的一次分配)是否会导致这种性能差异。也许运行时可以进一步优化它(如果您想尝试更深入,请查看Sharplab.io上的 JIT Asm 差异)。callvirtstruct List<T>.Enumerator
如果您检查源代码,Enumerable.Any您会发现它使用相同的foreach循环,差异归结为使用IEnumerable接口:
public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
if (source == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
if (predicate == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.predicate);
}
foreach (TSource element in source)
{
if (predicate(element))
{
return true;
}
}
return false;
}
Run Code Online (Sandbox Code Playgroud)
因此,正如@Jon Skeet在评论中正确诊断的那样,差异来自于使用列表与可枚举。
正如 Jon Skeet 在评论中建议的那样,我尝试将_items集合从 aList<int>更改IEnumerable<int>为 以使比较公平。简而言之,这似乎是关键的区别。我的Foreach似乎利用了这样一个事实:它知道集合_items需要一段List<T>时间,该Enumerable.Any方法需要一个IEnumerable<int>.
以下是基准测试结果:
项目:1 000,搜索项目:543
| 方法 | 意思是 | 比率 | 已分配 | 分配比例 |
|---|---|---|---|---|
| 福里奇 | 2.126 我们 | 1.00 | 40乙 | 1.00 |
| 任何 | 2.131 我们 | 1.00 | 40乙 | 1.00 |
项目:10 000,搜索项目:5 432
| 方法 | 意思是 | 比率 | 已分配 | 分配比例 |
|---|---|---|---|---|
| 福里奇 | 21.35 我们 | 1.00 | 40乙 | 1.00 |
| 任何 | 21.20 我们 | 0.99 | 40乙 | 1.00 |
项目:100 000,搜索项目:54 321
| 方法 | 意思是 | 比率 | 已分配 | 分配比例 |
|---|---|---|---|---|
| 福里奇 | 220.7 我们 | 1.00 | 40乙 | 1.00 |
| 任何 | 219.1 我们 | 0.99 | 40乙 | 1.00 |
使用 时IEnumerable<int>,这两种方法的执行效果相同。谢谢乔恩斯基特!