我正在 RaspberryPi 上測驗 .NET 應用程式,而該程式的每次迭代在 Windows 筆記本電腦上需要 500 毫秒,而在 RaspberryPi 上則需要 5 秒。經過一些除錯,我發現大部分時間都花在了foreach連接字串的回圈上。
編輯 1:澄清一下,我提到的 500 毫秒和 5 秒時間是整個回圈的時間。我在回圈之前放置了一個計時器,并在回圈完成后停止計時器。并且,兩者的迭代次數相同,均為 1000。
編輯 2:為了給回圈計時,我使用了這里提到的答案。
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;
}
出乎意料的是,我foreach用一個for回圈替換了它,突然間它開始花費幾乎與在那臺筆記本電腦上一樣的時間。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回圈快得多的場景?
uj5u.com熱心網友回復:
實際問題是連接字串不是forvs之間的區別foreach。即使在 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.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花了超過近10倍StringBuilder,并用100倍的RAM
uj5u.com熱心網友回復:
這可能不是您要處理的問題,但是如果字串可以像您的示例中那樣更改,那么使用 aStringBuilder是更好的選擇,并且可以幫助真正提高性能。
修改任何字串物件都會導致創建一個新的字串物件。這使得使用字串的成本很高。所以當用戶需要對字串進行重復操作時,就產生了需求StringBuilder。它提供了處理重復和多個字串操作操作的優化方法。它代表一個可變的字串。可變的意思是可以改變的字串。所以 String 物件是不可變的,而 StringBuilder 是可變的字串型別。它不會創建當前字串物件的新修改實體,而是在現有字串物件中進行修改。
因此,與其創建許多需要被垃圾收集并意味著占用大量記憶體的臨時物件,不如使用StringBuilder.
更多關于StringBuilder- https://docs.microsoft.com/en-us/dotnet/api/system.text.stringbuilder?view=net-6.0
轉載請註明出處,本文鏈接:https://www.uj5u.com/gongcheng/366606.html
上一篇:傳單地圖未定義并同時初始化
