Condividi tramite


Case study: Guida per principianti all'ottimizzazione del codice e riduzione dei costi di calcolo (C#, Visual Basic, C++, F#)

La riduzione del tempo di calcolo comporta la riduzione dei costi, in modo da poter ottimizzare il codice in modo da risparmiare denaro. Questo case study usa un'applicazione di esempio con problemi di prestazioni per illustrare come usare gli strumenti di profilatura per migliorare l'efficienza. Per confrontare gli strumenti di profilatura, vedere Quale strumento scegliere?

Questo case study illustra questi argomenti:

  • Importanza dell'ottimizzazione del codice e dell'impatto sulla riduzione dei costi di calcolo.
  • Come usare gli strumenti di profilatura di Visual Studio per analizzare le prestazioni dell'applicazione.
  • Come interpretare i dati forniti da questi strumenti per identificare i colli di bottiglia delle prestazioni.
  • Come applicare strategie pratiche per ottimizzare il codice, concentrandosi sull'utilizzo della CPU, sull'allocazione della memoria e sulle interazioni del database.

Seguire e quindi applicare queste tecniche alle proprie applicazioni per renderle più efficienti e convenienti.

Case study di ottimizzazione

L'applicazione di esempio esaminata in questo case study è un'applicazione .NET che esegue query su un database di blog e post di blog. Usa Entity Framework, un ORM (Object-Relational Mapping) comune per .NET, per interagire con un database locale SQLite. L'applicazione è strutturata per eseguire un numero elevato di query, simulando uno scenario reale in cui potrebbe essere necessaria un'applicazione .NET per gestire attività di recupero dati estese. L'applicazione di esempio è una versione modificata dell'esempio introduttivo di Entity Framework.

Il problema principale delle prestazioni con l'applicazione di esempio consiste nel modo in cui gestisce le risorse di calcolo e interagisce con il database. L'applicazione presenta un collo di bottiglia delle prestazioni che influisce significativamente sull'efficienza e, di conseguenza, sui costi di calcolo associati all'esecuzione. Il problema include i sintomi seguenti:

  • Utilizzo elevato della CPU: le applicazioni possono eseguire calcoli inefficienti o attività di elaborazione in modo da consumare inutilmente una grande quantità di risorse DELLA CPU. Ciò può causare tempi di risposta lenti e costi operativi aumentati.

  • Allocazione di memoria inefficiente: le applicazioni possono talvolta riscontrare problemi relativi all'utilizzo e all'allocazione della memoria. Nelle app .NET, la gestione inefficiente della memoria può comportare un aumento della Garbage Collection, che a sua volta può influire sulle prestazioni dell'applicazione.

  • Overhead di interazione del database: le applicazioni che eseguono un numero elevato di query su un database possono riscontrare colli di bottiglia correlati alle interazioni del database. Sono incluse query inefficienti, chiamate di database eccessive e un uso scarso delle funzionalità di Entity Framework, che possono compromettere le prestazioni.

Il case study mira a risolvere questi problemi usando gli strumenti di profilatura di Visual Studio per analizzare le prestazioni dell'applicazione. Comprendendo dove e come migliorare le prestazioni dell'applicazione, gli sviluppatori possono implementare ottimizzazioni per ridurre l'utilizzo della CPU, migliorare l'efficienza di allocazione della memoria, semplificare le interazioni del database e ottimizzare l'utilizzo delle risorse. L'obiettivo finale è migliorare le prestazioni complessive dell'applicazione, rendendolo più efficiente e conveniente per l'esecuzione.

Esercizio

Risolvere i problemi di prestazioni nell'applicazione .NET di esempio presenta diverse problematiche. Queste sfide derivano dalla complessità della diagnosi dei colli di bottiglia delle prestazioni. Le sfide principali per risolvere i problemi descritti sono le seguenti:

  • Diagnosi dei colli di bottiglia delle prestazioni: una delle principali sfide consiste nell'identificare accuratamente le cause principali dei problemi di prestazioni. L'utilizzo elevato della CPU, l'allocazione di memoria inefficiente e i sovraccarichi di interazione del database possono avere più fattori che contribuiscono. Gli sviluppatori devono usare gli strumenti di profilatura in modo efficace per diagnosticare questi problemi, che richiedono una certa comprensione del funzionamento di questi strumenti e di come interpretare l'output.

  • Vincoli di conoscenza e risorse: infine, i team possono affrontare vincoli correlati a conoscenze, competenze e risorse. La profilatura e l'ottimizzazione di un'applicazione richiedono competenze ed esperienze specifiche e non tutti i team possono avere accesso immediato a queste risorse.

Affrontare queste sfide richiede un approccio strategico che combina un uso efficace di strumenti di profilatura, conoscenze tecniche e un'attenta pianificazione e test. Il case study mira a guidare gli sviluppatori attraverso questo processo, fornendo strategie e informazioni dettagliate per superare queste sfide e migliorare le prestazioni dell'applicazione.

Strategia

Ecco una panoramica generale dell'approccio in questo case study:

  • Si avvia l'indagine prendendo una traccia dell'utilizzo della CPU. Lo strumento Utilizzo CPU di Visual Studio è spesso utile per avviare indagini sulle prestazioni e ottimizzare il codice per ridurre i costi.
  • Successivamente, per ottenere informazioni dettagliate aggiuntive per isolare i problemi o migliorare le prestazioni, viene raccolta una traccia usando uno degli altri strumenti di profilatura. Ad esempio:
    • Si esamini l'utilizzo della memoria. Per .NET si prova prima lo strumento allocazione oggetti .NET. Per .NET o C++, è possibile esaminare invece lo strumento Utilizzo memoria.
    • Per ADO.NET o Entity Framework, è possibile usare lo strumento Database per esaminare query SQL, tempi di query precisi e altro ancora.

La raccolta dati richiede le attività seguenti:

  • Impostazione dell'app su una build di rilascio.
  • Selezione dello strumento Utilizzo CPU dal profiler prestazioni (ALT+F2). I passaggi successivi coinvolgono alcuni degli altri strumenti.
  • Dal Profiler prestazioni avviare l'app e raccogliere una traccia.

Esaminare le aree di utilizzo elevato della CPU

Dopo aver raccolto una traccia con lo strumento Utilizzo CPU e averla caricata in Visual Studio, si controlla prima di tutto la pagina iniziale del report con estensione diagsession che mostra i dati riepilogati. Usare il collegamento Apri dettagli nel report.

Screenshot dell'apertura dei dettagli nello strumento Utilizzo CPU.

Nella visualizzazione dei dettagli del report aprire la visualizzazione Albero delle chiamate. Il percorso del codice con il maggior utilizzo della CPU nell'app viene chiamato percorso critico. L'icona di fiamma del percorso attivo (Screenshot che mostra l'icona Percorso critico.) può aiutare a identificare rapidamente i problemi di prestazioni che potrebbero essere migliorati.

Nella visualizzazione Albero delle chiamate è possibile visualizzare un utilizzo elevato della CPU per il GetBlogTitleX metodo nell'app, usando circa una quota del 60% dell'utilizzo della CPU dell'app. Tuttavia, il valore self CPU per GetBlogTitleX è basso, solo circa .10%. A differenza della CPU totale, il valore self CPU esclude il tempo impiegato in altre funzioni, quindi sappiamo di guardare più lontano l'albero delle chiamate per il collo di bottiglia reale.

Screenshot della visualizzazione Albero delle chiamate nello strumento Utilizzo CPU.

GetBlogTitleXeffettua chiamate esterne a due DLL LINQ, che usano la maggior parte del tempo di CPU, come evidenziato dai valori auto CPU molto elevati. Questo è il primo indizio che una query LINQ potrebbe essere un'area da ottimizzare.

Screenshot della visualizzazione Albero delle chiamate nello strumento Utilizzo CPU con l'opzione Auto CPU evidenziata.

Per ottenere un albero delle chiamate visualizzato e una visualizzazione diversa dei dati, aprire la visualizzazione Flame Graph . In alternativa, fare clic con il pulsante destro del mouse GetBlogTitleX e scegliere Visualizza in Flame Graph. Anche in questo caso, sembra che il GetBlogTitleX metodo sia responsabile di un sacco di utilizzo della CPU dell'app (mostrato in giallo). Le chiamate esterne alle DLL LINQ vengono visualizzate sotto la GetBlogTitleX casella e usano tutto il tempo di CPU per il metodo .

Screenshot della visualizzazione Flame Graph nello strumento Utilizzo CPU.

Raccogliere dati aggiuntivi

Spesso, altri strumenti possono fornire informazioni aggiuntive per facilitare l'analisi e isolare il problema. In questo case study viene adottato l'approccio seguente:

  • Esaminare prima di tutto l'utilizzo della memoria. Potrebbe esserci una correlazione tra utilizzo elevato della CPU e utilizzo elevato della memoria, quindi può essere utile esaminare entrambi per isolare il problema.
  • Poiché sono state identificate le DLL LINQ, verrà esaminato anche lo strumento Database.

Controllare l'utilizzo della memoria

Per vedere cosa sta accadendo con l'app in termini di utilizzo della memoria, viene raccolta una traccia usando lo strumento di allocazione di oggetti .NET (per C++, è possibile usare invece lo strumento Utilizzo memoria). La visualizzazione Albero delle chiamate nella traccia della memoria mostra il percorso critico e consente di identificare un'area di utilizzo elevato della memoria. Nessuna sorpresa a questo punto, il GetBlogTitleX metodo sembra generare un sacco di oggetti! Oltre 900.000 allocazioni di oggetti, infatti.

Screenshot della visualizzazione Albero delle chiamate nello strumento Allocazione oggetti .NET.

La maggior parte degli oggetti creati sono stringhe, matrici di oggetti e Int32s. È possibile vedere come questi tipi vengono generati esaminando il codice sorgente.

Controllare la query nello strumento Database

In Performance Profiler selezionare lo strumento Database anziché l'utilizzo della CPU (o selezionare entrambi). Dopo aver raccolto una traccia, aprire la scheda Query nella pagina di diagnostica. Nella scheda Query per la traccia database è possibile visualizzare la prima riga che mostra la query più lunga, 2446 ms. La colonna Record mostra il numero di record letti dalla query. È possibile usare queste informazioni per un confronto successivo.

Screenshot delle query di database nello strumento Database.

Esaminando l'istruzione SELECT generata da LINQ nella colonna Query, viene identificata la prima riga come query associata al GetBlogTitleX metodo . Per visualizzare la stringa di query completa, espandere la larghezza della colonna. La stringa di query completa è:

SELECT "b"."Url", "b"."BlogId", "p"."PostId", "p"."Author", "p"."BlogId", "p"."Content", "p"."Date", "p"."MetaData", "p"."Title"
FROM "Blogs" AS "b" LEFT JOIN "Posts" AS "p" ON "b"."BlogId" = "p"."BlogId" ORDER BY "b"."BlogId"

Si noti che l'app sta recuperando molti valori di colonna qui, forse più di quanto sia necessario. Si esaminerà ora il codice sorgente.

Ottimizza codice

È il momento di esaminare il GetBlogTitleX codice sorgente. Nello strumento Database fare clic con il pulsante destro del mouse sulla query e scegliere Vai al file di origine. Nel codice sorgente per GetBlogTitleXè disponibile il codice seguente che usa LINQ per leggere il database.

foreach (var blog in db.Blogs.Select(b => new { b.Url, b.Posts }).ToList())
  {
    foreach (var post in blog.Posts)
    {
      if (post.Author == "Fred Smith")
      {
        Console.WriteLine($"Post: {post.Title}");
      }
  }
}

Questo codice usa foreach cicli per cercare nel database tutti i blog con "Fred Smith" come autore. Esaminandolo, è possibile notare che molti oggetti vengono generati in memoria: una nuova matrice di oggetti per ogni blog nel database, stringhe associate per ogni URL e valori per le proprietà contenute nei post, ad esempio l'ID blog.

Vengono eseguite alcune ricerche e vengono fornite alcune raccomandazioni comuni per ottimizzare le query LINQ e ottenere questo codice.

Suggerimento

In alternativa, possiamo risparmiare tempo e lasciare che Copilot faccia la ricerca per noi.

foreach (var x in db.Posts.Where(p => p.Author.Contains("Fred Smith")).Select(b => b.Title).ToList())
{
  Console.WriteLine("Post: " + x);
}

In questo codice sono state apportate diverse modifiche per ottimizzare la query:

  • Aggiunta della Where clausola ed eliminazione di uno dei foreach cicli.
  • Proiettata solo la proprietà Title nell'istruzione Select , che è tutto ciò che è necessario in questo esempio.

Successivamente, si esegue il nuovo test usando gli strumenti di profilatura.

Ottimizzare il codice con Copilot

Se si usa Copilot, è possibile chiedere a Copilot di cercare problemi di prestazioni per noi. Selezionare Chiedi a Copilot dal menu di scelta rapida e digitare la domanda seguente:

Can you make the LINQ query in this method faster?

Suggerimento

È possibile usare i comandi barra, ad esempio /optimize , per creare domande valide per Copilot.

In questo esempio, Copilot fornisce le modifiche di codice suggerite seguenti, simili alla query ottimizzata, insieme a una spiegazione.

public void GetBlogTitleX()
{
    var posts = db.Posts
        .Where(post => post.Author == "Fred Smith")
        .Select(post => post.Title)
        .ToList();

    foreach (var postTitle in posts)
    {
        Console.WriteLine($"Post: {postTitle}");
    }
}

Risultati

Dopo aver aggiornato il codice, viene eseguito di nuovo lo strumento Utilizzo CPU per raccogliere una traccia. La visualizzazione Albero delle chiamate mostra che GetBlogTitleX è in esecuzione solo 1754 ms, usando il 37% del totale della CPU dell'app, un miglioramento significativo del 59%.

Screenshot del miglioramento dell'utilizzo della CPU nella visualizzazione Albero delle chiamate dello strumento Utilizzo CPU.

Passare alla visualizzazione Flame Graph per visualizzare un'altra visualizzazione che mostra il miglioramento. In questa visualizzazione viene GetBlogTitleX usata anche una parte più piccola della CPU.

Screenshot del miglioramento dell'utilizzo della CPU nella visualizzazione Flame Graph dello strumento Utilizzo CPU.

Controllare i risultati nella traccia dello strumento database e solo due record vengono letti usando questa query, anziché 100.000. Inoltre, la query è molto semplificata ed elimina l'OPERAZIONE LEFT JOIN non necessaria generata in precedenza.

Screenshot del tempo di query più veloce nello strumento Database.

Successivamente, ricontrollare i risultati nello strumento allocazione oggetti .NET e si noterà che GetBlogTitleX è responsabile solo di 56.000 allocazioni di oggetti, quasi un 95% di riduzione rispetto al 900.000!

Screenshot delle allocazioni di memoria ridotte nello strumento Allocazione oggetti .NET.

Iterare

Potrebbero essere necessarie più ottimizzazioni ed è possibile continuare a eseguire l'iterazione con le modifiche del codice per vedere quali modifiche migliorano le prestazioni e consentono di ridurre i costi di calcolo.

Passaggi successivi

Gli articoli e i post di blog seguenti forniscono altre informazioni per imparare a usare in modo efficace gli strumenti per le prestazioni di Visual Studio.