C# foreach 循环比 Raspberry Pi 上的 for 循环慢得多

C# foreach loop comically slower than for loop on a RaspberryPi

我在 RaspberryPi 上测试 .NET 应用程序,而该程序的每次迭代在 Windows 笔记本电脑上花费 500 毫秒,而在 RaspberryPi 上同样花费 5 秒。经过一些调试后,我发现大部分时间都花在了 foreach 循环连接字符串上。

编辑 1: 澄清一下,我提到的 500 毫秒和 5 秒时间是整个循环的时间。我在循环之前放置了一个计时器,并在循环完成后停止了计时器。并且,两者的迭代次数相同,均为 1000。

编辑 2: 为了给循环计时,我使用了提到的答案 here

private static string ComposeRegs(List<list_of_bytes> registers)
{
    string ret = string.Empty;
    foreach (list_of_bytes register in registers)
    {
        ret += Convert.ToString(register.RegisterValue) + ",";
    }
    return ret;
}

突然间,我用 for 循环替换了 foreach,突然间它开始花费的时间几乎与那台笔记本电脑上的时间相同。 500 到 600 毫秒。

private static string ComposeRegs(List<list_of_bytes> registers)
{
    string ret = string.Empty;
    for (UInt16 i = 0; i < 1000; i++)
    {
        ret += Convert.ToString(registers[i].RegisterValue) + ",";
    }
    return ret;
}

我应该始终使用 for 循环而不是 foreach 吗?或者这只是 for 循环比 foreach 循环快得多的场景?

如果字符串可以像您的示例中那样更改,那么使用 StringBuilder 是更好的选择,可以帮助您解决问题。

修改任何字符串对象都会导致创建一个新的字符串对象。这使得字符串的使用成本很高。所以当用户需要对字符串进行重复操作时,StringBuilder的需求就应运而生了。它提供了优化的方式来处理重复的和多个字符串操作操作。它代表一个可变的字符串。可变表示可以更改的字符串。所以 String 对象是不可变的,但 StringBuilder 是可变的字符串类型。它不会创建当前字符串对象的新修改实例,而是在现有字符串对象中进行修改。

因此,与其创建许多需要进行垃圾回收并占用大量内存的临时对象,不如使用 StringBuilder

更多关于 StringBuilder - https://docs.microsoft.com/en-us/dotnet/api/system.text.stringbuilder?view=net-6.0

实际问题是连接字符串而不是 forforeach 之间的区别。报告的时间 非常慢 即使在 Raspberry Pi 上也是如此。 1000 项数据太少了,任一台机器的 CPU 缓存都装不下。一个 RPi 有 1+ GHZ CPU,这意味着每个连接至少需要 1000 个周期。

问题是串联。字符串是不可变的。修改或连接字符串会创建一个新字符串。您的循环创建了 2000 个需要进行垃圾回收的临时对象。该过程昂贵。请改用 StringBuilder,最好使用 capacity 大致等于预期字符串的大小。

    [Benchmark]
    public string StringBuilder()
    {
        var sb = new StringBuilder(registers.Count * 3);
        foreach (list_of_bytes register in registers)
        {
            sb.AppendFormat("{0}",register.RegisterValue);
        }
        return sb.ToString();
    }

简单地测量单次执行,甚至平均 10 次执行,都不会产生有效数字。 GC 运行 很可能在其中一项测试中收集了这 2000 个对象。也很可能其中一项测试因 JIT 编译或任何其他原因而延迟。测试应该 运行 足够长以产生 稳定的 数字。

.NET 基准测试的事实标准是 BenchmarkDotNet。该库将 运行 每个基准测试足够长的时间以消除启动和冷却效果并考虑内存分配和 GC 收集。您不仅会看到每个测试需要多少时间,还会看到使用了多少 RAM 以及导致了多少次 GC

要实际测量您的代码,请尝试使用使用 BenchmarkDotNet 的基准测试:

[MemoryDiagnoser]
[MarkdownExporterAttribute.Whosebug]
public class ConcatTest
{

    private readonly List<list_of_bytes> registers;


    public ConcatTest()
    {
        registers = Enumerable.Range(0,1000).Select(i=>new list_of_bytes(i)).ToList();
    }

    [Benchmark]
    public string StringBuilder()
    {
        var sb = new StringBuilder(registers.Count*3);
        foreach (var register in registers)
        {
            sb.AppendFormat("{0}",register.RegisterValue);
        }
        return sb.ToString();
    }

    [Benchmark]
    public string ForEach()
    {
        string ret = string.Empty;
        foreach (list_of_bytes register in registers)
        {
            ret += Convert.ToString(register.RegisterValue) + ",";
        }
        return ret;
    }

    [Benchmark]
    public string For()
    {
        string ret = string.Empty;
        for (UInt16 i = 0; i < registers.Count; i++)
        {
            ret += Convert.ToString(registers[i].RegisterValue) + ",";
        }
        return ret;
    }

}

测试 运行 通过调用 BenchmarkRunner.Run<ConcatTest>()

using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Linq;

public class Program
{
    public static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<ConcatTest>();
        Console.WriteLine(summary);
    }
}

结果

运行 这在 Macbook 上产生了以下结果。请注意,BenchmarkDotNet 生成的结果可以在 Whosebug 中使用,结果中包含 运行时间信息 :

BenchmarkDotNet=v0.13.1, OS=macOS Big Sur 11.5.2 (20G95) [Darwin 20.6.0]
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET SDK=6.0.100
  [Host]     : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
  DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT


        Method |      Mean |    Error |   StdDev |    Gen 0 |   Gen 1 | Allocated |
-------------- |----------:|---------:|---------:|---------:|--------:|----------:|
 StringBuilder |  34.56 μs | 0.682 μs | 0.729 μs |   7.5684 |  0.3052 |     35 KB |
       ForEach | 278.36 μs | 5.509 μs | 5.894 μs | 818.8477 | 24.4141 |  3,763 KB |
           For | 268.72 μs | 3.611 μs | 3.015 μs | 818.8477 | 24.4141 |  3,763 KB |

ForForEach 花费的时间几乎是 StringBuilder 的 10 倍,使用的 RAM 是其 100 倍