在 .NET Core 中,for 和 foreach 现在在迭代值类型数组时相同吗?

Mik*_*keJ 5 c# foreach for-loop .net-core

我的问题: 我想知道三件事。首先,在 .NET Core 中,“foreach”循环是否针对与“for”循环相同的代码进行了优化,这是否特定于 .NET Core?使用 ILSpy 查看编译后的代码表明“foreach”被视为 for 循环。

其次,我想让一些人知道我使用 BenchmarkDotNet 的测试是否有缺陷,或者是否很难在桌面上使用 .NET 获得一致的结果。结果有很多变化,我不确定它们是否都有用。

最后,我想看看是否有人可以确认迭代值类型数组(尤其是大型结构数组)的最佳方法是使用带有 ref 迭代器变量的 foreach。像这样:

        var data = this.Data.AsSpan();
        foreach (ref var d in data)
        {
            if (d.Equals(Check))
                ++count;
        }
Run Code Online (Sandbox Code Playgroud)

更多细节: 我正在优化一些需要迭代结构数组的代码。我记得我之前看到过“for”循环比“foreach”快一些的指导。但是,正如此处所见,该指南已经很旧。SO 答案仍然是第一个出现的答案,但它已有 11 年的历史了。

我还注意到这个 SO答案是最近的,它着眼于 .NET 核心的新功能 - span 和 ref 变量。但结果令人惊讶的是“for 循环”似乎更慢。

鉴于第二个答案的结果似乎与我决定自己测试的旧指南相矛盾。

我认为迭代值类型数组的最快方法 - 使用指针 - 在大多数情况下是最快的,但并非总是如此。在大多数情况下,将 foreach 与 by ref 迭代器变量一起使用,结果与指针相同。

但结果在哪里不是很一致。我在展示的运行中使用了 10,000 个元素。我也用了50万。结果仍然与较大的数据大小不一致。

代码: 我跳过了第二个答案中使用的自定义枚举器,而是使用指针添加了一个 while 循环。看起来像这样:

    [Benchmark]
    public int RefInc()
    {
        var data = this.Data.AsSpan();
        ref T current = ref MemoryMarshal.GetReference(data);
        int length = data.Length;
        int count = 0;

        while (length > 0)
        {
            if (current.Equals(Check))
                ++count;

            --length;
            current = ref Unsafe.Add(ref current, 1);
        }

        return count;
    }
Run Code Online (Sandbox Code Playgroud)

我认为理论上这将是迭代值类型数组的最快方法。请注意,我绝不建议使用这种类型的循环,因为 for 和 foreach 更清晰。我只是想要一个参考点来再次比较其他人,认为这将是我能得到的最快的。

我还将包括简单的“foreach”基准测试以及该方法使用 ILSpy 的情况。

    [Benchmark]
    public int ForEach()
    {
        int count = 0;
        foreach (var d in this.Data)
        {
            if (d.Equals(Check))
                ++count;
        }
        return count;
    }
Run Code Online (Sandbox Code Playgroud)

使用 ILSpy:

[Benchmark]
public int ForEach()
{
    int count = 0;
    T[] data = Data;
    for (int i = 0; i < data.Length; i++)
    {
        T d = data[i];
        if (d.Equals(Check))
        {
            count++;
        }
    }
    return count;
}
Run Code Online (Sandbox Code Playgroud)

本课程中包含的整套基准测试:

public class ArrayEnumerationBenchMark<T> where T : IEquatable<T>
{
    readonly T Check = default(T);
    T[] Data;

    [GlobalSetup]
    public void Setup()
    {
        this.Data = new T[DataSize];
    }

    [Params(10000)]
    public int DataSize;


    [Benchmark]
    public int RefInc()
    {
        var data = this.Data.AsSpan();
        ref T current = ref MemoryMarshal.GetReference(data);
        int length = data.Length;
        int count = 0;

        while (length > 0)
        {
            if (current.Equals(Check))
                ++count;

            --length;
            current = ref Unsafe.Add(ref current, 1);
        }

        return count;
    }

    [Benchmark]
    public int ForEach()
    {
        int count = 0;
        foreach (var d in this.Data)
        {
            if (d.Equals(Check))
                ++count;
        }
        return count;
    }

    [Benchmark]
    public int ForEachSpan()
    {
        int count = 0;
        var data = this.Data.AsSpan();
        foreach (var d in data)
        {
            if (d.Equals(Check))
                ++count;
        }
        return count;
    }

    [Benchmark]
    public int ForEachSpanRef()
    {
        int count = 0;
        var data = this.Data.AsSpan();
        foreach (ref var d in data)
        {
            if (d.Equals(Check))
                ++count;
        }
        return count;
    }

    [Benchmark]
    public int For()
    {
        int count = 0;
        T[] data = this.Data;
        for (int i = 0; i < data.Length; ++i)
        {
            T d = data[i];

            if (d.Equals(Check))
                ++count;
        }
        return count;
    }

    [Benchmark]
    public int ForSpan()
    {
        int count = 0;
        var data = this.Data.AsSpan();
        for (int i = 0; i < data.Length; ++i)
        {
            T d = data[i];

            if (d.Equals(Check))
                ++count;
        }
        return count;
    }

    [Benchmark]
    public int ForRefSpan()
    {
        var data = this.Data.AsSpan();
        int count = 0;
        for (int i = 0; i < data.Length; ++i)
        {
            ref readonly T d = ref data[i];

            if (d.Equals(Check))
                ++count;
        }

        return count;
    }

    [Benchmark]
    public int ForRef()
    {
        int count = 0;
        for (int i = 0; i < this.Data.Length; ++i)
        {
            ref readonly T d = ref this.Data[i];

            if (d.Equals(Check))
                ++count;
        }

        return count;
    }
}
Run Code Online (Sandbox Code Playgroud)

Benchmark 类是通用的,可以接受任何 IEquatable 结构。我使用了第二个问题中的 Color 结构并添加了一些更大的结构,以查看结构的大小是否改变了结果。Color 结构和 DoubleColor 包含在下面的 LargeStruct 中。我将跳过 TripleColor 和 QuadColor 变体,因为这已经很长了。

public struct Color : IEquatable<Color>
{
    public float R;
    public float G;
    public float B;
    public float A;

    public bool Equals([AllowNull] Color other) => this.R == other.R;
}

public struct DoubleColor : IEquatable<DoubleColor>
{
    public Color First;
    public Color Second;

    public bool Equals([AllowNull] DoubleColor other)
    {
        return this.First.R == other.First.R;
    }
}
public struct LargeStruct : IEquatable<LargeStruct>
{
    public Color First;
    public Guid G0;
    public Guid G1;
    public Guid G2;
    public Guid G3;
    public Guid G4;
    public Guid G5;
    public Guid G6;
    public Guid G7;
    public Guid G8;
    public Guid G9;


    public bool Equals([AllowNull] LargeStruct other)
    {
        return this.First.R == other.First.R;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我使用 BenchmarkDotNet 运行程序执行此操作,如下所示:

    static void Main(string[] args)
    {
        var config = ManualConfig.Create(DefaultConfig.Instance)
                                    .WithOption(ConfigOptions.KeepBenchmarkFiles, true);

        BenchmarkRunner.Run<ArrayEnumerationBenchMark<byte>>(config);
        BenchmarkRunner.Run<ArrayEnumerationBenchMark<short>>(config);
        BenchmarkRunner.Run<ArrayEnumerationBenchMark<double>>(config);
        BenchmarkRunner.Run<ArrayEnumerationBenchMark<decimal>>(config);
        BenchmarkRunner.Run<ArrayEnumerationBenchMark<Color>>(config);
        BenchmarkRunner.Run<ArrayEnumerationBenchMark<DoubleColor>>(config);
        BenchmarkRunner.Run<ArrayEnumerationBenchMark<TripleColor>>(config);
        BenchmarkRunner.Run<ArrayEnumerationBenchMark<QuadColor>>(config);
        BenchmarkRunner.Run<ArrayEnumerationBenchMark<LargeStruct>>(config);
    }
Run Code Online (Sandbox Code Playgroud)

结果: 我将只展示一些结果以表明我对结果的担忧。我将在每个结果块上方添加类型。注意 BenchmarkDotNet 有时会从摘要中删除中值,我不知道为什么会这样。所以忽略那一栏。

字节

|         Method | DataSize |     Mean |     Error |    StdDev |
|--------------- |--------- |---------:|----------:|----------:|
|         RefInc |    10000 | 4.587 ?s | 0.0573 ?s | 0.0479 ?s |
|        ForEach |    10000 | 9.765 ?s | 0.1775 ?s | 0.2308 ?s |
|    ForEachSpan |    10000 | 4.879 ?s | 0.0971 ?s | 0.2027 ?s |
| ForEachSpanRef |    10000 | 4.767 ?s | 0.0950 ?s | 0.0889 ?s |
|            For |    10000 | 9.871 ?s | 0.1937 ?s | 0.2306 ?s |
|        ForSpan |    10000 | 4.756 ?s | 0.0680 ?s | 0.0568 ?s |
|     ForRefSpan |    10000 | 4.798 ?s | 0.0719 ?s | 0.0600 ?s |
|         ForRef |    10000 | 7.303 ?s | 0.0795 ?s | 0.0620 ?s |
Run Code Online (Sandbox Code Playgroud)

双倍的

|         Method | DataSize |      Mean |     Error |    StdDev |
|--------------- |--------- |----------:|----------:|----------:|
|         RefInc |    10000 | 11.649 ?s | 0.2311 ?s | 0.5168 ?s |
|        ForEach |    10000 |  7.441 ?s | 0.1470 ?s | 0.2867 ?s |
|    ForEachSpan |    10000 |  6.865 ?s | 0.1354 ?s | 0.2705 ?s |
| ForEachSpanRef |    10000 |  7.872 ?s | 0.1564 ?s | 0.2570 ?s |
|            For |    10000 |  7.347 ?s | 0.1178 ?s | 0.0920 ?s |
|        ForSpan |    10000 |  6.895 ?s | 0.1376 ?s | 0.2143 ?s |
|     ForRefSpan |    10000 |  6.890 ?s | 0.1371 ?s | 0.3540 ?s |
|         ForRef |    10000 |  8.861 ?s | 0.1755 ?s | 0.3208 ?s |
Run Code Online (Sandbox Code Playgroud)

颜色

|         Method | DataSize |      Mean |     Error |    StdDev |    Median |
|--------------- |--------- |----------:|----------:|----------:|----------:|
|         RefInc |    10000 |  8.570 ?s | 0.0864 ?s | 0.0674 ?s |  8.588 ?s |
|        ForEach |    10000 |  9.824 ?s | 0.1097 ?s | 0.0857 ?s |  9.832 ?s |
|    ForEachSpan |    10000 |  9.939 ?s | 0.1979 ?s | 0.1851 ?s |  9.899 ?s |
| ForEachSpanRef |    10000 | 14.416 ?s | 0.2858 ?s | 0.6214 ?s | 14.096 ?s |
|            For |    10000 |  9.846 ?s | 0.1415 ?s | 0.1323 ?s |  9.797 ?s |
|        ForSpan |    10000 | 11.860 ?s | 0.4646 ?s | 1.3552 ?s | 12.143 ?s |
|     ForRefSpan |    10000 | 13.926 ?s | 0.0906 ?s | 0.0708 ?s | 13.920 ?s |
|         ForRef |    10000 | 17.225 ?s | 0.3361 ?s | 0.6061 ?s | 16.950 ?s |
Run Code Online (Sandbox Code Playgroud)

大型结构

|         Method | DataSize |     Mean |   Error |   StdDev |   Median |
|--------------- |--------- |---------:|--------:|---------:|---------:|
|         RefInc |    10000 | 127.2 ?s | 1.86 ?s |  1.55 ?s | 127.3 ?s |
|        ForEach |    10000 | 252.4 ?s | 4.76 ?s |  4.89 ?s | 251.9 ?s |
|    ForEachSpan |    10000 | 253.5 ?s | 4.97 ?s |  8.31 ?s | 250.4 ?s |
| ForEachSpanRef |    10000 | 129.0 ?s | 2.54 ?s |  4.89 ?s | 127.1 ?s |
|            For |    10000 | 248.5 ?s | 4.68 ?s |  4.15 ?s | 247.6 ?s |
|        ForSpan |    10000 | 251.3 ?s | 4.48 ?s |  3.97 ?s | 250.5 ?s |
|     ForRefSpan |    10000 | 252.0 ?s | 3.48 ?s |  2.90 ?s | 251.2 ?s |
|         ForRef |    10000 | 258.9 ?s | 5.10 ?s | 12.70 ?s | 253.5 ?s |
Run Code Online (Sandbox Code Playgroud)