Performance Tips and Tricks in .NET Applications
Emmanuel Schanzer
Microsoft Corporation
Août 2001
Résumé: Cet article est destiné aux développeurs qui souhaitent ajuster leurs applications pour des performances optimales dans le monde managé. Des exemples de code, des explications et des instructions de conception sont traités pour les applications Base de données, Windows Forms et ASP, ainsi que des conseils spécifiques au langage pour Microsoft Visual Basic et Managed C++. (25 pages imprimées)
Contenu
Vue d’ensemble
Conseils sur les performances pour toutes les applications
Conseils pour l’accès à la base de données
Conseils sur les performances pour les applications ASP.NET
Conseils pour le portage et le développement en Visual Basic
Conseils pour le portage et le développement en C++ managé
Ressources supplémentaires
Annexe : Coût des appels virtuels et des allocations
Vue d’ensemble
Ce livre blanc est conçu comme référence pour les développeurs qui écrivent des applications pour .NET et recherchent différentes façons d’améliorer les performances. Si vous êtes un développeur qui ne connaît pas .NET, vous devez être familiarisé avec la plateforme et le langage de votre choix. Cet article s’appuie strictement sur ces connaissances et suppose que le programmeur en sait déjà suffisamment pour que le programme soit exécuté. Si vous transférez une application existante vers .NET, il est utile de lire ce document avant de commencer le port. Certains des conseils ici sont utiles dans la phase de conception et fournissent des informations que vous devez connaître avant de commencer le port.
Cet article est divisé en segments, avec des conseils organisés par type de projet et de développeur. Le premier ensemble de conseils est un must-read pour écrire dans n’importe quelle langue, et contient des conseils qui vous aideront avec n’importe quelle langue cible sur le Common Language Runtime (CLR). Une section associée suit avec des conseils spécifiques à ASP. Le deuxième ensemble de conseils est organisé par langage, traitant de conseils spécifiques sur l’utilisation de Managed C++ et Microsoft® Visual Basic®.
En raison des limitations de planification, le temps d’exécution de la version 1 (v1) devait d’abord cibler les fonctionnalités les plus larges, puis traiter les optimisations de cas spéciaux ultérieurement. Il en résulte quelques cas où les performances deviennent un problème. En tant que tel, cet article couvre plusieurs conseils conçus pour éviter ce cas. Ces conseils ne seront pas pertinents dans la prochaine version (vNext), car ces cas sont systématiquement identifiés et optimisés. Je les signalerai au fur et à mesure, et c’est à vous de décider si cela en vaut la peine.
Conseils sur les performances pour toutes les applications
Il existe quelques conseils à retenir lorsque vous travaillez sur le CLR dans n’importe quelle langue. Ceux-ci sont pertinents pour tout le monde et doivent être la première ligne de défense lors de la gestion des problèmes de performances.
Lever moins d’exceptions
La levée d’exceptions peut être très coûteuse. Veillez donc à ne pas en lever beaucoup. Utilisez Perfmon pour voir le nombre d’exceptions levées par votre application. Il peut vous surprendre de constater que certaines zones de votre application lèvent plus d’exceptions que prévu. Pour une meilleure granularité, vous pouvez également case activée le numéro d’exception par programmation à l’aide de compteurs de performances.
La recherche et la conception d’un code lourd d’exceptions peuvent entraîner un gain de performances décent. Gardez à l’esprit que cela n’a rien à voir avec les blocs try/catch : vous n’encourez le coût que lorsque l’exception réelle est levée. Vous pouvez utiliser autant de blocs try/catch que vous le souhaitez. L’utilisation gratuite d’exceptions est l’endroit où vous perdez les performances. Par exemple, vous devez vous tenir à l’écart des éléments tels que l’utilisation d’exceptions pour le flux de contrôle.
Voici un exemple simple de la façon dont les exceptions peuvent être coûteuses : nous allons simplement exécuter une boucle For , en générant des milliers ou des exceptions, puis en terminant. Essayez de commenter l’instruction throw pour voir la différence de vitesse : ces exceptions entraînent une surcharge considérable.
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;
}
- Méfiez-vous! Le temps d’exécution peut lever des exceptions par lui-même ! Par exemple, Response.Redirect() lève une exception ThreadAbort . Même si vous ne lèvez pas explicitement d’exceptions, vous pouvez utiliser des fonctions qui le font. Veillez à case activée Perfmon pour obtenir l’histoire réelle et que le débogueur case activée la source.
- Pour les développeurs Visual Basic : Visual Basic active la vérification int par défaut pour s’assurer que des éléments tels que le dépassement de capacité et la division par zéro lèvent des exceptions. Vous souhaiterez peut-être désactiver cette option pour obtenir des performances.
- Si vous utilisez COM, gardez à l’esprit que HRESULTS peut retourner en tant qu’exceptions. Assurez-vous de suivre ces informations avec soin.
Passer des appels segments
Un appel segmenté est un appel de fonction qui effectue plusieurs tâches, telles qu’une méthode qui initialise plusieurs champs d’un objet. Cela doit être vu par rapport aux appels bavards, qui effectuent des tâches très simples et nécessitent plusieurs appels pour effectuer les tâches (par exemple, définir chaque champ d’un objet avec un appel différent). Il est important d’effectuer des appels segmentés plutôt que bavards entre les méthodes pour lesquelles la surcharge est plus élevée que pour les appels de méthode intra-AppDomain simples. Les appels P/Invoke, d’interopérabilité et de communication à distance sont tous porteurs de surcharge, et vous souhaitez les utiliser avec parcimonie. Dans chacun de ces cas, vous devez essayer de concevoir votre application afin qu’elle ne repose pas sur de petits appels fréquents qui entraînent tant de surcharge.
Une transition se produit chaque fois que du code managé est appelé à partir de code non managé, et vice versa. Le temps d’exécution rend extrêmement facile pour le programmeur de faire de l’interopérabilité, mais cela est à un prix de performance. Lorsqu’une transition se produit, les étapes suivantes doivent être effectuées :
- Effectuer le marshaling des données
- Corriger la convention d’appel
- Protéger les registres enregistrés par les personnes appelées
- Changer de mode de thread pour que GC ne bloque pas les threads non managés
- Ériger un cadre de gestion des exceptions sur les appels en code managé
- Prendre le contrôle du thread (facultatif)
Pour accélérer le temps de transition, essayez d’utiliser P/Invoke quand vous le pouvez. La surcharge est de seulement 31 instructions, plus le coût du marshaling si le marshaling de données est nécessaire, et seulement 8 dans le cas contraire. COM Interop est beaucoup plus coûteux, prenant plus de 65 instructions.
Le marshaling des données n’est pas toujours coûteux. Les types primitifs ne nécessitent presque aucun marshaling, et les classes avec disposition explicite sont également bon marché. Le ralentissement réel se produit lors de la traduction de données, comme la conversion de texte d’ASCI en Unicode. Assurez-vous que les données passées au-delà de la limite managée ne sont converties que si elles doivent l’être : il peut s’avérer qu’en convenant d’un certain type de données ou format dans votre programme, vous pouvez supprimer beaucoup de surcharge de marshaling.
Les types suivants sont appelés blittable, ce qui signifie qu’ils peuvent être copiés directement au-delà de la limite managée/non managée sans aucun marshaling : sbyte, byte, short, ushort, int, uint, long, ulong, float et double. Vous pouvez les transmettre gratuitement, ainsi que des ValueTypes et des tableaux unidimensionnels contenant des types blittables. Les détails du marshaling peuvent être explorés plus en détail dans MSDN Library. Je vous recommande de le lire attentivement si vous passez beaucoup de votre temps à marshaler.
Concevoir avec ValueTypes
Utilisez des structs simples quand vous le pouvez, et quand vous ne faites pas beaucoup de boxe et de unboxing. Voici un exemple simple pour illustrer la différence de vitesse :
using System;
console d’espace de nomsApplication{
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");
}
}
}
Lorsque vous exécutez cet exemple, vous verrez que la boucle de struct est plus rapide. Toutefois, il est important de se méfier de l’utilisation de ValueTypes lorsque vous les traitez comme des objets. Cela ajoute une surcharge supplémentaire de boxe et de unboxing à votre programme, et peut finir par vous coûter plus cher que si vous aviez bloqué avec des objets! Pour voir cela en action, modifiez le code ci-dessus pour utiliser un tableau de foos et de barres. Vous constaterez que les performances sont plus ou moins égales.
Compromis Les valueTypes sont beaucoup moins flexibles que les objets et finissent par nuire aux performances s’ils sont utilisés de manière incorrecte. Vous devez être très prudent quant au moment et à la façon dont vous les utilisez.
Essayez de modifier l’exemple ci-dessus et de stocker les foos et les barres dans des tableaux ou des tables de hachage. Vous verrez le gain de vitesse disparaître, juste avec une seule opération de boxe et de déballage.
Vous pouvez suivre la façon dont vous boxez et unboxez en examinant les allocations et les regroupements de GC. Pour ce faire, vous pouvez utiliser Perfmon en externe ou des compteurs de performances dans votre code.
Consultez la discussion approfondie des ValueTypes dans Considérations sur les performances des technologies Run-Time dans le .NET Framework.
Utiliser AddRange pour ajouter des groupes
Utilisez AddRange pour ajouter une collection entière, plutôt que d’ajouter chaque élément de la collection de manière itérative. Presque tous les contrôles et collections windows ont à la fois des méthodes Add et AddRange , et chacune est optimisée pour un objectif différent. Ajouter est utile pour ajouter un seul élément, tandis que AddRange a une surcharge supplémentaire, mais est gagnant lors de l’ajout de plusieurs éléments. Voici quelques-unes des classes qui prennent en charge Add et AddRange :
- StringCollection, TraceCollection, etc.
- HttpWebRequest
- UserControl
- ColumnHeader
Découper votre groupe de travail
Réduisez le nombre d’assemblys que vous utilisez pour que votre ensemble de travail reste petit. Si vous chargez un assembly entier pour utiliser une seule méthode, vous payez un coût énorme pour très peu d’avantages. Vérifiez si vous pouvez dupliquer les fonctionnalités de cette méthode à l’aide du code que vous avez déjà chargé.
Il est difficile de suivre votre ensemble de travail et pourrait probablement faire l’objet d’un article entier. Voici quelques conseils pour vous aider :
- Utilisez vadump.exe pour suivre votre jeu de travail. Cela est abordé dans un autre livre blanc couvrant différents outils pour l’environnement managé.
- Examinez Perfmon ou Compteurs de performances. Ils peuvent vous fournir des commentaires détaillés sur le nombre de classes que vous chargez ou le nombre de méthodes qui obtiennent JITed. Vous pouvez obtenir des lectures pour le temps que vous passez dans le chargeur ou le pourcentage de votre temps d’exécution consacré à la pagination.
Utiliser les boucles For pour l’itération de chaîne ( version 1)
En C#, le mot clé foreach vous permet de parcourir les éléments d’une liste, d’une chaîne, etc. et d’effectuer des opérations sur chaque élément. Il s’agit d’un outil très puissant, car il agit comme énumérateur à usage général sur de nombreux types. Le compromis pour cette généralisation est la vitesse, et si vous comptez fortement sur l’itération de chaîne, vous devez utiliser une boucle For à la place. Étant donné que les chaînes sont des tableaux de caractères simples, elles peuvent être parcourues en utilisant beaucoup moins de surcharge que d’autres structures. Le JIT est assez intelligent (dans de nombreux cas) pour optimiser la vérification des limites et d’autres choses à l’intérieur d’une boucle For , mais il est interdit de le faire sur les marches foreach . Le résultat final est que dans la version 1, une boucle For sur des chaînes est jusqu’à cinq fois plus rapide que l’utilisation de foreach. Cela va changer dans les versions ultérieures, mais pour la version 1, il s’agit d’un moyen certain d’augmenter les performances.
Voici une méthode de test simple pour illustrer la différence de vitesse. Essayez de l’exécuter, puis de supprimer la boucle For et de supprimer les marques de commentaire de l’instruction foreach . Sur ma machine, la boucle For a pris environ une seconde, avec environ 3 secondes pour l’instruction 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;
}
}
Les compromisForeach
sont beaucoup plus lisibles et, à l’avenir, il deviendra aussi rapide qu’une boucle For pour des cas spéciaux comme des chaînes. À moins que la manipulation des chaînes ne soit un véritable porc de performance pour vous, le code légèrement messier peut ne pas en valoir la peine.
Utiliser StringBuilder pour la manipulation de chaînes complexes
Lorsqu’une chaîne est modifiée, l’heure d’exécution crée une chaîne et la retourne, laissant l’original à récupérer par mémoire. La plupart du temps, il s’agit d’un moyen simple et rapide de le faire, mais quand une chaîne est modifiée à plusieurs reprises, cela commence à être une charge sur les performances : toutes ces allocations finissent par devenir coûteuses. Voici un exemple simple de programme qui ajoute à une chaîne 50 000 fois, suivi d’un autre qui utilise un objet StringBuilder pour modifier la chaîne en place. Le code StringBuilder est beaucoup plus rapide et, si vous les exécutez, il devient immédiatement évident.
|
|
Essayez d’examiner Perfmon pour voir combien de temps est enregistré sans allouer des milliers de chaînes. Examinez le compteur « % de temps dans GC » sous la liste mémoire CLR .NET. Vous pouvez également suivre le nombre d’allocations que vous enregistrez, ainsi que les statistiques de collecte.
Compromis= Il existe une certaine surcharge associée à la création d’un objet StringBuilder , à la fois en temps et en mémoire. Sur une machine avec une mémoire rapide, un StringBuilder devient utile si vous effectuez environ cinq opérations. En règle générale, je dirais que 10 opérations de chaîne ou plus sont une justification de la surcharge sur n’importe quelle machine, même plus lente.
Précompiler les applications Windows Forms
Les méthodes sont JITed lorsqu’elles sont utilisées pour la première fois, ce qui signifie que vous payez une pénalité de démarrage plus importante si votre application effectue beaucoup d’appels de méthode au démarrage. Windows Forms utilisent beaucoup de bibliothèques partagées dans le système d’exploitation, et la surcharge de démarrage peut être beaucoup plus élevée que d’autres types d’applications. Bien que ce ne soit pas toujours le cas, la précompilation des applications Windows Forms se traduit généralement par une victoire des performances. Dans d’autres scénarios, il est généralement préférable de laisser le JIT s’en occuper, mais si vous êtes développeur Windows Forms, vous pouvez jeter un coup d’œil.
Microsoft vous permet de précompiler une application en appelant ngen.exe
. Vous pouvez choisir d’exécuter ngen.exe pendant l’installation ou avant de distribuer votre application. Il est certainement plus judicieux d’exécuter ngen.exe au moment de l’installation, car vous pouvez vous assurer que l’application est optimisée pour l’ordinateur sur lequel elle est installée. Si vous exécutez ngen.exe avant d’expédier le programme, vous limitez les optimisations à celles disponibles sur votre ordinateur. Pour vous donner une idée de la quantité de précompilation peut vous aider, j’ai effectué un test informel sur ma machine. Vous trouverez ci-dessous les heures de démarrage à froid de ShowFormComplex, une application winforms avec environ une centaine de contrôles.
État du code | Temps |
---|---|
Framework JITed ShowFormComplex JITed |
3,4 secondes |
Framework Précompiled, ShowFormComplex JITed | 2,5 s |
Framework Précompiled, ShowFormComplex Précompiled | 2.1sec |
Chaque test a été effectué après un redémarrage. Comme vous pouvez le voir, Windows Forms applications utilisent un grand nombre de méthodes à l’avance, ce qui en fait un gain de performances substantiel pour précompiler.
Utiliser des tableaux déchiquetés — Version 1
Le JIT v1 optimise les tableaux déchiquetés (simplement les « tableaux de tableaux ») plus efficacement que les tableaux rectangulaires, et la différence est tout à fait notable. Voici un tableau illustrant le gain de performances résultant de l’utilisation de tableaux en déchiquetés au lieu de tableaux rectangulaires en C# et En Visual Basic (les nombres plus élevés sont meilleurs) :
C# | Visual Basic 7 | |
---|---|---|
Affectation (déchiqueté) Affectation (rectangulaire) |
14.16 8.37 |
12.24 8.62 |
Réseau neuronal (déchiqueté) Filet neuronal (rectangulaire) |
4.48 3.00 |
4.58 3.13 |
Tri numérique (déchiqueté) Tri numérique (rectangulaire) |
4.88 2.05 |
5,07 2,06 |
Le point de référence d’affectation est un algorithme d’affectation simple, adapté à partir du guide pas à pas trouvé dans La prise de décision quantitative pour les entreprises (Gordon, Pressman et Cohn; Prentice-Hall; hors impression). Le test du réseau neuronal exécute une série de modèles sur un petit réseau neuronal, et le tri numérique est explicite. Ensemble, ces benchmarks représentent une bonne indication des performances réelles.
Comme vous pouvez le voir, l’utilisation de tableaux déchiquetés peut entraîner des augmentations de performances assez spectaculaires. Les optimisations apportées aux tableaux déchiquetés seront ajoutées aux futures versions du JIT, mais pour v1, vous pouvez gagner beaucoup de temps en utilisant des tableaux en déchiquetés.
Conserver la taille de la mémoire tampon d’E/S comprise entre 4 Ko et 8 Ko
Pour presque toutes les applications, une mémoire tampon comprise entre 4 Ko et 8 Ko vous donnera les performances maximales. Pour des instances très spécifiques, vous pouvez obtenir une amélioration à partir d’une mémoire tampon plus grande (chargement d’images volumineuses d’une taille prévisible, par exemple), mais dans 99,99 % des cas, cela ne gaspille que de la mémoire. Toutes les mémoires tampons dérivées de BufferedStream vous permettent de définir la taille sur ce que vous voulez, mais dans la plupart des cas, 4 et 8 vous offrent les meilleures performances.
Soyez à l’affût des opportunités d’E/S asynchrones
Dans de rares cas, vous pouvez tirer parti des E/S asynchrones. Par exemple, vous pouvez télécharger et décompresser une série de fichiers : vous pouvez lire les bits d’un flux, les décoder sur le processeur et les écrire dans un autre. L’utilisation efficace des E/S asynchrones nécessite beaucoup d’efforts et peut entraîner une perte de performances si elle n’est pas effectuée correctement. L’avantage est qu’une fois appliquées correctement, les E/S asynchrones peuvent vous offrir des performances dix fois plus élevées.
Un excellent exemple de programme utilisant des E/S asynchrones est disponible sur MSDN Library.
- Une chose à noter est qu’il existe une petite surcharge de sécurité pour les appels asynchrones : lors de l’appel d’un appel asynchrone, l’état de sécurité de la pile de l’appelant est capturé et transféré vers le thread qui exécutera réellement la requête. Cela peut ne pas être un problème si le rappel exécute beaucoup de code, ou si les appels asynchrones ne sont pas utilisés de manière excessive
Conseils pour l’accès à la base de données
La philosophie du réglage de l’accès à la base de données consiste à utiliser uniquement les fonctionnalités dont vous avez besoin et à concevoir une approche « déconnectée » : effectuer plusieurs connexions dans l’ordre, plutôt que de maintenir une seule connexion ouverte pendant une longue période. Vous devez tenir compte de cette modification et concevoir autour de celle-ci.
Microsoft recommande une stratégie à plusieurs niveaux pour des performances maximales, par opposition à une connexion directe de client à base de données. Considérez cela dans le cadre de votre philosophie de conception, car de nombreuses technologies en place sont optimisées pour tirer parti d’un scénario multi-fatigue.
Utiliser le Fournisseur managé Optimal
Faites le bon choix du fournisseur managé, plutôt que de vous fier à un accesseur générique. Il existe des fournisseurs managés écrits spécifiquement pour de nombreuses bases de données différentes, comme SQL (System.Data.SqlClient). Si vous utilisez une interface plus générique telle que System.Data.Odbc alors que vous pouvez utiliser un composant spécialisé, vous perdrez des performances en traitant le niveau d’indirection supplémentaire. L’utilisation du fournisseur optimal peut également vous faire parler un autre langage : le client SQL managé parle TDS à une base de données SQL, ce qui offre une amélioration spectaculaire par rapport à OleDbprotocol générique.
Choisir lecteur de données sur jeu de données lorsque vous le pouvez
Utilisez un lecteur de données chaque fois que vous n’avez pas besoin de conserver les données. Cela permet une lecture rapide des données, qui peuvent être mises en cache si l’utilisateur le souhaite. Un lecteur est simplement un flux sans état qui vous permet de lire les données à mesure qu’elles arrivent, puis de les supprimer sans les stocker dans un jeu de données pour plus de navigation. L’approche de flux est plus rapide et a moins de charge, car vous pouvez commencer à utiliser les données immédiatement. Vous devez évaluer la fréquence à laquelle vous avez besoin des mêmes données pour déterminer si la mise en cache pour la navigation vous convient. Voici un petit tableau illustrant la différence entre DataReader et DataSet sur les fournisseurs ODBC et SQL lors de l’extraction de données à partir d’un serveur (les nombres plus élevés sont préférables) :
ADO | SQL | |
---|---|---|
DataSet | 801 | 2507 |
DataReader | 1083 | 4585 |
Comme vous pouvez le voir, les performances les plus élevées sont obtenues lors de l’utilisation du fournisseur managé optimal avec un lecteur de données. Lorsque vous n’avez pas besoin de mettre en cache vos données, l’utilisation d’un lecteur de données peut vous fournir une amélioration considérable des performances.
Utiliser Mscorsvr.dll pour les machines MP
Pour les applications serveur et de niveau intermédiaire autonomes, assurez-vous qu’il mscorsvr
est utilisé pour les machines multiprocesseurs. Mscorwks n’est pas optimisé pour la mise à l’échelle ou le débit, tandis que la version du serveur a plusieurs optimisations qui lui permettent de bien évoluer lorsque plusieurs processeurs sont disponibles.
Utiliser des procédures stockées dans la mesure du possible
Les procédures stockées sont des outils hautement optimisés qui se traduisent par d’excellentes performances lorsqu’elles sont utilisées efficacement. Configurez des procédures stockées pour gérer les insertions, les mises à jour et les suppressions avec l’adaptateur de données. Les procédures stockées n’ont pas besoin d’être interprétées, compilées ou même transmises à partir du client, et réduisent à la fois le trafic réseau et la surcharge du serveur. Veillez à utiliser CommandType.StoredProcedure au lieu de CommandType.Text
Soyez prudent en ce qui concerne les chaînes de connexion dynamiques
Le regroupement de connexions est un moyen utile de réutiliser les connexions pour plusieurs demandes, plutôt que de payer la surcharge liée à l’ouverture et à la fermeture d’une connexion pour chaque requête. Elle est effectuée implicitement, mais vous obtenez un pool par chaîne de connexion unique. Si vous générez des chaînes de connexion dynamiquement, assurez-vous que les chaînes sont identiques chaque fois que le regroupement se produit. Sachez également que si la délégation se produit, vous obtiendrez un pool par utilisateur. Il existe de nombreuses options que vous pouvez définir pour le pool de connexions, et vous pouvez suivre les performances du pool à l’aide de Perfmon pour suivre des éléments tels que le temps de réponse, les transactions/s, etc.
Désactiver les fonctionnalités que vous n’utilisez pas
Désactivez l’inscription automatique des transactions si elle n’est pas nécessaire. Pour le Fournisseur managé SQL, cela s’effectue via la chaîne de connexion :
SqlConnection conn = new SqlConnection(
"Server=mysrv01;
Integrated Security=true;
Enlist=false");
Lorsque vous remplissez un jeu de données avec l’adaptateur de données, n’obtenez pas d’informations sur la clé primaire si vous n’en avez pas besoin (par exemple, ne définissez pas MissingSchemaAction.Add avec la clé) :
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;
}
Éviter les commandes générées automatiquement
Lorsque vous utilisez un adaptateur de données, évitez les commandes générées automatiquement. Celles-ci nécessitent des déplacements supplémentaires vers le serveur pour récupérer les métadonnées et vous donner un niveau inférieur de contrôle d’interaction. Bien que l’utilisation de commandes générées automatiquement soit pratique, il vaut la peine de le faire vous-même dans les applications critiques en matière de performances.
Méfiez-vous de la conception héritée d’ADO
N’oubliez pas que lorsque vous exécutez une commande ou un remplissage d’appel sur l’adaptateur, chaque enregistrement spécifié par votre requête est retourné.
Si des curseurs de serveur sont absolument nécessaires, ils peuvent être implémentés via une procédure stockée dans t-sql. Évitez dans la mesure du possible, car les implémentations basées sur le curseur de serveur ne sont pas très bien mises à l’échelle.
Si nécessaire, implémentez la pagination de manière sans état et sans connexion. Vous pouvez ajouter des enregistrements supplémentaires au jeu de données en :
- Vérifier que les informations PK sont présentes
- Modification de la commande select de l’adaptateur de données en fonction des besoins, et
- Remplissage d’appel
Conserver vos jeux de données allégées
Placez uniquement les enregistrements dont vous avez besoin dans le jeu de données. N’oubliez pas que le jeu de données stocke toutes ses données en mémoire et que plus vous demandez de données, plus il faudra du temps pour transmettre sur le réseau.
Utiliser l’accès séquentiel aussi souvent que possible
Avec un lecteur de données, utilisez CommandBehavior.SequentialAccess. Cela est essentiel pour traiter les types de données blob, car il permet aux données d’être lues hors du câble en petits blocs. Bien que vous ne puissiez utiliser qu’une seule partie des données à la fois, la latence de chargement d’un type de données volumineux disparaît. Si vous n’avez pas besoin de travailler l’objet entier en même temps, l’utilisation de l’accès séquentiel vous donnera de bien meilleures performances.
Conseils en matière de performances pour les applications ASP.NET
Mettre en cache de manière agressive
Lorsque vous concevez une application à l’aide de ASP.NET, veillez à concevoir en veillant à la mise en cache. Sur les versions serveur du système d’exploitation, vous disposez de nombreuses options pour ajuster l’utilisation des caches côté serveur et côté client. Il existe plusieurs fonctionnalités et outils dans ASP que vous pouvez utiliser pour obtenir des performances.
Mise en cache de sortie : stocke le résultat statique d’une requête ASP. Spécifié à l’aide de la <@% OutputCache %>
directive :
- Durée : un élément d’heure existe dans le cache
- VaryByParam : varie les entrées de cache par paramètres Get/Post
- VaryByHeader : varie les entrées de cache par en-tête Http
- VaryByCustom : varie les entrées de cache par navigateur
- Remplacez pour varier selon ce que vous voulez :
Mise en cache des fragments : lorsqu’il n’est pas possible de stocker une page entière (confidentialité, personnalisation, contenu dynamique), vous pouvez utiliser la mise en cache de fragments pour stocker des parties de celle-ci pour une récupération plus rapide ultérieurement.
a) VaryByControl : varie les éléments mis en cache en fonction des valeurs d’un contrôle
API cache : fournit une granularité extrêmement fine pour la mise en cache en conservant une table de hachage d’objets mis en cache en mémoire (System.web.UI.caching). Il est également :
a) Inclut les dépendances (clé, fichier, heure)
b) Expire automatiquement les éléments inutilisés
c) Prend en charge les rappels
La mise en cache intelligente peut vous offrir d’excellentes performances, et il est important de réfléchir au type de mise en cache dont vous avez besoin. Imaginez un site e-commerce complexe avec plusieurs pages statiques pour la connexion, puis une multitude de pages générées dynamiquement contenant des images et du texte. Vous pouvez utiliser la mise en cache de sortie pour ces pages de connexion, puis la mise en cache des fragments pour les pages dynamiques. Une barre d’outils, par exemple, peut être mise en cache sous forme de fragment. Pour de meilleures performances, vous pouvez mettre en cache des images couramment utilisées et du texte réutilisable qui apparaissent fréquemment sur le site à l’aide de l’API Cache. Pour obtenir des informations détaillées sur la mise en cache (avec un exemple de code), case activée le site Web ASP. NET.
Utiliser l’état de session uniquement si vous en avez besoin
Une fonctionnalité extrêmement puissante de ASP.NET est sa capacité à stocker l’état de session pour les utilisateurs, comme un panier d’achat sur un site e-commerce ou un historique de navigateur. Étant donné que cette option est activée par défaut, vous payez le coût en mémoire même si vous ne l’utilisez pas. Si vous n’utilisez pas l’état de session, désactivez-le et économisez la surcharge en ajoutant <@% EnabledSessionState = false %> à votre asp. Cela s’accompagne de plusieurs autres options, qui sont expliquées sur le site Web ASP. NET .
Pour les pages qui lisent uniquement l’état de session, vous pouvez choisir EnabledSessionState=readonly. Cela entraîne moins de surcharge que l’état complet de la session de lecture/écriture, et est utile lorsque vous n’avez besoin que d’une partie de la fonctionnalité et que vous ne souhaitez pas payer pour les fonctionnalités d’écriture.
Utiliser l’état d’affichage uniquement si vous en avez besoin
Un exemple d’état d’affichage peut être un formulaire long que les utilisateurs doivent remplir : s’ils cliquent sur Précédent dans leur navigateur, puis retournent, le formulaire reste rempli. Lorsque cette fonctionnalité n’est pas utilisée, cet état consomme de la mémoire et des performances. La plus grande perte de performances ici est peut-être qu’un signal aller-retour doit être envoyé sur le réseau chaque fois que la page est chargée pour mettre à jour et vérifier le cache. Étant donné qu’il est activé par défaut, vous devez spécifier que vous ne souhaitez pas utiliser l’état d’affichage avec <@% EnabledViewState = false %>. Vous devez en savoir plus sur l’état d’affichage sur le site web ASP. NET pour en savoir plus sur certaines des autres options et paramètres auxquels vous avez accès.
Éviter STA COM
Apartment COM est conçu pour traiter le threading dans les environnements non managés. Il existe deux types d’Apartment COM : monothread et multithread. MTA COM est conçu pour gérer le multithreading, tandis que STA COM s’appuie sur le système de messagerie pour sérialiser les demandes de thread. Le monde managé est à thread libre, et l’utilisation de COM d’appartement à thread unique nécessite que tous les threads non managés partagent essentiellement un seul thread pour l’interopérabilité. Cela se traduit par un impact massif sur les performances, et doit être évité dans la mesure du possible. Si vous ne pouvez pas porter l’objet COM Apartment vers le monde managé, utilisez <@%AspCompat = « true » %> pour les pages qui les utilisent. Pour obtenir une explication plus détaillée de STA COM, consultez MSDN Library.
Compilation par lots
Compilez toujours par lot avant de déployer une grande page sur le web. Vous pouvez lancer cette opération en effectuant une requête sur une page par répertoire et en attendant que le processeur soit à nouveau inactif. Cela empêche le serveur Web d’être enlisé avec des compilations tout en essayant de distribuer des pages.
Supprimer les modules HTTP inutiles
Selon les fonctionnalités utilisées, supprimez les modules HTTP inutilisés ou inutiles du pipeline. La récupération de la mémoire ajoutée et des cycles gaspiller peut vous donner un petit coup de pouce.
Éviter la fonctionnalité Autoeventwireup
Au lieu de s’appuyer sur autoeventwireup, remplacez les événements de Page. Par exemple, au lieu d’écrire une méthode Page_Load(), essayez de surcharger la méthode public void OnLoad( ). Cela permet au temps d’exécution d’avoir à effectuer un CreateDelegate() pour chaque page.
Encoder à l’aide d’ASCII lorsque vous n’avez pas besoin d’UTF
Par défaut, ASP.NET est configuré pour encoder les requêtes et les réponses en UTF-8. Si ASCII est tous les besoins de votre application, l’élimination de la surcharge UTF peut vous permettre de revenir à quelques cycles. Notez que cette opération ne peut être effectuée que par application.
Utiliser la procédure d’authentification optimale
Il existe plusieurs façons d’authentifier un utilisateur et certaines sont plus coûteuses que d’autres (par ordre d’augmentation du coût : Aucun, Windows, Forms, Passport). Assurez-vous d’utiliser le moins cher qui répond le mieux à vos besoins.
Conseils pour le portage et le développement en Visual Basic
Beaucoup de choses ont changé sous le capot de Microsoft® Visual Basic® 6 à Microsoft® Visual Basic® 7, et la carte des performances a changé avec elle. En raison de l’ajout de fonctionnalités et de restrictions de sécurité du CLR, certaines fonctions ne peuvent tout simplement pas s’exécuter aussi rapidement que dans Visual Basic 6. En fait, il existe plusieurs domaines où Visual Basic 7 est trouncé par son prédécesseur. Heureusement, il y a deux bonnes nouvelles :
- La plupart des pires ralentissements se produisent pendant des fonctions ponctuelles, telles que le chargement d’un contrôle pour la première fois. Le coût est là, mais vous ne le payez qu’une seule fois.
- Il existe de nombreux domaines dans lesquels Visual Basic 7 est plus rapide, et ces zones ont tendance à se trouver dans des fonctions qui sont répétées pendant l’exécution. Cela signifie que l’avantage augmente au fil du temps et, dans plusieurs cas, l’emporte sur les coûts ponctuels.
La majorité des problèmes de performances proviennent de zones où l’exécution ne prend pas en charge une fonctionnalité de Visual Basic 6, et elle doit être ajoutée pour conserver la fonctionnalité dans Visual Basic 7. Le travail en dehors du temps d’exécution est plus lent, ce qui rend certaines fonctionnalités beaucoup plus coûteuses à utiliser. Le bon côté est que vous pouvez éviter ces problèmes avec un peu d’effort. Il existe deux main domaines qui nécessitent du travail pour optimiser les performances, et quelques ajustements simples que vous pouvez faire ici et là. Ensemble, ils peuvent vous aider à contourner les drains de performances et à tirer parti des fonctions beaucoup plus rapides dans Visual Basic 7.
Gestion des erreurs
La première préoccupation est la gestion des erreurs. Cela a beaucoup changé dans Visual Basic 7, et il existe des problèmes de performances liés à la modification. Essentiellement, la logique requise pour implémenter OnErrorGoto et Resume est extrêmement coûteuse. Je vous suggère d’examiner rapidement votre code et de mettre en évidence toutes les zones dans lesquelles vous utilisez l’objet Err ou tout mécanisme de gestion des erreurs. À présent, examinez chacune de ces instances et voyez si vous pouvez les réécrire pour utiliser try/catch. Beaucoup de développeurs trouveront qu’ils peuvent convertir pour essayer/intercepter facilement dans la plupart de ces cas, et ils devraient voir une bonne amélioration des performances dans leur programme. La règle générale est « si vous pouvez facilement voir la traduction, faites-le ».
Voici un exemple de programme Visual Basic simple qui utilise On Error Go par rapport à la version try/catch .
|
|
L’augmentation de la vitesse est perceptible. SubWithError() prend 244 millisecondes à l’aide de OnErrorGoto, et seulement 169 millisecondes à l’aide de try/catch. La deuxième fonction prend 179 millisecondes, contre 164 millisecondes pour la version optimisée.
Utiliser la liaison anticipée
La deuxième préoccupation concerne les objets et la création de types. Visual Basic 6 fait beaucoup de travail sous le capot pour prendre en charge le cast d’objets, et de nombreux programmeurs n’en sont même pas conscients. Dans Visual Basic 7, il s’agit d’une zone à partir de laquelle vous pouvez tirer beaucoup de performances. Lorsque vous compilez, utilisez la liaison anticipée. Cela indique au compilateur d’insérer une contrainte de type uniquement lorsqu’elle est explicitement mentionnée. Cela a deux effets majeurs :
- Les erreurs étranges deviennent plus faciles à repérer.
- Les contraintes inutiles sont éliminées, ce qui entraîne des améliorations substantielles des performances.
Lorsque vous utilisez un objet comme s’il était d’un type différent, Visual Basic force l’objet pour vous si vous ne spécifiez pas. C’est pratique, car le programmeur doit se soucier de moins de code. L’inconvénient est que ces contraintes peuvent faire des choses inattendues, et le programmeur n’a aucun contrôle sur elles.
Il existe des cas où vous devez utiliser une liaison tardive, mais la plupart du temps, si vous n’êtes pas sûr, vous pouvez vous en tirer avec la liaison anticipée. Pour les programmeurs Visual Basic 6, cela peut être un peu maladroit au premier abord, car vous devez vous soucier des types plus que par le passé. Cela devrait être facile pour les nouveaux programmeurs, et les personnes familiarisées avec Visual Basic 6 le reprendront en un rien de temps.
Activer l’option Strict et Explicit
Avec Option Strict on, vous vous protégez contre les liaisons tardives par inadvertance et appliquez un niveau plus élevé de discipline de codage. Pour obtenir la liste des restrictions présentes avec Option Strict, consultez MSDN Library. La mise en garde est que toutes les contraintes de type restrictives doivent être spécifiées explicitement. Toutefois, cela peut en soi révéler d’autres sections de votre code qui font plus de travail que vous ne l’aviez pensé précédemment, et cela peut vous aider à corriger certains bogues dans le processus.
Option Explicit est moins restrictif que Option Strict, mais elle force toujours les programmeurs à fournir plus d’informations dans leur code. Plus précisément, vous devez déclarer une variable avant de l’utiliser. Cela déplace l’inférence de type de l’exécution au moment de la compilation. Cette case activée éliminée se traduit par des performances supplémentaires pour vous.
Je vous recommande de commencer par Option Explicite, puis d’activer Option Strict. Cela vous protégera contre un déluge d’erreurs du compilateur et vous permettra de commencer progressivement à travailler dans l’environnement plus strict. Lorsque ces deux options sont utilisées, vous garantissez des performances maximales pour votre application.
Utiliser la comparaison binaire pour le texte
Lors de la comparaison de texte, utilisez la comparaison binaire au lieu de la comparaison de texte. Au moment de l’exécution, la surcharge est beaucoup plus légère pour les binaires.
Réduire l’utilisation de Format()
Lorsque vous le pouvez, utilisez toString() au lieu de format(). Dans la plupart des cas, il vous fournira les fonctionnalités dont vous avez besoin, avec beaucoup moins de surcharge.
Utiliser Charw
Utilisez charw au lieu de char. Le CLR utilise Unicode en interne, et char
doit être traduit au moment de l’exécution s’il est utilisé. Cela peut entraîner une perte de performances substantielle et spécifier que vos caractères sont longs (l’utilisation de charw)
élimine cette conversion.
Optimiser les affectations
Utilisez exp += val au lieu de exp = exp + val. Étant donné que exp
peut être arbitrairement complexe, cela peut entraîner beaucoup de travail inutile. Cela force le JIT à évaluer les deux copies d’exp, et souvent cela n’est pas nécessaire. La première instruction peut être optimisée beaucoup mieux que la seconde, car le JIT peut éviter d’évaluer l’exp deux fois.
Éviter l’indirection inutile
Lorsque vous utilisez byRef, vous passez des pointeurs au lieu de l’objet réel. Souvent, cela est logique (fonctions d’effet secondaire, par exemple), mais vous n’en avez pas toujours besoin. La transmission de pointeurs entraîne plus d’indirection, ce qui est plus lent que l’accès à une valeur qui se trouve sur la pile. Lorsque vous n’avez pas besoin de passer par le tas, il est préférable de l’éviter.
Placer des concaténations dans une seule expression
Si vous avez plusieurs concaténations sur plusieurs lignes, essayez de les coller sur une seule expression. Le compilateur peut optimiser en modifiant la chaîne sur place, ce qui permet une augmentation de la vitesse et de la mémoire. Si les instructions sont divisées en plusieurs lignes, le compilateur Visual Basic ne génère pas le langage MSIL (Microsoft Intermediate Language) pour autoriser la concaténation sur place. Consultez l’exemple StringBuilder décrit précédemment.
Inclure des instructions de retour
Visual Basic permet à une fonction de retourner une valeur sans utiliser l’instruction return . Bien que Visual Basic 7 prenne en charge cela, l’utilisation explicite du retour permet au JIT d’effectuer des optimisations légèrement plus. Sans instruction return, chaque fonction reçoit plusieurs variables locales sur la pile pour prendre en charge en toute transparence le retour de valeurs sans la mot clé. La conservation de ces éléments rend plus difficile l’optimisation du JIT et peut avoir un impact sur les performances de votre code. Examinez vos fonctions et insérez un retour en fonction des besoins. Il ne modifie pas du tout la sémantique du code et peut vous aider à obtenir plus de vitesse à partir de votre application.
Conseils pour le portage et le développement en C++ managé
Microsoft cible managed C++ (MC++) sur un ensemble spécifique de développeurs. MC++ n’est pas le meilleur outil pour chaque travail. Après avoir lu ce document, vous pouvez décider que C++ n’est pas le meilleur outil et que les coûts de compromis n’en valent pas la peine. Si vous n’êtes pas sûr de MC++, il existe de nombreuses ressources utiles pour vous aider à prendre votre décision Cette section s’adresse aux développeurs qui ont déjà décidé qu’ils veulent utiliser MC++ d’une manière ou d’une autre et qui souhaitent en savoir plus sur les performances.
Pour les développeurs C++, le fonctionnement de C++ managé nécessite que plusieurs décisions soient prises. Portez-vous un ancien code ? Si c’est le cas, voulez-vous déplacer l’ensemble vers l’espace managé ou prévoyez-vous plutôt d’implémenter un wrapper ? Je vais me concentrer sur l’option « port-tout » ou traiter de l’écriture mc++ à partir de zéro pour les besoins de cette discussion, car ce sont les scénarios dans lesquels le programmeur remarquera une différence de performances.
Avantages du monde managé
La fonctionnalité la plus puissante de Managed C++ est la possibilité de combiner et de mettre en correspondance du code managé et non managé au niveau de l’expression. Aucun autre langage ne vous permet de le faire, et il ya quelques avantages puissants que vous pouvez obtenir à partir de celui-ci s’il est utilisé correctement. Je passerai en revue quelques exemples plus tard.
Le monde managé vous donne également d’énormes gains de conception, en ce sens que beaucoup de problèmes courants sont pris en charge pour vous. La gestion de la mémoire, la planification des threads et les contraintes de type peuvent être laissées au moment de l’exécution si vous le souhaitez, ce qui vous permet de concentrer vos énergies sur les parties du programme qui en ont besoin. Avec MC++, vous pouvez choisir exactement la quantité de contrôle que vous souhaitez conserver.
Les programmeurs MC++ ont le luxe d’utiliser le back-end Microsoft Visual C7® (VC7) lors de la compilation dans IL, puis d’utiliser le JIT en plus de cela. Les programmeurs habitués à travailler avec le compilateur Microsoft C++ sont habitués aux choses rapides. Le JIT a été conçu avec des objectifs différents et a un ensemble différent de forces et de faiblesses. Le compilateur VC7, non lié par les restrictions de temps du JIT, peut effectuer certaines optimisations que le JIT ne peut pas, telles que l’analyse de l’ensemble du programme, l’incorporation et l’enregistrement plus agressifs. Il existe également certaines optimisations qui peuvent être effectuées uniquement dans des environnements de type sécurisé, laissant plus de place à la vitesse que C++ autorise.
En raison des différentes priorités du JIT, certaines opérations sont plus rapides qu’auparavant, tandis que d’autres sont plus lentes. Il y a des compromis que vous faites pour la sécurité et la flexibilité de la langue, et certains d’entre eux ne sont pas bon marché. Heureusement, il existe des choses qu’un programmeur peut faire pour réduire les coûts.
Portage : tout le code C++ peut être compilé vers MSIL
Avant d’aller plus loin, il est important de noter que vous pouvez compiler n’importe quel code C++ dans MSIL. Tout fonctionnera, mais il n’y a aucune garantie de sécurité de type et vous payez la pénalité de marshaling si vous faites beaucoup d’interopérabilité. Pourquoi est-il utile de compiler sur MSIL si vous n’obtenez aucun des avantages ? Dans les situations où vous portez une base de code volumineuse, cela vous permet de porter progressivement votre code par morceaux. Vous pouvez passer votre temps à porter plus de code, plutôt que d’écrire des wrappers spéciaux pour coller le code porté et le code pas encore porté ensemble si vous utilisez MC++, ce qui peut entraîner une grande victoire. Cela fait du portage des applications un processus très propre. Pour en savoir plus sur la compilation de C++ en MSIL, consultez l’option de compilateur /clr.
Toutefois, la simple compilation de votre code C++ dans MSIL ne vous donne pas la sécurité ou la flexibilité du monde managé. Vous devez écrire dans MC++, et dans v1, cela signifie renoncer à quelques fonctionnalités. La liste ci-dessous n’est pas prise en charge dans la version actuelle du CLR, mais peut l’être à l’avenir. Microsoft a choisi de prendre en charge les fonctionnalités les plus courantes en premier, et a dû en couper d’autres pour être livré. Rien ne les empêche d’être ajoutés ultérieurement, mais en attendant, vous devrez vous en passer :
- Héritage multiple
- Modèles
- Finalisation déterministe
Vous pouvez toujours interagir avec du code non sécurisé si vous avez besoin de ces fonctionnalités, mais vous payez la pénalité de performances du marshaling des données. Gardez à l’esprit que ces fonctionnalités ne peuvent être utilisées qu’à l’intérieur du code non managé. L’espace géré n’a aucune connaissance de leur existence. Si vous décidez de porter votre code, pensez à combien vous vous appuyez sur ces fonctionnalités dans votre conception. Dans certains cas, la refonte est trop coûteuse et vous voudrez vous en tenir au code non managé. C’est la première décision que vous devez prendre, avant de commencer à pirater.
Avantages de MC++ par rapport à C# ou Visual Basic
Provenant d’un arrière-plan non managé, MC++ conserve une grande partie de la capacité à gérer du code non sécurisé. La capacité de MC++ à combiner le code managé et le code non managé en douceur offre au développeur une grande puissance, et vous pouvez choisir l’emplacement sur le dégradé que vous souhaitez placer lors de l’écriture de votre code. Sur un extrême, vous pouvez tout écrire en C++ direct et non appulté et simplement compiler avec /clr. D’autre part, vous pouvez tout écrire en tant qu’objets managés et gérer les limitations de langage et les problèmes de performances mentionnés ci-dessus.
Mais la véritable puissance de MC++ vient quand vous choisissez quelque part entre les deux. MC++ vous permet d’ajuster certaines des performances inhérentes au code managé, en vous donnant un contrôle précis sur le moment où utiliser des fonctionnalités dangereuses. C# a une partie de cette fonctionnalité dans les mot clé non sécurisés, mais elle ne fait pas partie intégrante du langage et elle est beaucoup moins utile que MC++. Passons en revue quelques exemples montrant la granularité plus fine disponible dans MC++, et nous allons parler des situations où cela s’avère utile.
Pointeurs « byref » généralisés
En C#, vous pouvez uniquement prendre l’adresse d’un membre d’une classe en la passant à un paramètre ref . Dans MC++, un pointeur byref est une construction de première classe. Vous pouvez prendre l’adresse d’un élément au milieu d’un tableau et retourner cette adresse à partir d’une fonction :
Byte* AddrInArray( Byte b[] ) {
return &b[5];
}
Nous exploitons cette fonctionnalité pour renvoyer un pointeur vers les « caractères » dans un System.String via notre routine d’assistance, et nous pouvons même parcourir des tableaux à l’aide de ces pointeurs :
System::Char* PtrToStringChars(System::String*);
for( Char*pC = PtrToStringChars(S"boo");
pC != NULL;
pC++ )
{
... *pC ...
}
Vous pouvez également effectuer un parcours de liste liée avec injection dans MC++ en prenant l’adresse du champ « suivant » (ce que vous ne pouvez pas faire en C#) :
Node **w = &Head;
while(true) {
if( *w == 0 || val < (*w)->val ) {
Node *t = new Node(val,*w);
*w = t;
break;
}
w = &(*w)->next;
}
En C#, vous ne pouvez pas pointer vers « Tête » ou prendre l’adresse du champ « suivant », de sorte que vous avez créé un cas spécial où vous insérez au premier emplacement, ou si « Head » a la valeur Null. De plus, vous devez regarder un nœud à l’avance tout le temps dans le code. Comparez ceci à ce qu’un bon C# produit :
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;
}
}
Accès utilisateur aux types boxed
Un problème de performances courant avec les langages OO est le temps passé à boxer et à déballer les valeurs. MC++ vous donne beaucoup plus de contrôle sur ce comportement, de sorte que vous n’aurez pas à déballer dynamiquement (ou statiquement) pour accéder aux valeurs. Il s’agit d’une autre amélioration des performances. Placez simplement __box mot clé avant tout type pour représenter sa forme encadrée :
__value struct V {
int i;
};
int main() {
V v = {10};
__box V *pbV = __box(v);
pbV->i += 10; // update without casting
}
En C#, vous devez unboxbox vers un « v », puis mettre à jour la valeur et re-boxer sur un objet :
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
}
Collections STL et collections managées — v1
Mauvaise nouvelle : en C++, l’utilisation des collections STL était souvent aussi rapide que l’écriture manuelle de cette fonctionnalité. Les frameworks CLR sont très rapides, mais ils souffrent de problèmes de boxing et de unboxing : tout est un objet, et sans modèle ou prise en charge générique, toutes les actions doivent être vérifiées au moment de l’exécution.
Les bonnes nouvelles: À long terme, vous pouvez parier que ce problème disparaîtra à mesure que les génériques sont ajoutés au temps d’exécution. Le code que vous déployez aujourd’hui connaîtra l’augmentation de la vitesse sans aucune modification. À court terme, vous pouvez utiliser la conversion statique pour empêcher la case activée, mais cela n’est plus sûr. Je vous recommande d’utiliser cette méthode dans un code strict où les performances sont absolument essentielles et que vous avez identifié deux ou trois points chauds.
Utiliser des objets gérés par pile
En C++, vous spécifiez qu’un objet doit être géré par la pile ou le tas. Vous pouvez toujours le faire dans MC++, mais il existe des restrictions que vous devez connaître. Le CLR utilise des ValueTypes pour tous les objets gérés par la pile, et il existe des limitations à ce que les ValueTypes peuvent faire (aucun héritage, par exemple). Plus d’informations sont disponibles sur MSDN Library.
Cas d’angle : Méfiez-vous des appels indirects dans le code managé — v1
Au moment de l’exécution v1, tous les appels de fonction indirects sont effectués en mode natif et nécessitent donc une transition vers un espace non managé. Tout appel de fonction indirect ne peut être effectué qu’à partir du mode natif, ce qui signifie que tous les appels indirects à partir de code managé nécessitent une transition managée-non managée. Il s’agit d’un problème sérieux lorsque la table retourne une fonction managée, car une deuxième transition doit ensuite être effectuée pour exécuter la fonction. Comparé au coût d’exécution d’une instruction d’appel unique, le coût est cinquante à cent fois plus lent qu’en C++!
Heureusement, lorsque vous appelez une méthode qui réside dans une classe garbage-collected, l’optimisation supprime cela. Toutefois, dans le cas spécifique d’un fichier C++ normal qui a été compilé à l’aide de /clr, la méthode retournée est considérée comme gérée. Étant donné que cela ne peut pas être supprimé par l’optimisation, vous êtes confronté au coût total de la double transition. Voici un exemple de cas de ce type.
//////////////////////// 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(){}
Il existe plusieurs façons d’éviter cela :
- Faire de la classe une classe managée (« __gc »)
- Supprimer l’appel indirect, si possible
- Laissez la classe compilée en tant que code non managé (par exemple, n’utilisez pas /clr)
Réduire les performances de la version 1
Il existe plusieurs opérations ou fonctionnalités qui sont simplement plus coûteuses dans MC++ sous la version 1 JIT. Je vais les répertorier et donner des explications, puis nous parlerons de ce que vous pouvez faire à leur sujet.
- Abstractions : il s’agit d’un domaine où le compilateur back-end C++ lent et costaud gagne considérablement sur le JIT. Si vous encapsulez un int à l’intérieur d’une classe à des fins d’abstraction et que vous y accédez strictement comme un int, le compilateur C++ peut réduire la surcharge du wrapper à pratiquement rien. Vous pouvez ajouter de nombreux niveaux d’abstraction au wrapper, sans augmenter le coût. Le JIT ne peut pas prendre le temps nécessaire pour éliminer ce coût, ce qui rend les abstractions approfondies plus coûteuses dans MC++.
- Virgule flottante : le JIT v1 n’effectue actuellement pas toutes les optimisations spécifiques à FP que fait le back-end VC++, ce qui rend les opérations à virgule flottante plus coûteuses pour l’instant.
- Tableaux multidimensionnels : le JIT est plus efficace pour gérer les tableaux déchiquetés que les tableaux multidimensionnels. Par conséquent, utilisez plutôt des tableaux déchiquetés.
- 64 bits arithmétique : dans les versions ultérieures, des optimisations 64 bits seront ajoutées au JIT.
Ce que vous pouvez faire
À chaque phase de développement, vous pouvez faire plusieurs choses. Avec MC++, la phase de conception est peut-être le domaine le plus important, car elle déterminera la quantité de travail que vous effectuez et la quantité de performances que vous obtenez en retour. Lorsque vous vous asseyez pour écrire ou porter une application, vous devez tenir compte des éléments suivants :
- Identifiez les zones où vous utilisez plusieurs héritages, modèles ou finalisation déterministe. Vous devrez vous débarrasser de ces éléments, sinon laisser cette partie de votre code dans un espace non managé. Réfléchissez au coût de la refonte et identifiez les zones qui peuvent être transférées.
- Localisez les points chauds des performances, tels que les abstractions approfondies ou les appels de fonctions virtuelles sur l’espace managé. Celles-ci nécessiteront également une décision de conception.
- Recherchez les objets qui ont été spécifiés comme gérés par la pile. Assurez-vous qu’ils peuvent être convertis en ValueTypes. Marquez les autres pour la conversion en objets gérés par tas.
Pendant la phase de codage, vous devez être conscient des opérations qui sont les plus coûteuses et des options dont vous disposez pour les traiter. L’une des choses les plus intéressantes avec MC++ est que vous arrivez à maîtriser tous les problèmes de performances avant de commencer à coder: cela est utile pour analyser le travail plus tard. Toutefois, il existe encore quelques modifications que vous pouvez effectuer pendant que vous codez et déboguez.
Déterminez les zones qui utilisent lourdement les fonctions arithmétiques, multidimensionnelles ou arithmétiques à virgule flottante. Parmi ces domaines, lequel est critique pour les performances ? Utilisez des profileurs pour choisir les fragments où la surcharge vous coûte le plus cher, et choisissez l’option qui semble la mieux :
- Conservez le fragment entier dans un espace non managé.
- Utilisez des casts statiques sur les accès à la bibliothèque.
- Essayez d’ajuster le comportement de boxe/unboxing (expliqué plus loin).
- Codez votre propre structure.
Enfin, réduisez le nombre de transitions que vous effectuez. Si vous avez du code non managé ou un appel d’interopérabilité dans une boucle, rendez la boucle entière non managée. De cette façon, vous ne paierez le coût de transition que deux fois, plutôt que pour chaque itération de la boucle.
Ressources supplémentaires
Les rubriques connexes sur les performances dans .NET Framework sont les suivantes :
Regardez les futurs articles en cours de développement, y compris une vue d’ensemble de la conception, de l’architecture et des philosophies de codage, une procédure pas à pas des outils d’analyse des performances dans le monde managé et une comparaison des performances de .NET avec d’autres applications d’entreprise disponibles aujourd’hui.
Annexe : Coût des appels virtuels et des allocations
Type d’appel | # Appels/s |
---|---|
Appel non virtuel ValueType | 809971805.600 |
Appel non virtuel de classe | 268478412.546 |
Appel virtuel de classe | 109117738.369 |
Appel valueType Virtual (méthode Obj) | 3004286.205 |
Appel ValueType Virtual (méthode Obj substituée) | 2917140.844 |
Type de chargement par newing (non statique) | 1434.720 |
Type de chargement par newing (méthodes virtuelles) | 1369.863 |
Remarque L’ordinateur de test est un PIII 733 Mhz, exécutant Windows 2000 Professionnel avec Service Pack 2.
Ce graphique compare le coût associé à différents types d’appels de méthode, ainsi que le coût d’instanciation d’un type qui contient des méthodes virtuelles. Plus le nombre est élevé, plus le nombre d’appels/instanciations par seconde peut être effectué. Bien que ces nombres varient certainement d’une machine et d’une configuration à l’autre, le coût relatif de l’exécution d’un appel par rapport à un autre reste important.
- Appel non virtuel ValueType : ce test appelle une méthode non virtuelle vide contenue dans un ValueType.
- Appel non virtuel de classe : ce test appelle une méthode non virtuelle vide contenue dans une classe.
- Appel virtuel de classe : ce test appelle une méthode virtuelle vide contenue dans une classe.
- ValueType Virtual (méthode Obj) Call : ce test appelle ToString() (une méthode virtuelle) sur un ValueType, qui recourt à la méthode objet par défaut.
- ValueType Virtual (méthode Obj substituée) : ce test appelle ToString() (une méthode virtuelle) sur un ValueType qui a remplacé la valeur par défaut.
- Type de chargement par newing (statique) : ce test alloue de l’espace pour une classe avec uniquement des méthodes statiques.
- Type de chargement par newing (méthodes virtuelles) : ce test alloue de l’espace pour une classe avec des méthodes virtuelles.
Une conclusion que vous pouvez tirer est que les appels de fonction virtuelle sont environ deux fois plus chers que les appels ordinaires lorsque vous appelez une méthode dans une classe. Gardez à l’esprit que les appels sont bon marché pour commencer, donc je ne supprimerais pas tous les appels virtuels. Vous devez toujours utiliser des méthodes virtuelles lorsqu’il est judicieux de le faire.
- Le JIT ne peut pas inline méthodes virtuelles. Vous perdez donc une optimisation potentielle si vous vous débarrassez des méthodes non virtuelles.
- L’allocation d’espace pour un objet qui a des méthodes virtuelles est légèrement plus lente que l’allocation d’un objet sans celles-ci, car un travail supplémentaire doit être effectué pour trouver de l’espace pour les tables virtuelles.
Notez que l’appel d’une méthode non virtuelle dans un ValueType est plus de trois fois plus rapide que dans une classe, mais une fois que vous la traitez comme une classe , vous perdez terriblement. Ceci est caractéristique des ValueTypes : traitez-les comme des structs et ils s’allument rapidement. Traitez-les comme des classes et ils sont péniblement lents. ToString() est une méthode virtuelle. Par conséquent, avant d’être appelé, le struct doit être converti en objet sur le tas. Au lieu d’être deux fois plus lent, l’appel d’une méthode virtuelle sur un ValueType est désormais dix-huit fois plus lent ! La morale de l’histoire ? Ne traitez pas ValueTypes comme des classes.
Si vous avez des questions ou des commentaires sur cet article, contactez Claudio Caldato, responsable de programme pour les problèmes de performances .NET Framework.