循环中 Enumerable Count 方法的性能

Performance of Enumerable Count method in a loop

我有一个由 IEnumerable

表示的项目序列

我需要遍历这些项目,它应该是 for-loop 因为索引很重要。

我的问题是,以下两个选项在性能上有区别吗?

1.

for (int i = 0; i < items.Count(); i++)
{
    //Do something
}
var itemsLength = items.Count();

for (int i = 0; i < itemsLength; i++)
{
    //Do something
}

换句话说,方法 items.Count() 运行 是否在选项 1 的每次迭代中一次又一次?

通常当涉及到这类性能问题时,测试它会更容易。使用 BenchmarkDotnet :

|  Method |      Mean |     Error |    StdDev | Ratio | RatioSD | Allocated |
|-------- |----------:|----------:|----------:|------:|--------:|----------:|
| Option2 |  49.40 ns |  0.700 ns |  0.654 ns |  1.00 |    0.00 |         - |
| Option1 | 955.00 ns | 16.993 ns | 15.064 ns | 19.30 |    0.34 |         - |

选项 1 慢了 20 倍。这里的 IEnumerable 是使用 Enumerable.Range(0,100)

生成的

然后我还测试了如果您的实际类型是 List 会发生什么,以及 Count() 是否优化为仅 return Count 属性。这些是结果:

|  Method |      Mean |    Error |   StdDev | Ratio | RatioSD | Allocated |
|-------- |----------:|---------:|---------:|------:|--------:|----------:|
| Option2 |  40.53 ns | 0.246 ns | 0.192 ns |  1.00 |    0.00 |         - |
| Option1 | 452.07 ns | 4.906 ns | 4.097 ns | 11.14 |    0.12 |         - |

这次选项 1 慢了 11 倍,似乎只是因为不必生成完整的 100 个可枚举项。

使用的测试用例:

[Benchmark]
public static int Option1()
{
    var x = 0;
    for (int i = 0; i < Data.Count(); i++)
    {
        x++;
    }

    return x;
}

[Benchmark(Baseline = true)]
public static int Option2()
{
    var itemsLength = Data.Count();
    var x = 0;
    for (int i = 0; i < itemsLength; i++)
    {
        x++;
    }

    return x;
}

在可枚举源上调用 Count() 将耗尽可枚举直到枚举所有元素。因此,在 for 循环中的 condition-block 中的可枚举对象上调用 Count() 将因此在每次迭代时耗尽可枚举对象。例如调用

var numbers = VerboseRange(1, 5);
for (var index = 0; index < numbers.Count(); index++)
{
    Console.WriteLine($"For-loop is at index {index}...");
}

IEnumerable<int> VerboseRange(int start, int count)
{
    foreach (var number in Enumerable.Range(start, count))
    {
        Console.WriteLine($"Yielded number {number}.");
        yield return number;
    }
}

会输出

Yielded number 1.
Yielded number 2.
Yielded number 3.
Yielded number 4.
Yielded number 5.
For-loop is at index 0...
Yielded number 1.
Yielded number 2.
Yielded number 3.
Yielded number 4.
Yielded number 5.
For-loop is at index 1...
Yielded number 1.
Yielded number 2.
Yielded number 3.
Yielded number 4.
Yielded number 5.
For-loop is at index 2...
Yielded number 1.
Yielded number 2.
Yielded number 3.
Yielded number 4.
Yielded number 5.
For-loop is at index 3...
Yielded number 1.
Yielded number 2.
Yielded number 3.
Yielded number 4.
Yielded number 5.
For-loop is at index 4...
Yielded number 1.
Yielded number 2.
Yielded number 3.
Yielded number 4.
Yielded number 5.

所以还是先数再说比较好

但是,我建议您使用计数器和 foreach-loop

var count = 0;
foreach (var item in items)
{
    // do something
    count++;
}

在 C# 7.0 中,您终于可以做到

foreach (var (item, index) in items.WithIndex())
{
    // do something
}