问题描述
我在RaspberryPI上测试.NET应用程序,该程序的每次迭代在Windows笔记本电脑上需要500毫秒,而在RaspberryPI上同样需要5秒。经过一些调试后,我发现大部分时间都花在了foreach
连接字符串的循环上。
编辑1:澄清一下,我提到的500 ms零5 s时间是整个循环的时间。我在循环之前放置了一个计时器,并在循环结束后停止计时器。而且,两者的迭代次数相同,为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
循环快得多的场景?
推荐答案
实际问题是连接字符串,而不是for
和foreach
之间的区别。报告的计时非常慢,即使在覆盆子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收集。您不仅会看到每次测试需要多少内存,而且还会看到使用了多少内存以及导致了多少GC
要实际测量您的代码,请尝试使用BenchmarkDotNet:
使用此基准[MemoryDiagnoser]
[MarkdownExporterAttribute.StackOverflow]
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生成的结果可以在StackOverflow中使用,并且运行时信息包含在结果中:
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 |
For
和ForEach
占用的内存几乎是StringBuilder
的10倍,使用的内存是StringBuilder
的100倍
这篇关于C#Foreach循环比RaspberryPI上的for循环慢得可笑的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!