在搜索 List<T> 时,为什么 Enumerable.Any(Func<TSource, bool> predicate) 比带有 if 语句的 foreach 慢

Mic*_*viš 23 c# linq .net-6.0

最近有件事引起了我的好奇心。

当他们做同样的事情时,为什么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乙 不适用

有两个基准:

  • Foreachforeach带有if语句的手册
  • Any : LINQ 的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方法有何不同?只是好奇...

Gur*_*ron 8

编译器可以优化内部 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在评论中正确诊断的那样,差异来自于使用列表与可枚举。


Mic*_*viš 4

正如 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>,这两种方法的执行效果相同。谢谢乔恩斯基特!