Performance Tips and Tricks in .NET Applications (Suggerimenti e trucchi per le prestazioni nelle applicazioni .NET)

 

Emmanuel Schanzer
Microsoft Corporation

Agosto 2001

Riepilogo: Questo articolo è destinato agli sviluppatori che vogliono modificare le proprie applicazioni per prestazioni ottimali nel mondo gestito. Il codice di esempio, le spiegazioni e le linee guida di progettazione vengono risolti per le applicazioni Database, Windows Forms e ASP, nonché suggerimenti specifici per il linguaggio per Microsoft Visual Basic e Managed C++. (25 pagine stampate)

Contenuto

Panoramica
Suggerimenti sulle prestazioni per tutte le applicazioni
Suggerimenti per l'accesso al database
Suggerimenti sulle prestazioni per le applicazioni di ASP.NET
Suggerimenti per la conversione e lo sviluppo in Visual Basic
Suggerimenti per la conversione e lo sviluppo in C++ gestito
Risorse aggiuntive
Appendice: Costo di chiamate virtuali e allocazioni

Panoramica

Questo white paper è progettato come riferimento per gli sviluppatori che scrivono applicazioni per .NET e cercano diversi modi per migliorare le prestazioni. Se si è uno sviluppatore che non ha familiarità con .NET, è consigliabile acquisire familiarità sia con la piattaforma che con il linguaggio preferito. Questo documento si basa strettamente su tale conoscenza e presuppone che il programmatore sappia già abbastanza per ottenere il programma in esecuzione. Se si esegue la conversione di un'applicazione esistente in .NET, vale la pena leggere questo documento prima di iniziare la porta. Alcuni dei suggerimenti riportati di seguito sono utili nella fase di progettazione e forniscono informazioni da tenere presente prima di iniziare la porta.

Questo documento è suddiviso in segmenti, con suggerimenti organizzati per progetto e tipo di sviluppatore. Il primo set di suggerimenti è un must-read per la scrittura in qualsiasi linguaggio e contiene consigli che consentono di usare qualsiasi linguaggio di destinazione in Common Language Runtime (CLR). Una sezione correlata segue con suggerimenti specifici di ASP. Il secondo set di suggerimenti è organizzato in base al linguaggio, con suggerimenti specifici sull'uso di Managed C++ e Microsoft® Visual Basic®.

A causa delle limitazioni di pianificazione, il tempo di esecuzione della versione 1 (v1) deve essere destinato prima alla funzionalità più ampia e quindi alle ottimizzazioni dei case speciali in un secondo momento. Ciò comporta alcuni casi in cui le prestazioni diventano un problema. Di conseguenza, questo documento illustra diversi suggerimenti progettati per evitare questo caso. Questi suggerimenti non saranno rilevanti nella versione successiva (vNext), poiché questi casi vengono identificati e ottimizzati sistematicamente. Lo farò notare come andiamo, ed è per voi decidere se vale la pena fare.

Suggerimenti sulle prestazioni per tutte le applicazioni

Esistono alcuni suggerimenti da ricordare quando si lavora su CLR in qualsiasi linguaggio. Questi sono rilevanti per tutti, e devono essere la prima linea di difesa quando si tratta di problemi di prestazioni.

Genera meno eccezioni

La generazione di eccezioni può essere molto costosa, quindi assicurarsi di non generare un sacco di loro. Usare Perfmon per verificare il numero di eccezioni generate dall'applicazione. Potrebbe sorprendere che alcune aree dell'applicazione generino più eccezioni di quanto previsto. Per una maggiore granularità, è anche possibile controllare il numero di eccezione a livello di codice usando i contatori delle prestazioni.

La ricerca e la progettazione di un codice elevato di eccezioni possono comportare una vittoria decente perf. Tenere presente che questo non ha nulla a che fare con i blocchi try/catch: si comporta solo il costo quando viene generata l'eccezione effettiva. È possibile usare il numero di blocchi try/catch desiderati. L'uso delle eccezioni gratuitamente è la posizione in cui si perderanno le prestazioni. Ad esempio, è consigliabile rimanere lontani da elementi come l'uso di eccezioni per il flusso di controllo.

Ecco un semplice esempio di come possono essere costose eccezioni: si eseguirà semplicemente un ciclo For , generando migliaia o eccezioni e quindi terminando. Provare a commentare l'istruzione throw per visualizzare la differenza in velocità: tali eccezioni comportano un sovraccarico enorme.

public static void Main(string[] args){
  int j = 0;
  for(int i = 0; i < 10000; i++){
    try{   
      j = i;
      throw new System.Exception();
    } catch {}
  }
  System.Console.Write(j);
  return;   
}
  • Attenzione! Il tempo di esecuzione può generare eccezioni in modo autonomo! Ad esempio, Response.Redirect() genera un'eccezione ThreadAbort . Anche se non si generano in modo esplicito eccezioni, è possibile usare le funzioni eseguite. Assicurarsi di controllare Perfmon per ottenere la storia reale e il debugger per controllare l'origine.
  • Per gli sviluppatori di Visual Basic: Visual Basic attiva il controllo int per impostazione predefinita, per assicurarsi che le operazioni come overflow e divide-by-zero generano eccezioni. È possibile disattivare questa opzione per ottenere prestazioni.
  • Se si usa COM, è consigliabile tenere presente che HRESULTS può restituire come eccezioni. Assicurarsi di tenere traccia di queste operazioni con attenzione.

Effettuare chiamate Chunky

Una chiamata a blocchi è una chiamata di funzione che esegue diverse attività, ad esempio un metodo che inizializza diversi campi di un oggetto. Ciò consiste nel visualizzare le chiamate chatty, che eseguono attività molto semplici e richiedono più chiamate per eseguire operazioni, ad esempio impostando ogni campo di un oggetto con una chiamata diversa. È importante eseguire chiamate chunky anziché chatty tra metodi in cui il sovraccarico è superiore a quello per le chiamate al metodo intra-AppDomain semplici. P/Invoke, interoperabilità e chiamate remoti tutte le chiamate di sovraccarico di trasporto e si vuole usarle in modo spasimo. In ognuno di questi casi, è consigliabile provare a progettare l'applicazione in modo che non si basa su chiamate piccole e frequenti che trasportano così tanto sovraccarico.

Una transizione si verifica ogni volta che il codice gestito viene chiamato dal codice non gestito e viceversa. Il tempo di esecuzione rende estremamente facile per il programmatore di eseguire l'interoperabilità, ma questo è a un prezzo di prestazioni. Quando si verifica una transizione, è necessario eseguire i passaggi seguenti:

  • Eseguire il marshalling dei dati
  • Correzione della convenzione di chiamata
  • Proteggere i registri salvati dal chiamante
  • Modalità thread switch in modo che GC non blocchi thread non gestiti
  • Erezione di un frame di gestione delle eccezioni nelle chiamate nel codice gestito
  • Controllo del thread (facoltativo)

Per velocizzare il tempo di transizione, provare a usare P/Invoke quando è possibile. Il sovraccarico è pari a 31 istruzioni e il costo del marshalling se è necessario il marshalling dei dati e solo 8 in caso contrario. L'interoperabilità COM è molto più costosa, prendendo fino a 65 istruzioni.

Il marshalling dei dati non è sempre costoso. I tipi primitivi richiedono quasi nessun marshalling a tutti e le classi con layout esplicito sono anche economici. Il rallentamento reale si verifica durante la traduzione dei dati, ad esempio la conversione del testo da ASCI a Unicode. Assicurarsi che i dati passati attraverso il limite gestito vengano convertiti solo se devono essere: può risultare che semplicemente accettando un determinato tipo di dati o formato nel programma è possibile tagliare un sacco di sovraccarico di marshalling.

I tipi seguenti sono chiamati blittable, ovvero possono essere copiati direttamente attraverso il limite gestito/non gestito senza alcun marshalling: sbyte, byte, short, ushort, int, uint, long, ulong, float e double. È possibile passarli gratuitamente, oltre a ValueTypes e matrici a dimensione singola contenenti tipi blittable. I dettagli gritty del marshalling possono essere esaminati ulteriormente in MSDN Library. Ti consigliamo di leggerlo attentamente se passi un sacco di tempo di marshalling.

Progettare con ValueTypes

Usare struct semplici quando è possibile e quando non si esegue un sacco di boxing e unboxing. Ecco un esempio semplice per illustrare la differenza di velocità:

using System;

console dello spazio dei nomiApplication{

  public struct foo{
    public foo(double arg){ this.y = arg; }
    public double y;
  }
  public class bar{
    public bar(double arg){ this.y = arg; }
    public double y;
  }
  class Class1{
    static void Main(string[] args){
      System.Console.WriteLine("starting struct loop...");
      for(int i = 0; i < 50000000; i++)
      {foo test = new foo(3.14);}
      System.Console.WriteLine("struct loop complete. 
                                starting object loop...");
      for(int i = 0; i < 50000000; i++)
      {bar test2 = new bar(3.14); }
      System.Console.WriteLine("All done");
    }
  }
}

Quando si esegue questo esempio, si noterà che il ciclo di struct è ordini di grandezza più veloce. Tuttavia, è importante tenere presente l'uso di ValueTypes quando si trattano come oggetti. In questo modo si aggiunge un sovraccarico di boxing e unboxing al programma e può costare più di quanto sarebbe se si fosse bloccato con oggetti! Per visualizzare questa operazione in azione, modificare il codice precedente per usare una matrice di foos e barre. Si noterà che le prestazioni sono più o meno uguali.

Compromessi I valoriType sono molto meno flessibili rispetto agli oggetti e finiscono per danneggiare le prestazioni se usati in modo errato. Devi essere molto attento su quando e su come usarli.

Provare a modificare l'esempio precedente e archiviare i foos e le barre all'interno di matrici o tabelle hash. Si noterà che il guadagno di velocità scompare, solo con un'operazione boxing e unboxing.

È possibile tenere traccia della quantità di caselle e di unbox esaminando le allocazioni e le raccolte GC. Questa operazione può essere eseguita usando Perfmon esternamente o contatori delle prestazioni nel codice.

Vedere la discussione approfondita di ValueTypes in Considerazioni sulle prestazioni delle tecnologie Run-Time in .NET Framework.

Usare AddRange per aggiungere gruppi

Usare AddRange per aggiungere un insieme intero anziché aggiungere ogni elemento nell'iterativa raccolta. Quasi tutti i controlli e le raccolte di Finestre hanno metodi Add e AddRange e ognuno è ottimizzato per uno scopo diverso. Add è utile per aggiungere un singolo elemento, mentre AddRange presenta un sovraccarico aggiuntivo, ma prevale quando si aggiungono più elementi. Di seguito sono riportate solo alcune classi che supportano Add e AddRange:

  • StringCollection, TraceCollection e così via.
  • HttpWebRequest
  • UserControl
  • ColumnHeader

Tagliare il working set

Ridurre al minimo il numero di assembly usati per mantenere ridotto il working set. Se si carica un intero assembly solo per usare un metodo, si paga un costo enorme per un vantaggio molto minimo. Verificare se è possibile duplicare la funzionalità del metodo usando il codice già caricato.

Tenere traccia del vostro set di lavoro è difficile, e probabilmente potrebbe essere l'oggetto di un intero documento. Ecco alcuni suggerimenti per aiutarti:

  • Usare vadump.exe per tenere traccia del working set. Questo argomento è illustrato in un altro white paper che illustra vari strumenti per l'ambiente gestito.
  • Esaminare i contatori delle prestazioni o Perfmon. Possono fornire feedback dettagliato sul numero di classi caricate o sul numero di metodi che ottengono JITed. È possibile ottenere letture per il tempo impiegato nel caricatore o la percentuale del tempo di esecuzione impiegato per il paging.

Usare i cicli For per l'iterazione di stringhe- versione 1

In C# la parola chiave foreach consente di esaminare gli elementi in un elenco, una stringa e così via. ed eseguire operazioni su ogni elemento. Si tratta di uno strumento molto potente, poiché funge da enumeratore per utilizzo generico su molti tipi. Il compromesso per questa generalizzazione è la velocità e se ci si basa principalmente sull'iterazione di stringa, è consigliabile usare invece un ciclo For . Poiché le stringhe sono matrici di caratteri semplici, possono essere visualizzate usando un sovraccarico molto inferiore rispetto ad altre strutture. JIT è abbastanza intelligente (in molti casi) per ottimizzare il controllo dei limiti di distanza e altre cose all'interno di un ciclo For , ma non è consentito eseguire questa operazione nelle passeggiate foreach . Il risultato finale è che nella versione 1, un ciclo For sulle stringhe è fino a cinque volte più veloce rispetto all'uso di foreach. Ciò cambierà nelle versioni future, ma per la versione 1 si tratta di un modo sicuro per migliorare le prestazioni.

Ecco un semplice metodo di test per illustrare la differenza di velocità. Provare a eseguirlo, quindi rimuovere il ciclo For e rimuovere il commento dall'istruzione foreach . Nel computer, il ciclo For ha richiesto circa un secondo, con circa 3 secondi per l'istruzione foreach .

public static void Main(string[] args) {
  string s = "monkeys!";
  int dummy = 0;

  System.Text.StringBuilder sb = new System.Text.StringBuilder(s);
  for(int i = 0; i < 1000000; i++)
    sb.Append(s);
  s = sb.ToString();
  //foreach (char c in s) dummy++;
  for (int i = 0; i < 1000000; i++)
    dummy++;
  return;   
  }
}

I compromessiForeach sono molto più leggibili e in futuro diventerà il più veloce di un ciclo For per casi speciali come stringhe. A meno che la manipolazione delle stringhe non sia un vero e proprio salto di prestazioni, il codice leggermente messia potrebbe non valerne la pena.

Usare StringBuilder per la manipolazione di stringhe complesse

Quando una stringa viene modificata, il runtime creerà una nuova stringa e la restituirà, lasciando l'originale come Garbage Collection. La maggior parte del tempo è un modo semplice e veloce per farlo, ma quando una stringa viene modificata ripetutamente, inizia a essere un onere sulle prestazioni: tutte queste allocazioni alla fine diventano costose. Ecco un semplice esempio di programma che aggiunge a una stringa 50.000 volte, seguito da uno che usa un oggetto StringBuilder per modificare la stringa sul posto. Il codice StringBuilder è molto più veloce e, se vengono eseguiti, diventa immediatamente ovvio.

namespace ConsoleApplication1.Feedback{
  using System;
  
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }
    public string text;
    public static int Main(string[] args) {
      Feedback test = new Feedback();
      String str = test.text;
      for(int i=0;i<50000;i++){
        str = str + "blue_toothbrush";
      }
      System.Console.Out.WriteLine("done");
      return 0;
    }
  }
}
namespace ConsoleApplication1.Feedback{
  using System;
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }
    public string text;
    public static int Main(string[] args) {
      Feedback test = new Feedback();
      System.Text.StringBuilder SB = 
        new System.Text.StringBuilder(test.text);
      for(int i=0;i<50000;i++){
        SB.Append("blue_toothbrush");
      }
      System.Console.Out.WriteLine("done");
      return 0;
    }
  }
}

Provare a esaminare Perfmon per vedere quanto tempo viene salvato senza allocare migliaia di stringhe. Esaminare il contatore "% time in GC" nell'elenco memoria CLR .NET. È anche possibile tenere traccia del numero di allocazioni salvate, nonché delle statistiche di raccolta.

Compromessi= La creazione di un oggetto StringBuilder comporta un sovraccarico, sia nel tempo che nella memoria. In un computer con memoria veloce, StringBuilder diventa utile se si eseguono circa cinque operazioni. Come regola generale, direi che 10 o più operazioni stringa è una giustificazione per l'overhead in qualsiasi computer, anche uno più lento.

Precompilare applicazioni Windows Forms

I metodi vengono JITed quando vengono usati per la prima volta, il che significa che si paga una penalità di avvio maggiore se l'applicazione esegue molte chiamate al metodo durante l'avvio. Windows Forms usare molte librerie condivise nel sistema operativo e il sovraccarico nell'avvio può essere molto più elevato rispetto ad altri tipi di applicazioni. Anche se non sempre il caso, la precompilazione delle applicazioni Windows Forms in genere comporta una vittoria delle prestazioni. In altri scenari è in genere preferibile lasciare che jit si occupi di esso, ma se si è un Windows Forms sviluppatore potrebbe essere utile esaminare.

Microsoft consente di precompilare un'applicazione chiamando ngen.exe. È possibile scegliere di eseguire ngen.exe durante l'installazione o prima di distribuire l'applicazione. È sicuramente più opportuno eseguire ngen.exe durante l'installazione, perché è possibile assicurarsi che l'applicazione sia ottimizzata per il computer in cui è installato. Se si esegue ngen.exe prima di spedire il programma, limitare le ottimizzazioni a quelle disponibili nel computer . Per darvi un'idea di quanto può essere utile la precompilazione, ho eseguito un test informale sul mio computer. Di seguito sono riportati i tempi di avvio a freddo per ShowFormComplex, un'applicazione winforms con circa un centinaio di controlli.

Stato del codice Ora
Framework JITed

ShowFormComplex JITed

3,4 sec
Framework Precompilato, ShowFormComplex JITed 2,5 sec
Precompilato framework, ShowFormComplex Precompilato 2.1sec

Ogni test è stato eseguito dopo un riavvio. Come si può notare, le applicazioni Windows Forms usano molti metodi in anticipo, rendendo più vantaggioso il precompilazione delle prestazioni.

Usare matrici irregolari- versione 1

Il jit v1 ottimizza le matrici irregolari (semplicemente "matrici di matrici") in modo più efficiente rispetto alle matrici rettangolari e la differenza è piuttosto evidente. Ecco una tabella che illustra il miglioramento delle prestazioni risultante dall'uso di matrici irregolari al posto di quelli rettangolari in C# e Visual Basic (i numeri più alti sono migliori):

  C# Visual Basic 7
Assegnazione (irregolare)

Assegnazione (rettangolare)

14.16

8.37

12.24

8.62

Rete neurale (irregolare)

Rete neurale (rettangolare)

4.48

3,00

4,58

3.13

Ordinamento numerico (frastagliato)

Ordinamento numerico (rettangolare)

4.88

2.05

5.07

2.06

Il benchmark di assegnazione è un semplice algoritmo di assegnazione, adattato dalla guida dettagliata disponibile in Quantitative Decision Making for Business (Gordon, Pressman e Cohn; Prentice-Hall; fuori stampa). Il test di rete neurale esegue una serie di modelli su una rete neurale di piccole dimensioni e l'ordinamento numerico è autoesplicativo. Presi insieme, questi benchmark rappresentano una buona indicazione delle prestazioni reali.

Come si può notare, l'uso di matrici irregolari può comportare un aumento delle prestazioni piuttosto significativo. Le ottimizzazioni apportate alle matrici irregolari verranno aggiunte alle versioni future di JIT, ma per la versione 1 è possibile risparmiare molto tempo usando matrici irregolari.

Mantenere le dimensioni del buffer di I/O tra 4 KB e 8 KB

Per quasi ogni applicazione, un buffer compreso tra 4 KB e 8 KB offrirà le prestazioni massime. Per le istanze molto specifiche, è possibile ottenere un miglioramento da un buffer più grande (caricando immagini di grandi dimensioni di dimensioni prevedibili, ad esempio), ma nel 99,99% dei casi si perderà solo memoria. Tutti i buffer derivati da BufferedStream consentono di impostare le dimensioni su qualsiasi elemento desiderato, ma nella maggior parte dei casi 4 e 8 offrono le migliori prestazioni.

Opportunità di I/O asincrone di Lookout for

In rari casi, è possibile trarre vantaggio dalle operazioni di I/O asincrone. Un esempio potrebbe essere scaricare e decomprimere una serie di file: è possibile leggere i bit in da un flusso, decodificarli nella CPU e scriverli in un altro. L'uso dell'I/O asincrono richiede molto impegno e può comportare una perdita di prestazioni se non viene eseguita correttamente. Il vantaggio è che, se applicato correttamente, L'I/O asincrona può offrire fino a dieci volte le prestazioni.

Un esempio eccellente di un programma che usa I/O asincrono è disponibile in MSDN Library.

  • Un aspetto da notare è che è presente un piccolo sovraccarico di sicurezza per le chiamate asincrone: quando si richiama una chiamata asincrona, lo stato di sicurezza dello stack del chiamante viene acquisito e trasferito al thread che eseguirà effettivamente la richiesta. Questo potrebbe non essere un problema se il callback esegue un numero elevato di codice o se le chiamate asincrone non vengono usate eccessivamente

Suggerimenti per Accesso al database

La filosofia di ottimizzazione per l'accesso al database consiste nell'usare solo le funzionalità necessarie e progettare un approccio "disconnesso": effettuare diverse connessioni in sequenza, invece di tenere aperta una singola connessione per molto tempo. È consigliabile prendere in considerazione questa modifica e progettarla.

Microsoft consiglia una strategia a più livelli per ottenere prestazioni massime, anziché una connessione diretta da client a database. Considera questo come parte della tua filosofia di progettazione, poiché molte delle tecnologie in atto sono ottimizzate per sfruttare uno scenario multi-stanco.

Usare il Provider gestito ottimale

Scegliere il provider gestito corretto anziché basarsi su una funzione di accesso generica. Esistono provider gestiti scritti in modo specifico per molti database diversi, ad esempio SQL (System.Data.SqlClient). Se si usa un'interfaccia più generica, ad esempio System.Data.Odbc, quando è possibile usare un componente specializzato, si perderanno le prestazioni con il livello aggiunto di riferimento indiretto. L'uso del provider ottimale può anche avere una lingua diversa: il client SQL gestito parla TDS a un database SQL, offrendo un miglioramento significativo rispetto al protocollo OleDbprotocol generico.

Selezionare lettore dati su set di dati quando è possibile

Usare un lettore di dati ogni volta che non è necessario mantenere i dati in giro. In questo modo è possibile leggere rapidamente i dati, che possono essere memorizzati nella cache se l'utente desidera. Un lettore è semplicemente un flusso senza stato che consente di leggere i dati non appena arriva e quindi di rilasciarli senza archiviarli in un set di dati per una maggiore navigazione. L'approccio al flusso è più veloce e ha meno sovraccarico, perché è possibile iniziare immediatamente a usare i dati. È consigliabile valutare la frequenza con cui sono necessari gli stessi dati per decidere se la memorizzazione nella cache per la navigazione è appropriata. Ecco una piccola tabella che illustra la differenza tra DataReader e DataSet in entrambi i provider ODBC e SQL durante il pull dei dati da un server (i numeri più alti sono migliori):

  ADO SQL
DataSet 801 2507
DataReader 1083 4585

Come si può notare, le prestazioni più elevate vengono ottenute quando si usa il provider gestito ottimale insieme a un lettore dati. Quando non è necessario memorizzare nella cache i dati, l'uso di un lettore di dati può offrire un aumento enorme delle prestazioni.

Usare Mscorsvr.dll per i computer MP

Per le applicazioni server e di livello intermedio autonomo, assicurarsi di mscorsvr essere usati per i computer multiprocessore. Mscorwks non è ottimizzato per il ridimensionamento o la velocità effettiva, mentre la versione del server include diverse ottimizzazioni che consentono di ridimensionare correttamente quando sono disponibili più processori.

Utilizzare stored procedure quando possibile

Le stored procedure sono strumenti altamente ottimizzati che comportano prestazioni eccellenti quando vengono usate in modo efficace. Configurare le stored procedure per gestire inserimenti, aggiornamenti ed eliminazioni con l'adattatore dati. Le stored procedure non devono essere interpretate, compilate o persino trasmesse dal client e ridurre il sovraccarico del traffico di rete e del server. Assicurarsi di usare CommandType.StoredProcedure anziché CommandType.Text

Prestare attenzione alle stringhe di connessione dinamiche

Il pool di connessioni è un modo utile per riutilizzare le connessioni per più richieste, anziché pagare l'overhead di apertura e chiusura di una connessione per ogni richiesta. Viene eseguita in modo implicito, ma si ottiene un pool per ogni stringa di connessione univoca. Se si generano stringhe di connessione in modo dinamico, assicurarsi che le stringhe siano identiche ogni volta che si verifica il pooling. Tenere presente anche che, se si verifica la delega, si otterrà un pool per utente. Sono disponibili numerose opzioni che è possibile impostare per il pool di connessioni ed è possibile tenere traccia delle prestazioni del pool usando Perfmon per tenere traccia di elementi come il tempo di risposta, le transazioni/sec e così via.

Disattivare le funzionalità che non si usano

Disattiva l'integrazione automatica delle transazioni se non è necessaria. Per l'Provider gestito SQL, questa operazione viene eseguita tramite la stringa di connessione:

SqlConnection conn = new SqlConnection(
"Server=mysrv01;
Integrated Security=true;
Enlist=false");

Quando si compila un set di dati con l'adattatore dati, non ottenere informazioni sulla chiave primaria se non è necessario (ad esempio, non impostare MissingSchemaAction.Add con chiave):

public DataSet SelectSqlSrvRows(DataSet dataset,string connection,string query){
    SqlConnection conn = new SqlConnection(connection);
    SqlDataAdapter adapter = new SqlDataAdapter();
    adapter.SelectCommand = new SqlCommand(query, conn);
    adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
    adapter.Fill(dataset);
    return dataset;
}

Evitare comandi generati automaticamente

Quando si usa un adattatore dati, evitare i comandi generati automaticamente. Questi richiedono viaggi aggiuntivi al server per recuperare i metadati e fornire un livello inferiore di controllo di interazione. Anche se l'uso dei comandi generati automaticamente è pratico, vale la pena di farlo manualmente nelle applicazioni critiche per le prestazioni.

Attenzione alla progettazione legacy di ADO

Tenere presente che quando si esegue un riempimento di comando o di chiamata nell'adattatore, viene restituito ogni record specificato dalla query.

Se i cursori del server sono assolutamente necessari, possono essere implementati tramite una stored procedure in t-sql. Evitare laddove possibile perché le implementazioni basate su cursore del server non vengono ridimensionate correttamente.

Se necessario, implementare il paging in modo senza stato e senza connessione. È possibile aggiungere altri record al set di dati tramite:

  • Assicurarsi che le informazioni PK siano presenti
  • Modifica del comando select dell'adattatore dati in base alle esigenze e
  • Chiamata di Fill

Mantenere snella i set di dati

Inserire solo i record necessari nel set di dati. Tenere presente che il set di dati archivia tutti i dati in memoria e che il maggior numero di dati richiesti, maggiore sarà il tempo necessario per la trasmissione attraverso la rete.

Usare l'accesso sequenziale il più spesso possibile

Con un lettore dati, usare CommandBehavior.SequentialAccess. Ciò è essenziale per gestire i tipi di dati BLOB perché consente di leggere i dati dalla rete in blocchi di piccole dimensioni. Anche se è possibile usare solo una parte dei dati alla volta, la latenza per il caricamento di un tipo di dati di grandi dimensioni scompare. Se non è necessario lavorare contemporaneamente l'intero oggetto, l'uso di Accesso sequenziale offre prestazioni molto migliori.

Suggerimenti sulle prestazioni per le applicazioni ASP.NET

Memorizzare nella cache in modo aggressivo

Quando si progetta un'app usando ASP.NET, assicurarsi di progettare con attenzione la memorizzazione nella cache. Nelle versioni server del sistema operativo sono disponibili numerose opzioni per modificare l'uso delle cache sul lato server e sul lato client. In ASP sono disponibili diverse funzionalità e strumenti che è possibile usare per ottenere prestazioni.

Memorizzazione nella cache di output: archivia il risultato statico di una richiesta ASP. Specificato usando la <@% OutputCache %> direttiva :

  • Durata: l'elemento Time esiste nella cache
  • VaryByParam: varia le voci della cache in base ai parametri Get/Post
  • VaryByHeader: varia le voci della cache in base all'intestazione Http
  • VaryByCustom: varia le voci della cache in base al browser
  • Eseguire l'override per variare in base a qualsiasi elemento desiderato:
    • Memorizzazione nella cache dei frammenti: quando non è possibile archiviare un'intera pagina (privacy, personalizzazione, contenuto dinamico), è possibile usare la memorizzazione nella cache dei frammenti per archiviare parti di esso per un recupero più rapido in un secondo momento.

      a) VaryByControl: varia gli elementi memorizzati nella cache in base ai valori di un controllo

    • API cache: offre granularità estremamente fine per la memorizzazione nella cache mantenendo una tabella hash degli oggetti memorizzati nella cache (System.web.UI.caching). E anche:

      a) Include dipendenze (chiave, file, ora)

      b) Scade automaticamente gli elementi inutilizzati

      c) Supporta i callback

La memorizzazione nella cache in modo intelligente può offrire prestazioni eccellenti ed è importante considerare il tipo di memorizzazione nella cache necessaria. Si immagini un sito di e-commerce complesso con diverse pagine statiche per l'accesso e quindi una sequenza di pagine generate dinamicamente contenenti immagini e testo. È possibile usare la memorizzazione nella cache di output per tali pagine di accesso e quindi memorizzazione nella cache dei frammenti per le pagine dinamiche. Una barra degli strumenti, ad esempio, può essere memorizzata nella cache come frammento. Per ottenere prestazioni ancora migliori, è possibile memorizzare nella cache le immagini di uso comune e il testo boilerplate visualizzati di frequente nel sito usando l'API Cache. Per informazioni dettagliate sulla memorizzazione nella cache (con codice di esempio), vedere il sito Web ASP. NET .

Usare lo stato della sessione solo se è necessario

Una funzionalità estremamente potente di ASP.NET è la possibilità di archiviare lo stato della sessione per gli utenti, ad esempio un carrello acquisti in un sito di e-commerce o una cronologia del browser. Poiché questa opzione è attivata per impostazione predefinita, si paga il costo in memoria anche se non lo si usa. Se non si usa lo stato sessione, disattivarlo e risparmiare il sovraccarico aggiungendo <@% EnabledSessionState = false %> all'asp. Sono disponibili diverse altre opzioni, illustrate nel sito Web ASP. NET.

Per le pagine che leggeno solo lo stato della sessione, è possibile scegliere EnabledSessionState=readonly. Ciò comporta un sovraccarico inferiore rispetto allo stato di sessione di lettura/scrittura completo ed è utile quando è necessaria solo una parte della funzionalità e non si vuole pagare per le funzionalità di scrittura.

Usare lo stato di visualizzazione solo se è necessario

Un esempio di Stato visualizzazione potrebbe essere un modulo lungo che gli utenti devono compilare: se fanno clic su Indietro nel browser e quindi restituiscono, il modulo rimarrà compilato. Quando questa funzionalità non viene usata, questo stato mangia memoria e prestazioni. Forse il più grande svuotamento delle prestazioni qui è che deve essere inviato un segnale round trip nella rete ogni volta che la pagina viene caricata per aggiornare e verificare la cache. Poiché è attiva per impostazione predefinita, è necessario specificare che non si vuole usare Visualizza stato con <@% EnabledViewState = false %>. Per altre informazioni sullo stato di visualizzazione nel sito Web ASP. NET , vedere alcune delle altre opzioni e impostazioni a cui si ha accesso.

Evitare STA COM

Apartment COM è progettato per gestire il threading in ambienti non gestiti. Esistono due tipi di Apartment COM: a thread singolo e multithreading. MTA COM è progettato per gestire il multithreading, mentre STA COM si basa sul sistema di messaggistica per serializzare le richieste di thread. Il mondo gestito è senza thread e l'uso di Single Threaded Apartment COM richiede che tutti i thread non gestiti condividono essenzialmente un singolo thread per l'interoperabilità. Ciò comporta un colpo di prestazioni elevato e dovrebbe essere evitato ogni volta che possibile. Se non è possibile convertire l'oggetto COM apartment nel mondo gestito, usare @%AspCompat = "true" %> per le pagine che li usano<. Per una spiegazione più dettagliata di STA COM, vedere MSDN Library.

Compilazione batch

Compilare sempre batch prima di distribuire una pagina di grandi dimensioni nel Web. Questa operazione può essere avviata eseguendo una richiesta a una pagina per directory e aspettando di nuovo l'idles della CPU. Ciò impedisce al server Web di essere inattivo con le compilazioni durante il tentativo di servire pagine.

Rimuovere moduli HTTP non necessari

A seconda delle funzionalità usate, rimuovere moduli HTTP inutilizzati o non necessari dalla pipeline. Il recupero della memoria aggiunta e dei cicli di spreco può offrire una piccola velocità di aumento della velocità.

Evitare la funzionalità Autoeventwireup

Invece di basarsi su autoeventwireup, eseguire l'override degli eventi da Page. Ad esempio, anziché scrivere un metodo Page_Load(), provare a eseguire l'overload del metodo Public void OnLoad(). In questo modo è possibile eseguire l'esecuzione da un oggetto CreateDelegate() per ogni pagina.

Codifica con ASCII quando non è necessario UTF

Per impostazione predefinita, ASP.NET viene configurato per codificare le richieste e le risposte come UTF-8. Se ASCII è tutte le esigenze dell'applicazione, l'overhead UTF può restituire alcuni cicli. Si noti che questa operazione può essere eseguita solo in base all'applicazione.

Usare la procedura di autenticazione ottimale

Esistono diversi modi per autenticare un utente e alcuni di più costosi di altri (in ordine di aumento dei costi: Nessuno, Windows, Forms, Passport). Assicurarsi di usare quello più economico adatto alle proprie esigenze.

Suggerimenti per la conversione e lo sviluppo in Visual Basic

Molto è cambiato sotto il cofano da Microsoft Visual Basic 6 a Microsoft®® Visual Basic®® 7 e la mappa delle prestazioni è cambiata con esso. A causa delle restrizioni di funzionalità e sicurezza aggiunte di CLR, alcune funzioni non possono essere eseguite rapidamente come hanno fatto in Visual Basic 6. Infatti, ci sono diverse aree in cui Visual Basic 7 viene tronato dal suo predecessore. Fortunatamente, ci sono due pezzi di buona notizia:

  • La maggior parte dei rallentamenti peggiori si verifica durante le funzioni una sola volta, ad esempio il caricamento di un controllo per la prima volta. Il costo è presente, ma lo paga solo una volta.
  • Esistono molte aree in cui Visual Basic 7 è più veloce e queste aree tendono a trovarsi in funzioni ripetute durante l'esecuzione. Ciò significa che il vantaggio cresce nel tempo e in diversi casi supera i costi di una sola volta.

La maggior parte dei problemi di prestazioni proviene da aree in cui il tempo di esecuzione non supporta una funzionalità di Visual Basic 6 e deve essere aggiunto per mantenere la funzionalità in Visual Basic 7. Lavorare al di fuori del tempo di esecuzione è più lento, rendendo alcune funzionalità molto più costose da usare. Il lato brillante è che è possibile evitare questi problemi con un po 'di sforzo. Esistono due aree principali che richiedono lavoro per ottimizzare le prestazioni e alcune semplici modifiche che è possibile eseguire qui e là. Insieme, questi consentono di aggirare gli svuotamenti delle prestazioni e di sfruttare le funzioni molto più veloci in Visual Basic 7.

Gestione degli errori

La prima preoccupazione è la gestione degli errori. Questo è cambiato molto in Visual Basic 7 e ci sono problemi di prestazioni correlati alla modifica. Essenzialmente, la logica necessaria per implementare OnErrorGoto e Resume è estremamente costosa. È consigliabile esaminare rapidamente il codice e evidenziare tutte le aree in cui si usa l'oggetto Err o qualsiasi meccanismo di gestione degli errori. Ora esaminare ognuna di queste istanze e verificare se è possibile riscriverle per usare try/catch. Molti sviluppatori troveranno che possono convertire in modo semplice il tentativo/catch per la maggior parte di questi casi e dovrebbero vedere un buon miglioramento delle prestazioni nel loro programma. La regola di identificazione è "se è possibile vedere facilmente la traduzione, farlo".

Ecco un esempio di un semplice programma Visual Basic che usa On Error Goto rispetto alla versione try/catch .

Sub SubWithError()
On Error Goto SWETrap
  Dim x As Integer
  Dim y As Integer
  x = x / y
SWETrap:  Exit Sub
  End Sub
 
Sub SubWithErrorResumeLabel()
  On Error Goto SWERLTrap
  Dim x As Integer
  Dim y As Integer
  x = x / y 
SWERLTrap:
  Resume SWERLExit
  End Sub
SWERLExit:
  Exit Sub
Sub SubWithError()
  Dim x As Integer
  Dim y As Integer
  Try    x = x / y  Catch    Return  End Try
  End Sub
 
Sub SubWithErrorResumeLabel()
  Dim x As Integer
  Dim y As Integer
  Try
    x = x / y
  Catch
  Goto SWERLExit
  End Try
 
SWERLExit:
  Return
  End Sub

L'aumento della velocità è evidente. SubWithError() richiede 244 millisecondi usando OnErrorGoto e solo 169 millisecondi usando try/catch. La seconda funzione richiede 179 millisecondi rispetto a 164 millisecondi per la versione ottimizzata.

Usare l'associazione anticipata

La seconda preoccupazione riguarda gli oggetti e il typecasting. Visual Basic 6 funziona molto sotto il cofano per supportare il cast di oggetti e molti programmatori non sono nemmeno consapevoli di esso. In Visual Basic 7 si tratta di un'area che si può spremere molte prestazioni. Quando si compila, usare l'associazione anticipata. Questo indica al compilatore di inserire una coercizione di tipo viene eseguita solo quando viene indicato in modo esplicito. Questo ha due effetti principali:

  • Gli errori strani diventano più facili da tenere traccia.
  • Le coercizioni non autorizzate vengono eliminate, causando miglioramenti sostanziali delle prestazioni.

Quando si usa un oggetto come se fosse di un tipo diverso, Visual Basic coercerà l'oggetto se non si specifica. Questo è utile, poiché il programmatore deve preoccuparsi di meno codice. Il lato negativo è che queste coercizioni possono fare cose impreviste e il programmatore non ha alcun controllo su di loro.

Esistono istanze quando è necessario usare l'associazione tardiva, ma la maggior parte del tempo se non si è certi di poter uscire con l'associazione anticipata. Per i programmatori di Visual Basic 6, questo può essere un po'imbarazzante per prima cosa, poiché è necessario preoccuparsi dei tipi più che in passato. Questo dovrebbe essere facile per i nuovi programmatori e le persone che hanno familiarità con Visual Basic 6 lo ritireranno in nessun tempo.

Attivare l'opzione Strict ed esplicito

Con Option Strict on, si protegge da un'associazione tardiva inavvertita e si applica un livello superiore di disciplina di codifica. Per un elenco delle restrizioni presenti con Option Strict, vedere MSDN Library. L'avviso a questo è che tutte le coercioni di tipo ristretto devono essere specificate in modo esplicito. Tuttavia, questo può scoprire altre sezioni del codice che stanno facendo più lavoro di quanto si era pensato in precedenza, e può aiutare a modificare alcuni bug nel processo.

L'opzione Esplicita è meno restrittiva di Option Strict, ma impone comunque ai programmatori di fornire altre informazioni nel codice. In particolare, è necessario dichiarare una variabile prima di usarla. In questo modo viene spostata l'inferenza del tipo dall'ora di esecuzione in fase di compilazione. Questo controllo eliminato si traduce in prestazioni aggiunte.

Ti consigliamo di iniziare con Option Explicit e quindi attivare Option Strict. In questo modo si proteggerà da unuge di errori del compilatore e consente di iniziare gradualmente a lavorare nell'ambiente più rigoroso. Quando vengono usate entrambe queste opzioni, è possibile garantire prestazioni massime per l'applicazione.

Usare il confronto binario per il testo

Quando si confronta il testo, usare il confronto binario anziché il confronto di testo. In fase di esecuzione, il sovraccarico è molto più leggero per binario.

Ridurre al minimo l'uso di Format()

Quando è possibile, usare toString() anziché format().. Nella maggior parte dei casi, fornisce le funzionalità necessarie, con un sovraccarico molto inferiore.

Usare Charw

Usare charw anziché char. CLR usa Unicode internamente e char deve essere convertito in fase di esecuzione se viene usato. Ciò può comportare una notevole perdita di prestazioni e specificare che i caratteri sono una parola completa lunga (l'uso di charw) elimina questa conversione.

Ottimizzare le assegnazioni

Usare exp += val anziché exp = exp + val. Poiché exp può essere arbitrariamente complesso, questo può comportare un sacco di lavoro non necessario. In questo modo il jit valuta entrambe le copie di exp e molte volte questo non è necessario. La prima istruzione può essere ottimizzata molto meglio del secondo, poiché JIT può evitare di valutare l'exp due volte.

Evitare l'inutile riferimento indiretto

Quando si usa byRef, si passano puntatori anziché l'oggetto effettivo. Molte volte questo ha senso (funzioni collaterali, ad esempio), ma non è sempre necessario. Il passaggio dei puntatori comporta un numero maggiore di riferimento indiretto, più lento rispetto all'accesso a un valore presente nello stack. Quando non è necessario passare attraverso l'heap, è consigliabile evitarlo.

Inserire le concatenazioni in un'unica espressione

Se sono presenti più concatenazioni su più righe, provare a incollarle tutte su un'unica espressione. Il compilatore può ottimizzare modificando la stringa sul posto, fornendo una velocità e un aumento della memoria. Se le istruzioni sono suddivise in più righe, il compilatore Visual Basic non genererà il linguaggio MSIL (Microsoft Intermediate Language) per consentire la concatenazione sul posto. Vedere l'esempio di StringBuilder illustrato in precedenza.

Includi istruzioni Return

Visual Basic consente a una funzione di restituire un valore senza utilizzare l'istruzione return . Anche se Visual Basic 7 supporta questa operazione, l'uso esplicito di return consente a JIT di eseguire leggermente più ottimizzazioni. Senza un'istruzione return, a ogni funzione vengono fornite diverse variabili locali nello stack per supportare in modo trasparente la restituzione di valori senza la parola chiave . Mantenere questi elementi in questo modo rende più difficile l'ottimizzazione jit e può influire sulle prestazioni del codice. Esaminare le funzioni e inserire come necessario. Non modifica affatto la semantica del codice e consente di ottenere una maggiore velocità dall'applicazione.

Suggerimenti per la conversione e lo sviluppo in C++ gestito

Microsoft ha come destinazione Managed C++ (MC++) in un set specifico di sviluppatori. MC++ non è lo strumento migliore per ogni processo. Dopo aver letto questo documento, è possibile decidere che C++ non è lo strumento migliore e che i costi di compromesso non valgono i vantaggi. Se non si è certi di MC++, ci sono molte risorse valide che consentono di prendere la decisione Questa sezione è destinata agli sviluppatori che hanno già deciso di usare MC++ in qualche modo e vogliono conoscere gli aspetti delle prestazioni.

Per gli sviluppatori C++, il funzionamento di C++ gestito richiede che vengano prese diverse decisioni. Si sta eseguendo la conversione di codice precedente? In tal caso, spostare l'intero elemento nello spazio gestito o si prevede di implementare un wrapper? Mi concentrerò sull'opzione "port-everything" o gestire la scrittura di MC++ da zero ai fini di questa discussione, poiché questi sono gli scenari in cui il programmatore noterà una differenza di prestazioni.

Vantaggi del mondo gestito

La funzionalità più potente di Managed C++ è la possibilità di combinare e associare codice gestito e non gestito a livello di espressione. Nessun altro linguaggio consente di eseguire questa operazione e ci sono alcuni vantaggi potenti che è possibile ottenere da esso se usato correttamente. Esaminerò alcuni esempi di questo più avanti.

Il mondo gestito ti dà anche enormi vittorie di progettazione, in che molti problemi comuni sono presi cura di te. La gestione della memoria, la pianificazione dei thread e la coercizione dei tipi possono essere lasciate al tempo di esecuzione se si desidera, consentendo di concentrare le energie sulle parti del programma che ne hanno bisogno. Con MC++, è possibile scegliere esattamente il controllo che si vuole mantenere.

I programmatori MC++ hanno il lusso di poter usare il back-end di Microsoft Visual C® 7 (VC7) durante la compilazione in IL e quindi l'uso di JIT sopra questo. I programmatori usati per lavorare con il compilatore Microsoft C++ vengono usati per eseguire operazioni veloci. Il JIT è stato progettato con obiettivi diversi e ha un set diverso di punti di forza e debolezza. Il compilatore VC7, non vincolato dalle restrizioni temporali di JIT, può eseguire determinate ottimizzazioni che jit non possono, ad esempio l'analisi dell'intero programma, l'inlining più aggressivo e l'iscrizione. Esistono anche alcune ottimizzazioni che possono essere eseguite solo in ambienti typesafe, lasciando più spazio alla velocità consentita da C++.

A causa delle diverse priorità nel JIT, alcune operazioni sono più veloci rispetto a prima, mentre altre sono più lente. Ci sono compromessi che si fanno per la sicurezza e la flessibilità del linguaggio, e alcuni di loro non sono economici. Fortunatamente, ci sono cose che un programmatore può fare per ridurre al minimo i costi.

Conversione: tutto il codice C++ può essere compilato in MSIL

Prima di procedere, è importante notare che è possibile compilare qualsiasi codice C++ in MSIL. Tutto funzionerà, ma non c'è garanzia di sicurezza dei tipi e si paga la penalità di marshalling se si esegue un sacco di interoperabilità. Perché è utile compilare in MSIL se non si ottengono vantaggi? Nelle situazioni in cui si esegue la conversione di una codebase di grandi dimensioni, ciò consente di convertire gradualmente il codice in parti. È possibile dedicare più tempo alla conversione di codice, invece di scrivere wrapper speciali per associare il codice convertito e non ancora convertito insieme se si usa MC++, e questo può comportare una grande vittoria. Rende le applicazioni di conversione un processo molto pulito. Per altre informazioni sulla compilazione di C++ in MSIL, vedere l'opzione del compilatore /clr.

Tuttavia, la compilazione del codice C++ in MSIL non offre la sicurezza o la flessibilità del mondo gestito. È necessario scrivere in MC++e nella versione 1 significa rinunciare ad alcune funzionalità. L'elenco seguente non è supportato nella versione corrente di CLR, ma potrebbe essere in futuro. Microsoft ha scelto di supportare prima le funzionalità più comuni e ha dovuto tagliare alcuni altri per la spedizione. Non c'è nulla che impedisce loro di essere aggiunti in un secondo momento, ma nel frattempo è necessario eseguire senza di essi:

  • Ereditarietà multipla
  • Modelli
  • Finalizzazione deterministica

È sempre possibile interagire con codice non sicuro se sono necessarie queste funzionalità, ma si pagherà la penalità delle prestazioni del marshalling dei dati in avanti e indietro. Tenere presente che tali funzionalità possono essere usate solo all'interno del codice non gestito. Lo spazio gestito non ha alcuna conoscenza della loro esistenza. Se si decide di convertire il codice, considerare quanto si fa affidamento su tali funzionalità nella progettazione. In alcuni casi, la riprogettazione è troppo costosa e si vuole attenersi al codice non gestito. Questa è la prima decisione che dovresti prendere, prima di iniziare l'hacking.

Vantaggi di MC++ su C# o Visual Basic

Proveniente da uno sfondo non gestito, MC++ mantiene molte delle possibilità di gestire codice non sicuro. La capacità di MC++di combinare senza problemi il codice gestito e non gestito offre allo sviluppatore una notevole potenza ed è possibile scegliere dove si desidera inserire il gradiente durante la scrittura del codice. In un estremo, è possibile scrivere tutto in modo semplice, non pianificato C++ e semplicemente compilare con /clr. In alternativa, è possibile scrivere tutto come oggetti gestiti e gestire le limitazioni del linguaggio e i problemi di prestazioni indicati in precedenza.

Ma la vera potenza di MC++ viene quando si sceglie da qualche parte tra. MC++ consente di modificare alcuni dei riscontri delle prestazioni intrinseci nel codice gestito, offrendo un controllo preciso su quando usare funzionalità non sicure. C# ha alcune di queste funzionalità nella parola chiave unsafe , ma non è parte integrante del linguaggio ed è molto meno utile di MC++. Verranno ora illustrati alcuni esempi che illustrano la granularità più fine disponibile in MC++e verranno illustrate le situazioni in cui è utile.

Puntatori "byref" generalizzati

In C# è possibile accettare solo l'indirizzo di un membro di una classe passandolo a un parametro ref . In MC++, un puntatore byref è un costrutto di prima classe. È possibile accettare l'indirizzo di un elemento al centro di una matrice e restituire tale indirizzo da una funzione:

Byte* AddrInArray( Byte b[] ) {
   return &b[5];
}

Questa funzionalità viene sfruttata per restituire un puntatore ai "caratteri" in un system.String tramite la routine helper ed è anche possibile scorrere le matrici usando questi puntatori:

System::Char* PtrToStringChars(System::String*);   
for( Char*pC = PtrToStringChars(S"boo");
  pC != NULL;
  pC++ )
{
      ... *pC ...
}

È anche possibile eseguire un attraversamento dell'elenco collegato con inserimento in MC++ prendendo l'indirizzo del campo "next" (che non è possibile eseguire in C#):

Node **w = &Head;
while(true) {
  if( *w == 0 || val < (*w)->val ) {
    Node *t = new Node(val,*w);
    *w = t;
    break;
  }
  w = &(*w)->next;
}

In C#, non è possibile puntare a "Head" o prendere l'indirizzo del campo "next", quindi è necessario creare un caso speciale in cui si inserisce nella prima posizione o se "Head" è Null. Inoltre, è necessario esaminare un nodo in anticipo tutto il tempo nel codice. Confrontare questo risultato con quello che un buon C# produrrebbe:

if( Head==null || val < Head.val ) {
  Node t = new Node(val,Head);
  Head = t;
}else{
  // we know at least one node exists,
  // so we can look 1 node ahead
  Node w=Head;
while(true) {
  if( w.next == null || val < w.next.val ){
    Node t = new Node(val,w.next.next);
    w.next = t;
    break;
  }
  w = w.next;
  }
}         

Accesso utente a tipi boxed

Un problema di prestazioni comune con le lingue OO è il tempo impiegato per il boxing e i valori di unboxing. MC++ offre un maggiore controllo su questo comportamento, quindi non è necessario eseguire la posta in arrivo in modo dinamico (o statico) per accedere ai valori. Si tratta di un altro miglioramento delle prestazioni. Posizionare __box parola chiave prima di qualsiasi tipo per rappresentare il formato boxed:

__value struct V {
  int i;
};
int main() {
  V v = {10};
  __box V *pbV = __box(v);
  pbV->i += 10;           // update without casting
}

In C# è necessario annullare la posta in arrivo in "v", quindi aggiornare il valore e tornare a un oggetto:

struct B { public int i; }
static void Main() {
  B b = new B();
  b.i = 5;
  object o = b;         // implicit box
  B b2 = (B)o;            // explicit unbox
  b2.i++;               // update
  o = b2;               // implicit re-box
}

Raccolte STL e raccolte gestite- v1

La cattiva notizia: in C++, l'uso delle raccolte STL era spesso altrettanto veloce quanto la scrittura manuale di tale funzionalità. I framework CLR sono molto veloci, ma soffrono di problemi di boxing e unboxing: tutto è un oggetto e senza modello o supporto generico, tutte le azioni devono essere controllate in fase di esecuzione.

La buona notizia: a lungo termine, è possibile scommettere che questo problema andrà via man mano che i generics vengono aggiunti al runtime. Il codice distribuito oggi sperimenterà l'aumento della velocità senza modifiche. A breve termine, è possibile usare il cast statico per impedire il controllo, ma questo non è più sicuro. È consigliabile usare questo metodo in codice stretto in cui le prestazioni sono assolutamente critiche e sono state identificate due o tre aree sensibili.

Usare oggetti gestiti stack

In C++, specificare che un oggetto deve essere gestito dallo stack o dall'heap. È comunque possibile eseguire questa operazione in MC++, ma è necessario tenere presente alcune restrizioni. CLR usa ValueTypes per tutti gli oggetti gestiti dallo stack e sono previste limitazioni per le operazioni che i ValueType possono eseguire (ad esempio, nessuna ereditarietà). Altre informazioni sono disponibili in MSDN Library.

Caso angolo: Tenere presente le chiamate indirette all'interno del codice gestito- v1

Nel tempo di esecuzione v1 tutte le chiamate di funzione indirette vengono eseguite in modo nativo e quindi richiedono una transizione nello spazio non gestito. Qualsiasi chiamata di funzione indiretta può essere eseguita solo dalla modalità nativa, che significa che tutte le chiamate indirette dal codice gestito necessitano di una transizione gestita a non gestita. Si tratta di un problema grave quando la tabella restituisce una funzione gestita, poiché deve essere eseguita una seconda transizione per eseguire la funzione. Rispetto al costo dell'esecuzione di una singola istruzione Call , il costo è di 100 volte più lento rispetto a C++.

Fortunatamente, quando si chiama un metodo che si trova all'interno di una classe garbage collection, l'ottimizzazione rimuove questa operazione. Tuttavia, nel caso specifico di un normale file C++ compilato usando /clr, il metodo restituito verrà considerato gestito. Poiché questa operazione non può essere rimossa dall'ottimizzazione, si raggiunge il costo di transizione doppia completo. Di seguito è riportato un esempio di questo caso.

//////////////////////// a.h:    //////////////////////////
class X {
public:
   void mf1();
   void mf2();
};

typedef void (X::*pMFunc_t)();


////////////// a.cpp: compiled with /clr  /////////////////
#include "a.h"

int main(){
   pMFunc_t pmf1 = &X::mf1;
   pMFunc_t pmf2 = &X::mf2;

   X *pX = new X();
   (pX->*pmf1)();
   (pX->*pmf2)();

   return 0;
}


////////////// b.cpp: compiled without /clr /////////////////
#include "a.h"

void X::mf1(){}


////////////// c.cpp: compiled with /clr ////////////////////
#include "a.h"
void X::mf2(){}

Esistono diversi modi per evitare questo:

  • Rendere la classe in una classe gestita ("__gc")
  • Rimuovere la chiamata indiretta, se possibile
  • Lasciare la classe compilata come codice non gestito (ad esempio, non usare /clr)

Ridurre al minimo i risultati delle prestazioni: versione 1

Esistono diverse operazioni o funzionalità che sono semplicemente più costose in MC++ nella versione 1 JIT. Li listerò e darò una spiegazione, e poi parleremo di ciò che si può fare su di loro.

  • Astrazioni: si tratta di un'area in cui il compilatore back-end C++ lento vince pesantemente sul JIT. Se si esegue il wrapping di un oggetto int all'interno di una classe per scopi di astrazione e si accede strettamente come int, il compilatore C++ può ridurre il sovraccarico del wrapper a praticamente nulla. È possibile aggiungere molti livelli di astrazione al wrapper, senza aumentare il costo. Il JIT non è in grado di richiedere il tempo necessario per eliminare questo costo, rendendo più costose astrazioni profonde in MC++.
  • Virgola mobile: il JIT v1 non esegue attualmente tutte le ottimizzazioni specifiche di FP eseguite dal back-end VC++, rendendo le operazioni a virgola mobile più costose per il momento.
  • Matrici multidimensionali: il JIT è migliore nella gestione di matrici frastagliate rispetto a quelle multidimensionali, quindi usare invece matrici frastagliate.
  • Aritmetica a 64 bit: nelle versioni future verranno aggiunte ottimizzazioni a 64 bit al JIT.

Cosa è possibile fare

In ogni fase di sviluppo, esistono diverse operazioni che è possibile eseguire. Con MC++, la fase di progettazione è forse l'area più importante, perché determina la quantità di lavoro che si termina e la quantità di prestazioni restituite. Quando ci siede per scrivere o convertire un'applicazione, è consigliabile prendere in considerazione le operazioni seguenti:

  • Identificare le aree in cui si usano più ereditarietà, modelli o finalizzazione deterministica. Sarà necessario eliminare questi elementi oppure lasciare tale parte del codice nello spazio non gestito. Considerare il costo della riprogettazione e identificare le aree che possono essere convertite.
  • Individuare i punti caldi sulle prestazioni, ad esempio astrazioni profonde o chiamate di funzione virtuali nello spazio gestito. Questi richiederanno anche una decisione di progettazione.
  • Cercare gli oggetti specificati come gestiti dallo stack. Assicurarsi che possano essere convertiti in ValueTypes. Contrassegnare gli altri per la conversione in oggetti gestiti dall'heap.

Durante la fase di codifica, è consigliabile essere consapevoli delle operazioni più costose e delle opzioni che si hanno a disposizione. Una delle cose più belle di MC++ è che si arriva a prendere in mano tutti i problemi di prestazioni in primo piano, prima di iniziare a scrivere codice: questo è utile per ridurre il lavoro più avanti. Tuttavia, sono ancora disponibili alcune modifiche che è possibile eseguire durante il codice e il debug.

Determinare quali aree usano pesantemente le funzioni aritmetiche a virgola mobile, matrici multidimensionali o libreria. Quale di queste aree sono critiche per le prestazioni? Usare i profiler per selezionare i frammenti in cui il sovraccarico costa la maggior parte e selezionare quale opzione sembra migliore:

  • Mantenere l'intero frammento nello spazio non gestito.
  • Usare cast statici sugli accessi alla libreria.
  • Provare a modificare il comportamento di boxing/unboxing (spiegato più avanti).
  • Codice della propria struttura.

Infine, lavorare per ridurre al minimo il numero di transizioni che si esegue. Se si dispone di un codice non gestito o di una chiamata di interoperabilità seduta in un ciclo, rendere l'intero ciclo non gestito. In questo modo si pagherà due volte il costo della transizione, anziché per ogni iterazione del ciclo.

Risorse aggiuntive

Gli argomenti correlati sulle prestazioni in .NET Framework includono:

Guarda gli articoli futuri attualmente in fase di sviluppo, inclusa una panoramica delle filosofie di progettazione, architettura e codifica, una procedura dettagliata degli strumenti di analisi delle prestazioni nel mondo gestito e un confronto delle prestazioni di .NET con altre applicazioni aziendali disponibili oggi.

Appendice: Costo di chiamate virtuali e allocazioni

Tipo di chiamata # Chiamate/sec
Chiamata non virtuale ValueType 809971805.600
Chiamata non virtuale di classe 268478412.546
Chiamata virtuale classe 109117738.369
Chiamata di ValueType Virtual (metodo Obj) 3004286.205
Chiamata di ValueType Virtual (metodo Obj sottoposto a override) 2917140.844
Tipo di carico in base al nuovo (non statico) 1434.720
Tipo di carico per newing (metodi virtuali) 1369.863

Nota Il computer di test è un'istanza piii 733Mhz, che esegue Windows 2000 Professional con Service Pack 2.

Questo grafico confronta il costo associato a diversi tipi di chiamate di metodo, nonché il costo di creazione di un'istanza di un tipo che contiene metodi virtuali. Maggiore è il numero, è possibile eseguire più chiamate/istanze al secondo. Anche se questi numeri variano certamente su computer e configurazioni diverse, il costo relativo dell'esecuzione di una chiamata su un altro rimane significativo.

  • ValueType Non Virtual Call: questo test chiama un metodo non virtuale vuoto contenuto in un valore ValueType.
  • Classe Non virtuale Chiamata: questo test chiama un metodo non virtuale vuoto contenuto all'interno di una classe.
  • Chiamata virtuale classe: questo test chiama un metodo virtuale vuoto contenuto all'interno di una classe.
  • Chiamata a ValueType Virtual (Metodo Obj): questo test chiama ToString() (un metodo virtuale) in un valueType, che fa riferimento al metodo oggetto predefinito.
  • Chiamata di ValueType Virtual (Metodo Obj sottoposto a override): questo test chiama ToString() (un metodo virtuale) in un ValueType che ha eseguito l'override dell'impostazione predefinita.
  • Load Type by Newing (Static): questo test alloca spazio per una classe con solo metodi statici.
  • Tipo di carico per newing (metodi virtuali): questo test alloca lo spazio per una classe con metodi virtuali.

Una conclusione che è possibile disegnare è che le chiamate di funzione virtuale sono circa due volte più costose delle chiamate regolari quando si chiama un metodo in una classe. Tenere presente che le chiamate sono a buon mercato per iniziare, quindi non rimuoverei tutte le chiamate virtuali. È consigliabile usare sempre metodi virtuali quando è consigliabile farlo.

  • I metodi virtuali JIT non possono essere inline, quindi si perde un'ottimizzazione potenziale se si eliminano metodi non virtuali.
  • Lo spazio di allocazione per un oggetto con metodi virtuali è leggermente più lento rispetto all'allocazione di un oggetto senza di essi, poiché è necessario eseguire operazioni aggiuntive per trovare spazio per le tabelle virtuali.

Si noti che la chiamata di un metodo non virtuale all'interno di un valore ValueType è superiore a tre volte più veloce di una classe, ma una volta considerata come una classe che si perde terribilmente. Questa è la caratteristica dei ValueType: trattarli come struct e sono veloci. Trattarli come classi e sono dolorosamente lenti. ToString() è un metodo virtuale, quindi prima di poterlo chiamare, lo struct deve essere convertito in un oggetto nell'heap. Invece di essere due volte più lento, chiamare un metodo virtuale in un ValueType è ora diciotto volte più lento! Morale della storia? Non considerare ValueType come classi.

Se si hanno domande o commenti su questo articolo, contattare Claudio Caldato, program manager per i problemi di prestazioni di .NET Framework.