Il benchmarking è fondamentale per comprendere e ottimizzare le prestazioni del tuo codice C#. Lo strumento di riferimento nell’ecosistema .NET per questo scopo è BenchmarkDotNet. È una libreria potente e affidabile che ti aiuta a misurare le performance del tuo codice in modo accurato, eliminando il rumore da fattori esterni come la compilazione JIT, il garbage collection e la pianificazione del sistema operativo.
Ecco un esempio di analisi usando BenchmarkDotNet, con una spiegazione delle sue funzionalità e di come interpretare i risultati.
Perché BenchmarkDotNet?
- Accuratezza: Esegue cicli di riscaldamento (warm-up), iterazioni multiple e analisi statistiche per fornire misurazioni estremamente affidabili.
- Facilità d’uso: Si integra perfettamente con i tuoi progetti C# e utilizza attributi simili ai framework di unit testing.
- Report completi: Genera report dettagliati in vari formati (console, Markdown, HTML, CSV) con metriche ricche.
- Diagnosi: Offre diagnostici integrati (ad esempio,
MemoryDiagnoser,HardwareCounters) per raccogliere ulteriori informazioni sulle prestazioni, oltre al solo tempo di esecuzione.
Esempio di Analisi Benchmark in C#
Confrontiamo le prestazioni di due modi comuni per concatenare stringhe in C#: usando l’operatore + e StringBuilder.
1. Setup del Progetto
Per prima cosa, creiamo una nuova applicazione console .NET:
Bash
dotnet new console -n BenchmarkDemo cd BenchmarkDemo
Poi, installiamo il pacchetto NuGet BenchmarkDotNet:
Bash
dotnet add package BenchmarkDotNet
2. Creazione della Classe Benchmark
Creaiamo un nuovo file C# (ad esempio, StringConcatenationBenchmarks.cs) e aggiungi il seguente codice:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Text; // Richiesto per StringBuilder
namespace BenchmarkDemo
{
[MemoryDiagnoser] // Questo diagnostico mostrerà i dettagli di allocazione della memoria
public class StringConcatenationBenchmarks
{
private const int N = 1000; // Numero di concatenazioni
[Benchmark(Baseline = true)] // Contrassegna questo come riferimento per il confronto
public string ConcatenateWithPlus()
{
string result = string.Empty;
for (int i = 0; i < N; i++)
{
result += "hello";
}
return result;
}
[Benchmark]
public string ConcatenateWithStringBuilder()
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < N; i++)
{
sb.Append("hello");
}
return sb.ToString();
}
}
}
[MemoryDiagnoser]: Questo attributo dice a BenchmarkDotNet di raccogliere statistiche sull’allocazione della memoria, il che è molto utile per le operazioni sulle stringhe.[Benchmark]: Questo attributo contrassegna i metodi che BenchmarkDotNet deve misurare.[Benchmark(Baseline = true)]: Uno dei tuoi metodi benchmark dovrebbe essere designato comeBaseline. Questo rende più facile confrontare le prestazioni degli altri metodi rispetto a uno standard noto.
3. Esecuzione dei Benchmark
Modifichiamo il file Program.cs per eseguire i benchmark:
C#
using BenchmarkDotNet.Running;
using BenchmarkDemo; // Assicurati di includere il tuo namespace benchmark
namespace BenchmarkDemo
{
public class Program
{
public static void Main(string[] args)
{
// È cruciale eseguire i benchmark in modalità Release per risultati accurati.
// Puoi farlo eseguendo: dotnet run -c Release
var summary = BenchmarkRunner.Run<StringConcatenationBenchmarks>();
}
}
}
Importante: Eseguiamo sempre i tuoi benchmark in modalità Release e al di fuori del debugger. Il debug aggiunge un overhead che può falsare i risultati, e la modalità Release abilita le ottimizzazioni del compilatore che non sono presenti in modalità Debug.
Per eseguire, apriamo il terminale nella directory del progetto BenchmarkDemo ed esegui:
Bash
dotnet run -c Release
BenchmarkDotNet eseguirà diverse iterazioni (warm-up, pilot, misurazioni effettive) e poi stamperà un riepilogo dettagliato nella console. Genererà anche vari file di report nella cartella BenchmarkDotNet.Artifacts.
4. Analisi dei Risultati
Ecco un esempio di come potrebbe apparire l’output della console (i valori effettivi varieranno in base alla tua macchina):
BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4412/22H2/2022Update), .NET SDK 8.0.300 Intel Core i7-10750H CPU 2.60GHz (Comet Lake), 1 CPU, 12 logical and 6 physical cores .NET SDK 8.0.300 [Host] : .NET 8.0.5 (8.0.524.21711), X64 RyuJIT AVX2 DefaultJob : .NET 8.0.5 (8.0.524.21711), X64 RyuJIT AVX2 | Method | Mean | Error | StdDev | Ratio | Allocated | |---------------------------------|-------------|-----------|-----------|-------|-----------| | ConcatenateWithPlus | 2,058.0 us | 40.57 us | 33.88 us | 1.00 | 5600000 B | | ConcatenateWithStringBuilder | 12.00 us | 0.08 us | 0.07 us | 0.00 | 3600 B |
Analizziamo le colonne chiave nella tabella dei risultati:
- Method (Metodo): Il nome del metodo benchmarked.
- Mean (Media): Il tempo di esecuzione medio del metodo su tutte le misurazioni. Questa è solitamente la metrica più importante. Nel nostro esempio,
ConcatenateWithStringBuilderha una media significativamente inferiore (12.00 us) rispetto aConcatenateWithPlus(2.058,0 us), indicando che è molto più veloce. - Error (Errore): Metà dell’intervallo di confidenza al 99,9%. Ti dà un’idea della precisione del valore
Mean. Un errore minore indica misurazioni più consistenti. - StdDev (Deviazione Standard): Misura la dispersione dei tempi di esecuzione dalla
Mean. Una deviazione standard minore indica prestazioni più consistenti. - Ratio (Rapporto): Il tempo di esecuzione medio del metodo corrente diviso per il tempo di esecuzione medio del metodo baseline. Per
ConcatenateWithPlus(il nostro baseline), il rapporto è 1,00. PerConcatenateWithStringBuilder, il rapporto è vicino a 0, confermando le sue prestazioni superiori. - Allocated (Allocato): La quantità di memoria gestita allocata da una singola invocazione del metodo. Questo è fornito dal
MemoryDiagnoser. Qui,ConcatenateWithPlusalloca ben 5,6 MB a causa dell’immutabilità delle stringhe e delle ripetute riallocazioni, mentreConcatenateWithStringBuilderalloca solo 3,6 KB. Questa è un’intuizione critica sul motivo per cuiStringBuilderè preferito per le manipolazioni frequenti di stringhe. - Gen 0 / Gen 1 / Gen 2 (non mostrato in questo output semplificato, ma spesso presente con
MemoryDiagnoser): Queste colonne indicano il numero di garbage collection in diverse generazioni (Gen 0, Gen 1, Gen 2) per 1000 operazioni. Un numero elevato di GC può anche influire sulle prestazioni.
Analisi dell’Output dell’Esempio:
I risultati del benchmark mostrano chiaramente che ConcatenateWithStringBuilder è di gran lunga superiore a ConcatenateWithPlus per le concatenazioni ripetute di stringhe.
- Prestazioni:
StringBuilderè ordini di grandezza più veloce (Media: 12.00 us contro 2.058,0 us). - Utilizzo della memoria:
StringBuilderalloca significativamente meno memoria (3,6 KB contro 5,6 MB), il che è un enorme vantaggio per l’efficienza della memoria e la riduzione della pressione sul garbage collection.
Questo esempio evidenzia una comune insidia di performance in C# e dimostra come il benchmarking possa fornire dati concreti per convalidare le migliori pratiche.
Altre Funzionalità e Suggerimenti Utili di BenchmarkDotNet:
[Params]Attribute: da usare per testare i tuoi metodi con diverse dimensioni di input o configurazioni.C#[Params(100, 1000, 10000)] public int N; // Eseguirà il benchmark per N = 100, 1000 e 10000[GlobalSetup]e[GlobalCleanup]: I metodi contrassegnati con questi attributi vengono eseguiti una volta prima di tutti i benchmark e una volta dopo tutti i benchmark, rispettivamente. Utili per inizializzare o pulire risorse condivise tra tutti i metodi benchmark.[IterationSetup]e[IterationCleanup]: Eseguiti prima/dopo ogni iterazione di un benchmark. Utili per risorse necessarie per ogni esecuzione ma che non dovrebbero influenzare altri benchmark.- Custom Jobs (Lavori Personalizzati): definire diversi “lavori” per eseguire i tuoi benchmark su varie runtime .NET, JIT o piattaforme.
- Exporters (Esportatori): BenchmarkDotNet può esportare i risultati in vari formati come HTML, CSV, Markdown, ecc. Puoi specificarli nel tuo
Program.cso usando argomenti da riga di comando. Ad esempio, per generare un report HTML:C#var config = DefaultConfig.Instance.AddExporter(BenchmarkDotNet.Exporters.HtmlExporter.Default); BenchmarkRunner.Run<StringConcatenationBenchmarks>(config); - Integrazione con Visual Studio: Visual Studio 2022 (versione 17.9+) offre strumenti integrati per visualizzare i dati di
BenchmarkDotNet, inclusi i dati sull’utilizzo della CPU e sull’allocazione, se utilizzi il pacchetto NuGetMicrosoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnoserse i diagnostici pertinenti.
Eseguendo sistematicamente il benchmarking del tuo codice C# con strumenti come BenchmarkDotNet, puoi prendere decisioni basate sui dati per migliorare le prestazioni e l’utilizzo delle risorse della tua applicazione.