| April 13, 2009 4:00 PM PDT | |
Version PDF
Cet article est également disponible en téléchargement [PDF de 154 ko]
Introduction
Des opérations bancaires à l’achat de musique en passant par les réservations hôtelières, il n’est guère d’activité commerciale qui échappe au passage en ligne. C’est pourquoi les performances logicielles sont aujourd’hui un vecteur essentiel de la réussite des entreprises. Avec des délais de chargements parfois très longs, la visite d’un site Internet est pourtant trop souvent source de frustration et provoque le départ vers un site concurrent. Or ce phénomène risque de porter un coup fatal aux sites marchands, car ce sont autant de clients perdus, sans doute définitivement. Lorsque le trafic sur un site augmente, l’on constate en effet souvent un ralentissement voire une indisponibilité complète. Or, en faisant appel à des tests de performances et de charge applicatives, on peut espérer éviter de telles interruptions de service. Pourtant, s’il existe des outils qui donnent des indications sur les performances d’une application, ils ne fournissent pas nécessairement d’explication sur leurs dégradations. Si l’on pouvait savoir où chercher ainsi que ce qui fonctionne ou non, on pourrait plus aisément améliorer une application…
Grâce à la plate-forme .NET* Framework de Microsoft, à ses caractéristiques et à ses fonctions conviviales, les développeurs peuvent dorénavant concevoir des solutions complètes pour l’entreprise, plus fonctionnelles et plus performantes. La contrepartie est cependant le risque de voir des architectes et développeurs élaborer des solutions assez pauvres et dépourvues partiellement ou totalement de scalabilité, parce que, malgré les qualités de la plate-forme .NET, la réalisation de ces solutions reste loin d’être évidente. Il sera ainsi question dans cet article des freins aux performances que risque de poser .NET et des erreurs les plus courantes à éviter. Il propose par ailleurs de nombreuses astuces pour écrire du code .NET ultraperformant.
Cet article aborde les sujets suivants :
- Composants de la plate-forme .NET et modèle d’exécution du CLR (Common Language Runtime)
- Support du threading dans .NET et astuces pour éviter les erreurs de parallélisation les plus courantes
- Gestion automatique de la mémoire : écriture de code « GC friendly » (Garbage Collector)
- Revue des outils existants pour stimuler les performances par optimisation du code .NET
Composants de la plate-forme .NET et modèle d’exécution du CLR
La plate-forme .NET instaure un environnement d’exécution appelé « CLR » (Common Language Runtime). Celui-ci assure l’exécution du code ainsi que les services qui facilitent le processus de développement. Il procure des fonctions telles que la gestion automatique de mémoire (GC) et des exceptions, la sécurité, la sécurité de type et le JIT (compilateur en juste-à-temps pour la conversion de MSIL [Microsoft* Intermediate Language] en code natif). Il est implémenté sous forme de DLL nommée <mscorwks.dll>. Il assure également le support des bibliothèques BCL (Base Class Libraries) qui se situent par-dessus lui et qui fournissent des fonctions telles que String, d’E/S fichiers et réseau, de classes de collection, d’accès aux données (ADO.NET) et de traitement XML. Il existe par-dessus les BCL des couches de présentation (Web Forms et Windows Forms) qui assurent des fonctions d’interface utilisateur. Il s’accompagne enfin des langages livrés par Microsoft pour lui. Il existe ainsi actuellement plus de quinze langages qui lui sont destinés.
Modèle d’exécution du CLR
Chaque langage possède un compilateur qui se charge de la compilation et de la conversion du code vers MSIL. Il existe de multiples optimisations qui sont implémentées dans chacun des compilateurs et qui permettent ainsi la génération de code IL performant. Ensuite, le CLR prend le relais et déclenche, via compilateur JIT, la conversion du code IL en code natif que le CLR pourra exécuter. Le compilateur JIT possède lui aussi de nombreuses optimisations intégrées, capables de produire du code natif optimisé dans le sens des performances. Si le code est « non managé », on peut alors contourner presque tout cela et exécuter des programmes non managés. A noter que .NET assure aussi d’autres fonctions grâce auxquelles il est possible de faire appel à des pointeurs pour accéder à des tableaux, etc., via une fonction appelée « unsafe », et ainsi obtenir des gains de performances.
Support du threading dans .NET et astuces pour éviter les erreurs de parallélisation les plus courantes
Le support du threading par .NET est implémenté dans l’espace de noms System.Threading, ce qui permet de distribuer les classes et fonctions — telles que la création-destruction de threads et les primitives de synchronisation pour accès atomique — qui nécessitaient du code parallélisé. Cet espace de noms fournit également une classe appelée « Threadpool », qui permet d’utiliser le pool de threads système.
Threadpool se charge de la création et du nettoyage des threads. Il les recycle afin de limiter les coûts correspondant à leur création et à leur nettoyage. Il surveille également d’autres threads en fonctionnement tels que ceux du GC, pour que celui-ci adapte la logique de création de threads. Un développeur peut en effet omettre de prendre en compte le nombre de threads qu’il convient d’utiliser et qui constitue pourtant un paramètre essentiel à de bonnes performances. Threadpool intègre par ailleurs des heuristiques qui lui permettent de moduler le nombre de threads. Il est recommandé de l’utiliser lorsque l’on envisage de paralléliser une application. ASP.NET exploite déjà Threadpool pour le traitement des requêtes Web.
Nous avons déjà évoqué plus haut le fait que Threadpool décide automatiquement du nombre de threads nécessaires à des performances optimales. Pour des applications ASP.NET (Web), l’optimisation s’effectue à partir du fichier <machine.config>, en limitant les problèmes de contention. On a recours à cette méthode d’optimisation lorsque les trois conditions suivantes sont remplies :²
- Il reste des ressources CPU disponibles.
- L’application réalise des opérations E/S bloquées.
- L’objet ApplicationsRequests en ASP.NET dans le compteur de performances d’Application Queue indique que des requêtes sont placées en file d’attente.
<system.web>
<!--
<processModel autoConfig="true"/> -> This default means it is adjusted automatically
-->
<httpRuntime
minFreeThreads="32" -> Requests will be queued if total # of available threads falls below this number.
minLocalRequestFreeThreads="32" -> Requests from the local host will be queued if total #
of available threads falls below this number.
/>
<processModel
enable="true"
maxWorkerThreads="12" -> maximum # of worker threads in a threadpool. This is per CPU.
maxIoThreads="12" -> maximum number of I/O threads in a threadpool. This is per CPU.
minWorkerThreads="40" -> minimum worker threads available in the system @ any time.
This is for the entire system
/>
NB. Ces valeurs ne sont pas conseillées, mais employées ici pour les besoins de la démonstration.
Comment fonctionne la formule ?
The number of worker threads = maxWorkerThreads*# of CPU (Cores) in the system – minFreeThreads
On obtient 16 = 12*4–32 (pour une machine quadricœur). Le nombre total de requêtes simultanées que l’on peut traiter est donc de seize, mais cela soulève une question intéressante : Comment sait-on si cela a effectivement fonctionné ? On consulte alors le compteur de performances « Pipeline Instance Count », qui devrait être égal à 16, car un seul worker thread peut s’exécuter dans un compteur d’instance de pipeline.
Il convient de faire très attention à cette opération, car on risque de constater une dégradation des performances si on utilise des valeurs aléatoires.
Les API de threading .NET et Threadpool facilitent certes le travail du développeur, mais il subsiste encore malgré tout de nombreuses questions liées au threading qui peuvent avoir une incidence négative sur la performance et la scalabilité.
- La création d’un nombre de threads trop faible ou trop important risque d’avoir une incidence sur les performances. Dans ce cas de figure, faites appel à Threadpool pour vous aider. Idéalement, le nombre de threads correspondra au nombre de cœurs, ce qui se traduira par des performances maximales pour chaque thread pouvant s’exécuter simultanément sur un processeur.
- Le fait de paralléliser des parties de l’application qu’il ne faudrait pas : c’est de loin le problème majeur lorsque l’on parallélise. C’est pourquoi il faut effectuer une analyse complète de l’application concernée avant de décider ce que l’on va paralléliser. Pour obtenir des gains de performances significatifs, il faut paralléliser la partie de votre code la plus employée.
- Le multithreading complique par ailleurs le débogage ainsi que les situations d’interblocage (interlocks) et de courses aux données (data races). Pour vous permettre de résoudre certains de ces bogues délicats, ayez un bon journal de débogage.
Astuces de threading
- Répartissez équitablement le travail parmi les threads. Si la répartition est mal équilibrée, un thread de terminer son exécution rapidement, mais devra patienter que d’autres threads en aient fait de même, ce qui a une incidence sur les performances.
- Ne faites pas appeler par les threads trop de données partagées. Lorsque des données ou structures de données se trouvent partagées entre des threads, cela nécessite en effet une synchronisation pour leur mise à jour, ce qui augmente les codes/chemins séquentiels dans l’application, avec une incidence négative sur la scalabilité.
- L’acquisition d’un verrou (lock) doit se faire le plus tard possible et sa libération au plus tôt. C’est primordial : vous devez ne prendre un verrou que juste avant d’en avoir absolument besoin et, une fois la région atomique du code exécutée, il faut le libérer avant toute autre action. Voici un exemple dans .NET :
void foo ()
{
int a, b;
…. //some code
//Following code has to be atomically executed
{
}
…. //Some other code
//End of atomic region
}
//WRONG: Increased atomic region. Lock will be held longer thus hurting performance
void foo ()
{
int a, b;
Object obj ; //for synchronization
Monitor.Enter(); or lock(obj) {
…. //some code
//Following code has to be atomically executed
{
}
…. //Some other code
Monitor.Exit(); or }
//End of atomic region
}
//WRONG: Entire function is synchronized. Bad idea.
using System.Runtime.CompilerServices;
MethodImplAttribute(MethodImplOptions.Synchronized)]
void foo () ;
{
int a, b;
…. //some code
//Following code has to be atomically executed
{
}
…. //Some other code
//End of atomic region
}
//Correct: Synchronizing just that block which needs atomic execution
void foo ()
{
int a, b;
Object obj;
…. //some code
lock(obj) {
//Following code has to be atomically executed
{
}
}//end of lock
//End of atomic region
…. //Some other code
}
- Faites appel à des primitives de synchronisation adaptées : il en existe une multitude qui sont livrées avec la plate-forme .NET, de celles qui ont des fonctions en nombre limité (très rapides) jusqu’à celles dont les fonctions sont multiples (très lentes). Une utilisation correcte est importante si l’on veut obtenir des performances optimales. Les primitives de synchronisations peuvent se définir ainsi :
- Monitor ou lock. Fournit un mécanisme qui synchronise l’accès aux objets.
- Interlocked. Fourni un accès atomique aux variables partagées par plusieurs threads. Par exemple, pour n’importe quelles opérations atomiques ++ ou ––, utilisez la classe Interlocked.
- Mutex. Primitives de synchronisations utilisables pour la synchronisation interprocessus. Elles sont sensiblement plus lentes, aussi ne sont-elles à utiliser qu’en cas de force majeure.
- ReaderWriterLock. C’est un verrou qui supporte la situation « writer unique / lecteurs multiples ». Dans un scénario où vous effectuez fréquemment des opérations de lecture sur vos données, mais avec des mises à jour sporadiques seulement, utilisez cette méthode, car elle supporte plusieurs lecteurs.
- ReaderWriterLockSlim. Similaire à ReaderWriterLock, mais avec des règles simplifiées pour la récurrence et la mise à niveau (upgrade et downgrade) de l’état du verrou. Permet également d’éviter les nombreux risques d’« étreintes fatales » (deadlocks) et procure des gains de performance. Son utilisation est préconisée.
- Semaphore. Limite le nombre des threads qui peuvent accéder simultanément à une ressource ou à un pool de ressources. A n’utiliser que lorsque vous avez besoin de contrôler un pool de ressources.
- N’utilisez jamais Thread.Suspend ni Thread.Resume pour la synchronisation d’activités. L’opération « suspendre et libérer » ne se déclenche pas immédiatement, car le CLR doit s’assurer que le contrôle de l’exécution se trouve dans un état sécurisé. Dans le cas contraire, on peut aboutir à des courses aux données ou à des étreintes fatales.¹
- N’utilisez jamais Thread.Abort pour arrêter un autre thread. ¹
- Ne verrouillez pas this et type sur un objet. Verrouiller ce pointeur n’est pas judicieux, pour la bonne raison que son état visible peut entraîner un problème d’exactitude. De la même manière, le fait de verrouiller le type d’un objet n’est pas souhaitable non plus, car ces objets sont identiques sur l’ensemble des domaines d’application et, de ce fait, c’est un verrouillage de toutes les instances d’objets sur l’ensemble des domaines d’application au cours d’un processus qui intervient.
//Wrong //Correct
lock (this) { public class foo {
do something; Object sync_obj = new Object();
} lock(sync_obj) {
Do something
&nbs p; }
//Wrong //Correct
lock(typeof(foo)) public class foo {
{ private static Object sync_obj = new Object();
Do something; lock (sync_obj) {
Do something;
} }
- Utilisez [ThreadStatic] pour éliminer ou réduire la contention sur le verrou. Si une donnée peut être stockée en mémoire TLS (Thread Local Storage) (par thread) plutôt que partagée, on peut traiter l’effet combiné après réalisation des tâches par les threads. Pensez-y pour réduire la contention sur le verrou.
- Réalisez l’acquisition et la libération du verrou dans le même ordre. Sinon, vous risquez de provoquer une situation d’étreinte fatale.
Thread1 Thread2
lock(obj_A) { lock(obj_B) {
lock(obj_B) { lock(obj_A) {
Do something; Do something ;
} }
} }
- Toutes les collections dans .NET ne sont pas threadsafe. Certaines classes de collections (ArrayList, par exemple) autorisent en effet plusieurs lecteurs simultanés. Il faut faire appel à méthode « Synchronized » pour la rendre threadsafe pour les mises à jour.
ArrayList myAr = new ArrayList();
ArrayList mySyncAr = ArrayList.Synchronized (myAr); //use mySyncAr
- Bien qu’elle soit synchronisée, l’énumération via les collections n’est pas non plus threadsafe. Si un autre thread modifie la collection sous-jacente, une exception se déclenchera.
Gestion automatique de la mémoire
La gestion automatique de la mémoire (GC pour « Garbage Collection » et « Garbage Collector », c’est-à-dire « Ramasse-miettes ») est l’un des services les plus importants qu’assure la plate-forme .NET. Le GC gère l’allocation et la récupération de mémoire dans une application. Chaque fois que vous appelez new pour créer un nouvel objet, et tant qu’il reste de l’espace disponible, le GC alloue de la mémoire depuis le tas (heap) managé, et lorsqu’il vient à en manquer, il déclenche une collection puis récupère de la mémoire afin de pouvoir recommencer son allocation. Nous allons à présent examiner de plus près les algorithmes du GC, leur principe de fonctionnement, les variantes de GC et l’écriture de code « GC friendly ».
Le GC .NET est un algorithme Mark & Compact générationnel. Il en existe ainsi trois générations (Gen0, 1 et 2). Le GC .NET considère que la plupart des objets créés par le programmeur « meurent jeunes » et que, de ce fait, seule une partie de votre aide managée totale peut être collectée (ce qui est beaucoup plus rapide que de collecter l’ensemble du tas managé). Le GC va d’abord pointer les objets racines (pour trouver ceux qui sont « vivants »), pour ensuite compresser le tas (en déplaçant tous les objets vivants vers une partie du tas qui constitue les générations plus anciennes). Les allocations se déroulent toujours dans le tas Gen0. Le tas Gen0 initial représente une fraction du dernier niveau du cache. L’idée consiste à faire rentrer Gen0 dans le cache pour éviter des ratés de cache.
Variantes du GC .NET :
- Workstation GC (WKS)
- Server GC (SVR)
NB. Choisir une version appropriée du GC est primordial pour la performance optimale de votre application.
Workstation (WKS) GC. Le GC WKS (stations de travail) possède deux variantes. Le GC concurrent (on), qui est la version par défaut, peut être désactivé. Ce GC aura moins de temps de pause, ce qui augmente la réactivité de l’UI. Il arrête les threads d’application pour un laps de temps plus court lorsque cela est absolument indispensable. Si vous avez une application de type throughput (de type console sans UI), le fait de désactiver le GC concurrent est susceptible de vous apporter de meilleures performances. Dans le fichier de configuration de votre application (<foo.exe.config>, par exemple), vous pouvez ajouter ce qui suit :²
<configuration>
<runtime>
<gcConcurrent enabled="false"/>
</runtime>
</configuration>
Le GC WKS dispose d’un tas et d’un thread GC par processus. Il constitue l’option par défaut pour n’importe quelle application non ASP.NET, et cela même sur des systèmes multiprocesseurs, auquel cas SP.NET sélectionne automatiquement GC SVR (serveurs).
Server GC (SVR). Comme le nom l’indique, le GC SVR est optimisé pour des applications de type serveur (meilleure scalabilité). Il possède un tas GC par processeur et un thread GC par tas GC. Si, par exemple, le système est quadriprocesseur, on a alors quatre tas et quatre threads GC fonctionnant sur chacun de ces tas. Un processus peut créer des objets dans plusieurs tas (pour un équilibrage de charge de l’allocation sur les tas) et, comme indiqué plus haut, ce n’est pas l’option par défaut. Pour activer le GC Server, ajoutez ce qui suit dans les fichiers de configuration de l’application.
<configuration>²
<runtime>
<gcServer enabled=“true"/>
</runtime>
</configuration>
Conseils pour choisir le GC approprié
- Pour toutes les applications de type serveur, sélectionnez le GC Server.
- Les applications Web ASP.NET sur machines bi ou multiprocesseurs sélectionnent automatiquement le GC SVR. Cependant, au cas où vous opteriez pour un scénario de batteries de serveur Web (« jardin » Web), faites plutôt appel au GC WKS, car la consommation mémoire pourrait s’avérer très élevée compte tenu de la multiplicité des processus w3wp. Le GC SVR considère en effet qu’il est chef de file et va de ce fait chercher à capturer le plus grand nombre de ressources possibles. Aussi, si vous avez plusieurs processus multiples qui l’exécutent, il est possible de constater une dégradation des performances ainsi qu’une augmentation de l’utilisation des ressources système. Pour activer ASP.NET en faisant appel à GC WKS, ajoutez la configuration suivante au fichier <Aspnet.config>, dans le même répertoire qu’<Aspnet_isapi.dll>.
<configuration>
<runtime>
<gcServer enabled="false"/>
<gcConcurrent enabled="false"/> </runtime>
</configuration>
- Dans le cas d’une application cliente avec interface utilisateur qui nécessite de la réactivité, choisissez plutôt GC WKS avec l’option Concurrent activée.
- Dans le cas d’une application sur console (pas d’interface utilisateur), mieux vaut désactiver le GC concurrent pour de meilleures performances.
- Si c’est une économie de ressources systèmes que vous recherchez (mémoire, etc.) alors pensez au GC WKS.
NB. Lorsque vous appelez le GC Server sur une machine monoprocesseur, l’option Concurrent du GC WKS est désactivée. Le CLR part du principe que, puisque vous appelez GC SVR, vous vous souciez plus du débit que de la réactivité de l’interface utilisateur, et il va ainsi désactiver automatiquement le GC concurrent.
Conseils pour l’écriture de code « GC friendly »
- N’appelez jamais GC.Collect depuis votre code. Le GC .NET est un GC à optimisation dynamique. A chaque collection, il collecte des informations comme le taux de survie et optimise ces paramètres internes de réglage du GC pour que le GC suivant soit plus efficace que le précédent. Contrairement à Java, le GC .NET n’affiche pas toute une série de paramètres d’optimisation pour le développeur. Donc, lorsque vous appelez GC.Collect dans votre code, celui-ci collecte ces paramètres. Puisque vous avez occasionné le GC, le GC suivant ne sera pas aussi productif. De plus, si GC.Collect s’exécute non pas une, mais plusieurs fois (par exemple avant d’entamer un travail chronophage qui implique par exemple de mettre davantage de mémoire à votre disposition), le GC ne sera absolument pas intéressant. Il existe toutefois une exception : si vous avez ouvert un formulaire personnalisé dont vous avez modifié la configuration et que vous savez ne pas en avoir de nouveau besoin avant un certain temps, vous pouvez alors poursuivre et appeler GC.Collect(), afin que meurent tous les objets à longue durée de vie en Gen2. Il est conseillé d’utiliser le code ci-dessous (à partir de la build Orcas). Dans ce cas, bien que GC.Collect soit appelé sur Gen2, le GC décide s’il peut être utile qu’il effectue une collecte (deuxième paramètre, Optimized). (A noter que cela n’est pas possible avec VS2005 et les versions antérieures.)
using System;
class Program
{
static void Main(String[] args) {
GC.Collect(2, GCCollectionMode.Optimized);
}
}
- Créez des objets qui à courte durée de vie. L’optimisation du GC .NET repose sur le principe que la plupart des objets alloués sont temporaires et meurent jeunes pour pouvoir être collectés en Gen0, qui est économique. ²
- N’allouez pas un trop grand nombre d’objets. Une seule petite ligne de code est capable de déclencher de nombreuses allocations. La plupart du temps, c’est une allocation qui déclenche une collection. Garder un œil sur ce que vous allouez, et plus spécialement sur les boucles.²
- N’allouez pas un trop grand nombre d’objets à durée moyenne. Les objets qui ne sont ni temporaires ni à longue durée de vie longue finissent en Gen2 et meurent. Cela place le tas Gzen2 sous pression et vous pourriez vous retrouver à exécuter des collections complètes, ce qui est coûteux.²
- N’allouez pas un trop grand nombre d’objets temporaires volumineux. Les objets volumineux (> 85 ko) sont alloués sur un tas d’autres objets, volumineux eux aussi. Or ce tas séparé n’est jamais compressé (il est coûteux pendant la compression de déplacer un grand nombre d’objets volumineux). Cela pourrait exercer une pression sur le tas d’objets volumineux, avec pour conséquence de vous voir effectuer des collections complètes, coûteuses encore une fois.²
- Dispose et Finalize. N’implémentez ces méthodes que si vous y êtes contraints et uniquement lorsqu’une exception se produit (pour éviter une fuite de mémoire). En outre, n’implémentez Finalize que si vous vous retrouvez avec une ressource non managée, et simplifiez le plus possible le code. ²
Conseils pour améliorer la performance du code managé
Après avoir traité le threading et le GC, nous allons à présent aborder la MV en général, la génération du code et des conseils de bases ASP.NET et ADO.NET pour l’écriture d’un code de meilleure qualité
- Evitez le boxing inutile.¹
int i = 123;
object o = i; (Implicit boxing) //box keyword
int j = (int)o; //unbox keyword
Chaque fois que l’on effectue un boxing, un nouvel objet se crée sur le tas managé et la valeur est copiée dans celui-ci. Si cette opération se répète fréquemment, on crée alors un grand nombre d’objets (avec une incidence sur le GC) ainsi que le code supplémentaire que l’on exécute pour une conversion boxing et unboxing.
Foo myFoo = new Foo();
myArrayList.Add(myFoo);
Foo myFoo = (Foo) myArrayList[i]; //castclass keyword
Les classes de collection adoptent la générique « objet » comme paramètre. Un casting de type est nécessaire lorsque l’on récupère des objets (votre type) depuis les classes de collection. Cela nécessite une vérification coûteuse de type runtime en observant la table de méthode de cet objet. Si votre objet est hérité, cela pourrait impliquer de sauter un niveau, ce qui, là encore, est coûteux. On peut éviter cela en utilisant des génériques (similaires à un template C++) comme décrit ci-dessous et qui ne nécessitent pas de runtime de vérification de type connue au moment de la compilation.
List<Foo> myList = new List<Foo>();
Foo myfoo = myList[i]; //no check reqd
- Lancez moins d’exceptions. Le fait de lancer des exceptions peut être coûteux, car cela nécessite de procéder pas à pas pour la gestion des frames. N’utilisez pas les exceptions comme instance de contrôle de flux dans votre application.
<Wrong>
void foo (int parameter)
{ int ret = 0;
val = …. ;
try
& nbsp; {
ret = val / parameter;
}catch(DivideByZeroException) { return ERROR_VAL ;}
}
<Correct>
void foo (int parameter)
{
if (parameter == 0) return ERROR_VAL; else {… ;}
}
- Utilisez StringBuilder pour des manipulations de chaînes complexes. Chaque fois que vous modifiez une chaîne (comme ajouter, etc.), cela en crée une nouvelle et la première est prête à être collectée. Si vous avez de cinq à sept manipulations de chaînes, pensez à utiliser StringBuilder.
- N’utilisez pas trop d’API de réflexion. Ces API reposent sur les métadonnées embarquées dans les assemblies. C’est pourquoi l’analyse et la recherche de cette information sont très coûteuses.
- Ne virtualisez pas et ne synchronisez pas inutilement les fonctions. JIT risquerait de désactiver certaines optimisations avec pour conséquence un code généré qui pourrait ne pas être optimal.
- N’écrivez pas de fonctions trop grandes. JIT risquerait de désactiver certaines optimisations afin de raccourcir les délais de compilation (JIT).
- Evitez d’appeler de petites fonctions dans une boucle. Alignez-vous (au cas où JIT ne l’aurait pas fait). Toutes les erreurs dans une boucle sont accentuées.
- Préférez les tables aux collections, à moins que vous n’ayez besoin des fonctions supplémentaires que les classes de collections fournissent. ¹
- Utilisez des chaînes en escalier plutôt que multidimensionnelles, car les premières possèdent des optimisations MSIL spéciales pour des accès aux chaînes plus rapides.¹
- Des ensembles de travail plus petits permettent de meilleures performances. Veillez donc à utiliser ngen pour les pages partagées.
- N’abusez pas des appels de code non managé Pinvoke (appels bavards) et effectuez moins de tâches en code non managé. Le coût des transitions (managé vers non managé et inversement) peut annuler les gains de performances voire dégrader celles-ci.
Ngen. <Ngen.exe> (livré avec le CLR) fait appel au compilateur JIT sur MSIL afin de générer du code natif et le stocker sur le disque. Une fois l’image native créée, le runtime utilise cette image automatiquement chaque fois qu’elle exécute l’assembly. Le fait d’utiliser l’image native élimine la compilation à la volée en utilisant le compilateur JIT à l’exécution et permet ainsi de réduire le délai de démarrage de l’application.
<Ngen.exe> peut améliorer les performances d’une application :
- En raccourcissant ses délais de démarrage. Utilisez <ngen.exe> pour raccourcir le délai de démarrage de votre application winform. A chaque fois, mesurez votre application avec et sans « ngening ».
- En réduisant l’utilisation globale de mémoire pour une application qui utilise des assemblies partagées (chargées dans différents domaines d’application).
Interop. Lorsque vous construisez des applications en code managé, il peut parfois s’avérer nécessaire d’appeler des bibliothèques non managées en appelant par exemple un composant COM. Dans certaines circonstances, vous pouvez également, dans un souci de performances, utiliser du code non managé (en appelant par exemple des bibliothèques tierces fortement optimisées). CLR fournit plusieurs chemins pour y parvenir :
- En utilisant Pinvoke (Platform Invoke). Tous les appels ows de DLL Windows, d’API Win32 ou de DLK personnalisées en provenance de code managé.¹
- En utilisant MC++ (IJW). Pour permettre aux utilisateurs de MC++ d’appeler les DLL standards.¹
- COM Interop. Gérer les langages pour appeler des composants COM via des interfaces COM. ¹
Améliorer les performances Interop ¹
- Evitez les appels bavards qui augmentent les allers-retours inutiles, car cela entraîne un surcoût à cause des transitions multiples.
- Evitez le marshalling inefficace de paramètres, car cela entraîne une perte inutile de cycles processeurs système.
- Procédez au nettoyage complet (Dispose) des composants non managés, faute de quoi il peut y avoir une incidence sur l’utilisation mémoire du serveur et entraîner des fuites de mémoire.
- Ne pointez pas trop énergiquement les objets à durée de vie courte, car cela risque de conduire à une fragmentation sur le tas managé, qui peut entraîner une dégradation des performances.
Améliorer la performance ASP.NET
- Appliquez des stratégies de mise en cache efficaces.¹ Une stratégie de mise en cache bien pensée est l’élément le plus important dans la phase de design d’une application. Il existe différentes méthodes de mise en cache — de sortie, de page partiel, etc. — qui doivent permettre de réduire les allers-retours vers la base de données. Il est primordial de faire une analyse afin de déterminer où la mise en cache s’impose. Il vous faudra en particulier veiller à la mise en cache :
- Si la création ou la recherche de données ou d’output sont très coûteuses.
- En cas d’utilisation fréquente.
- Si les données sont plutôt statiques et peu susceptibles de changer.
- Fractionnez votre application de façon logique (présentation, logique économique, couche ado.net [base de données]) pour qu’elle soit d’une maintenance aisée et pour optimiser individuellement les couches.¹
- Désactivez la connexion IIS sur le serveur d’applications.
- Utilisez les contrôles serveur de manière efficace, faute de quoi le délai de chargement des pages risque de s’allonger.
- Améliorez les temps de réponses des pages. Utilisez Page.IsPostBack pour limiter allers-retours avec le serveur.
- Assurez-vous que les pages sont compilées par lots. En mélangeant plusieurs langages dans le même répertoire, la compilation de l’ensemble des pages ne pourra pas se faire en une seule assembly.
- Assurez-vous que l’attribut de débogage n’est pas positionné sur les pages.
- Effectuez Validate et Fail le plus tôt possible pour éviter un travail coûteux.
- N’utilisez l’état d’interface utilisateur que lorsque c’est indispensable. L’état de l’interface utilisateur est séquentialisé et déséquentialisé sur le serveur, ce qui est coûteux.
Améliorer la performance ADO.NET
- Utilisez des procédures stockées. Ce sont des procédures simples pour la maintenance et l’amélioration des performances SQL.
- Analysez et utilisez data reader et data set judicieusement.¹ Pensez à utiliser des lecteurs de données :
- Si vous n’avez pas besoin de mettre en cache les données ou bien que les données sont en lecture seule.
- Lorsque vous souhaitez rechercher rapidement un grand nombre d’enregistrements.
- Si vous n’avez pas à choisir des enregistrements aléatoires.
- Utilisez try{} et finally{} pour vérifier la fermeture des connexions. Si une exception se produit et que vous avez entré close à la fin, essayez block ou in catch block. L’exécution pourra peut-être s’interrompre et maintenir les connexions ouvertes.
- N’allez chercher dans la base que les données dont vous avez besoin, pour limiter le délai.
- Utilisez une transaction de type adéquat (niveau SQL, ADO.NET et ASP.NET) et limitez sa durée.
- Utilisez le mécanisme de pagination si votre objectif est d’obtenir de larges ensembles de données pour améliorer le confort utilisateur et réduire le délai.
Après des conseils, trucs, astuces et autres bonnes pratiques pour écrire du code .Net ultraperformant, voici à présent, mais sans entrer dans le détail, une liste d’outils de stimulation des performances dans le cadre d’une l’optimisation de code .Net :
Perfmon. Cet outil de niveau système propose plusieurs compteurs liés au CLR et ASP.NET. C’est le premier auquel il convient de faire appel pour l’analyse d’une application .NET quelle qu’elle soit. Les compteurs disponibles seront abordés plus en détail dans de futurs articles.
Analyseur Vtune™. C’est un outil de profilage d’Intel qui supporte les applications .NET, y compris ASP.NET.
CLR Profiler. C’est un outil de Microsoft utilisé pour profiler la mémoire (allocation) d’une application. Il est gratuit et téléchargeable depuis MSDN.
SOS. OOutil de Microsoft qui permet de gérer les extensions de débogage. Il est gratuit et livré comme <SOS.dll> avec le CLR. Il affiche de nombreuses structures internes de données du CLR dont le GC, les exceptions, les objets, le verrouillage, etc. Il peut s’employer pour repérer des bogues de fonctions (OutOfMemoryException, par exemple) ainsi que d’autres en rapport avec les performances (verrouillage, etc.)
VSTS Profiler. C’est un profileur intégré, issu de Visual Studio Team system 2008 de Microsoft. Il est capable d’échantillonner une application et de repérer des chaînes hotspot et hot call, etc.
VSTS. Le système Visual Studio Team de Microsoft (pour les testeurs) intègre une fonction qui permet d’analyser les performances de charge pour des applications Web de Tier n. Il est d’un emploi très simple et offre la possibilité d’enregistrer des URL et de visualiser, depuis un système client, les compteurs perform de l’ensemble des machines, etc.
Résumé
A l’ère de l’Internet, les performances applicatives sont essentielles à la compétitivité de l’entreprise et à sa réussite. Le fait d’intégrer une ingénierie des performances tout au long du cycle de développement logiciel est primordial pour atteindre voire dépasser les objectifs en la matière. De plus, cette ingénierie des performances doit être proactive et non réactive (il ne faut pas attendre qu’un client se plaigne d’un problème, par exemple). Cet article propose ainsi des informations, des conseils et des bonnes pratiques (BKM) pour améliorer les performances. Il passe également en revue les problèmes susceptibles d’intervenir dans le threading, etc. pour ceux qui utilisent le SDK de Microsoft Framework.
Références
¹ « Improving .NET Application Performance and Scalability »
(Amélioration des performances et de la scalabilité des applications .NET).
² Maoni’s WebLog – CLR Garbage Collector (http://blogs.msdn.com/maoni).
Reportez-vous à notre Notice d'optimisation pour plus d'informations sur les choix et l'optimisation des performances dans les produits logiciels Intel.
Commentaires (0) 
Trackbacks (0)
Réagir 
Day Kol (Intel)
|
