Conseils pour l'amélioration du code à durée critique
L'écriture d'un code rapide passe par une bonne compréhension de tous les aspects de votre application et la façon dont elle interagit avec le système.Cette rubrique suggère des alternatives à quelques techniques de codage parmi les plus évidentes afin de vous aider à faire en sorte que les portions à durée critique de votre code s'exécutent correctement.
En résumé, pour améliorer le code à durée critique, vous devez connaître :
les parties de votre programme qui doivent être rapides ;
la taille et la vitesse de votre code ;
le coût des nouvelles fonctionnalités ;
la somme de travail minimale requise pour accomplir cette tâche.
Pour collecter des informations sur les performances de votre code, vous pouvez utiliser l'Analyseur de performances (perfmon.exe).
Accès au cache et erreurs de page
Tri et recherche
MFC et les bibliothèques de classes
Bibliothèques partagées
Tas
Threads
Petit jeu de travail
Accès au cache sans résultat et erreurs de page
L'accès au cache sans résultat, que ce soit le cache interne ou le cache externe, ainsi que les erreurs de page (l'accès à la mémoire auxiliaire pour les données et les instructions du programme) ralentissent l'exécution d'un programme.
L'accès au cache d'une UC peut coûter à votre programme jusqu'à 10 à 20 cycles d'horloge.Un accès au cache externe peut coûter entre 20 et 40 cycles d'horloge.Une erreur de page peut coûter jusqu'à un million de cycles d'horloge (prenons l'exemple d'un processeur qui gère 500 millions d'instructions par seconde et un délai de 2 millisecondes par erreur de page).Par conséquent, il convient pour l'exécution du programme d'écrire un code qui réduit le nombre d'accès au cache sans résultat et les erreurs de page.
Si certains programmes sont lents c'est en partie parce qu'ils acceptent un grand nombre d'erreurs de page ou accèdent au cache sans résultat plus que nécessaire.Pour éviter cette situation, il est important d'utiliser des structures de données avec une localité de référence appropriée, ce qui revient à garder ensemble les éléments connexes.Parfois, une structure de données qui apparaît formidable se révèle être désastreuse en raison d'une localité de référence médiocre, mais l'inverse est vrai aussi parfois.En voici deux exemples :
Les listes liées allouées de manière dynamique peuvent diminuer les performances du programme car lorsque vous cherchez un élément ou lorsque vous traversez une liste jusqu'à la fin, chaque lien sauté peut générer un accès au cache sans résultat ou provoquer une erreur de page.Une implémentation de liste basée sur de simples tableaux peut en fait être beaucoup plus rapide en raison d'une meilleure mise en cache et d'un nombre moindre d'erreurs de page ; même si le tableau est plus difficile à développer, cette méthode est quand même plus rapide.
Les tables de hachage qui utilisent les listes liées à allocation dynamique peuvent diminuer les performances.Par extension, les tables de hachage qui utilisent les listes liées à allocation dynamique pour stocker leur contenu peuvent donner lieu à un résultat encore plus médiocre.En fait, et en dernière analyse, une recherche linéaire simple dans un tableau peut se révéler plus rapide (en fonction des circonstances).Les tables de hachage basées sur les tableaux (appelées « hachage fermé ») représentent une implémentation qui est souvent oubliée, mais dont les performances sont excellentes.
Tri et recherche
Le tri est, par nature, une opération qui demande beaucoup de temps par rapport à de nombreuses autres opérations types.La meilleure façon d'éviter un ralentissement inutile est d'éviter le tri aux moments critiques.Vous pouvez :
remettre le tri à un moment non critique pour les performances ;
trier les données en amont, à un moment non critique pour les performances ;
trier uniquement la partie des données qui en a vraiment besoin.
Vous pouvez, parfois, générer la liste dans un ordre trié.Soyez vigilant, car si vous devez insérer des données dans un ordre trié, vous aurez peut-être besoin d'une structure de données plus complexe avec une localité de référence médiocre, d'où des accès au cache sans résultat et des erreurs de page.Aucune approche n'est adaptée à tous les cas.Essayez plusieurs approches et évaluez les différences.
Voici quelques conseils d'ordre général pour le tri :
Utilisez un tri stock pour réduire les bogues.
Toutes les tâches préalables susceptibles de réduire la complexité du tri valent la peine d'être exécutées.Si un seul passage sur vos données permet de simplifier les comparaisons et de réduire le tri de O(n journal n) à O(n), vous serez vraisemblablement gagnant*.*
Pensez à la localité de référence de l'algorithme de tri et aux données qu'il doit normalement utiliser pendant son exécution.
Il existe moins d'alternatives pour la recherche que pour le tri.Si la recherche est à durée critique, une recherche binaire ou une consultation de la table de hachage est toujours préférable, mais comme avec le tri, vous ne devez pas perdre de vue la localité.Une recherche linéaire dans un petit tableau peut être plus rapide qu'une recherche binaire dans une structure de données comportant un grand nombre de pointeurs, qui provoque des erreurs de page ou des accès au cache sans résultat.
MFC et les bibliothèques de classes
Les MFC (Microsoft Foundation Classes) peuvent simplifier grandement l'écriture du code.Lorsque vous écrivez du code à durée critique, vous devez connaître le temps système inhérent à certaines classes.Examinez le code MFC que votre code à durée critique utilise pour déterminer s'il répond aux critères de performances requis.La liste suivante recense les classes MFC et les fonctions que vous devez connaître :
CString MFC appelle la bibliothèque Runtime C pour allouer de manière dynamique de la mémoire à une CString.Dans l'ensemble, CString est aussi efficace que n'importe quelle autre chaîne allouée dynamiquement.Comme pour toute chaîne allouée de manière dynamique, elle est associée au temps système d'une libération et d'une allocation dynamique.Souvent, un tableau char simple sur la pile peut jouer le même rôle et être plus rapide.N'utilisez pas une CString pour stocker une chaîne constante.Utilisez plutôt const char *.Toute opération que vous exécutez à l'aide d'un objet CString se caractérise par un certain temps système.L'utilisation des fonctions de chaîne de la bibliothèque Runtime peut permettre de gagner du temps.
CArray La classe CArray offre la souplesse qu'un tableau ordinaire ne permet pas, mais votre programme n'en a peut-être pas besoin.Si vous connaissez les limites spécifiques du tableau, vous pouvez lui préférer un tableau fixe global.Si vous faites appel à CArray, utilisez CArray::SetSize pour établir sa taille et spécifier le nombre d'éléments nécessaire à son extension lorsqu'une réallocation est requise.Sinon, l'ajout d'éléments peut entraîner la réallocation et la copie fréquentes de votre tableau, ce qui est inefficace et présente un risque de fragmentation de la mémoire.Sachez également que si vous insérez un élément dans un tableau, CArray déplace les éléments suivants en mémoire et peut avoir besoin d'augmenter la taille du tableau.Ces actions peuvent donner lieu à des accès au cache sans résultat et à des erreurs de page.Si vous examinez le code que les MFC utilisent, vous constaterez que vous pouvez écrire quelque chose de plus spécifique dans votre scénario pour améliorer les performances.Dans la mesure où CArray est un modèle, par exemple, vous pouvez fournir des spécialisations CArray pour des types spécifiques.
CList CList est une liste doublement liée ; par conséquent, l'insertion d'éléments est rapide au début, à la fin et au niveau d'une position connue (POSITION) dans la liste.La recherche d'un élément par valeur ou index requiert toutefois un processus séquentiel pouvant être lent dans le cas d'une longue liste.Si votre code ne requiert pas de liste doublement liée, vous pouvez reconsidérer l'utilisation de CList.L'utilisation d'une liste liée simple économise le temps système que représente la mise à jour d'un pointeur supplémentaire pour toutes les opérations ainsi que la mémoire correspondant à ce pointeur.La mémoire supplémentaire n'est pas énorme, mais il s'agit d'un nouveau risque pour un accès au cache sans résultat et des erreurs de page.
IsKindOf Cette fonction peut générer de nombreux appels et accéder à une grande quantité de mémoire dans différentes zones de données, d'où une localité de référence incorrecte.Elle est utile pour une version Debug (dans un appel ASSERT, par exemple), mais essayez d'éviter de l'utiliser dans une version Release.
PreTranslateMessage Utilisez PreTranslateMessage lorsqu'une arborescence particulière de fenêtres nécessite d'autres accélérateurs clavier ou lorsque vous devez insérer une fonction de gestion de messages dans la pompe de messages.PreTranslateMessage modifie les messages de distribution MFC.Si vous substituez PreTranslateMessage, faites-le seulement au niveau requis.Par exemple, il n'est pas nécessaire de substituer CMainFrame::PreTranslateMessage si seuls les messages adressés aux enfants d'une vue particulière vous intéressent.Substituez plutôt PreTranslateMessage pour la classe d'affichage.
Ne contournez pas le chemin de distribution normal en utilisant PreTranslateMessage pour gérer des messages envoyés à une fenêtre quelconque.Utilisez à cet effet les procédures de fenêtre et les tables des messages MFC.
OnIdle Des événements d'inactivité se produisent aux moments où vous ne les attendez pas, comme par exemple entre les événements WM_KEYDOWN et WM_KEYUP .Les minuteries (Timers) peuvent être un moyen efficace pour le déclenchement du code.Ne forcez pas l'appel répétitif de OnIdle en générant de faux messages ou en retournant toujours TRUE à partir d'une substitution de OnIdle, qui ne laisserait jamais votre thread se reposer.Là encore, une minuterie (Timer) ou un thread distinct peut être une solution plus appropriée.
Bibliothèques partagées
La réutilisation du code est souhaitable.Cependant, si vous pensez utiliser le code d'une autre personne, vous devez veiller à bien connaître le comportement de ce code dans les cas où les performances constituent un critère crucial pour vous.La meilleure façon de le savoir est d'exécuter pas à pas le code source ou d'effectuer des mesures à l'aide d'outils tels que PView ou l'Analyseur de performances.
Tas
Utilisez des tas multiples avec discernement.Les tas supplémentaires créés à l'aide de HeapCreate et HeapAlloc permettent de gérer, puis de supprimer un jeu connexe d'allocations.N'attribuez pas une trop grande quantité de mémoire.Si vous utilisez des tas multiples, portez une attention particulière à la quantité de mémoire qui est attribuée initialement.
À la place de tas multiples, vous pouvez utiliser des fonctions d'assistance pour servir d'interface entre le code et le tas par défaut.Les fonctions d'assistance facilitent les stratégies d'allocation personnalisées qui peuvent améliorer les performances de votre application.Par exemple, si vous effectuez fréquemment de petites allocations, vous pouvez souhaiter implanter ces allocations dans une partie du tas par défaut.Vous pouvez allouer un grand bloc de mémoire, puis utiliser une fonction d'assistance pour effectuer une sous-allocation à partir de ce bloc.Si vous procédez ainsi, vous n'aurez pas de tas supplémentaires avec la mémoire non utilisée car l'allocation provient du tas par défaut.
Dans certains cas, cependant, l'utilisation du tas par défaut peut réduire la localité de référence.Utilisez Process Viewer, Spy++ ou l'Analyseur de performances pour mesurer les effets de déplacement des objets d'un tas à un autre.
Mesurez vos tas de façon à pouvoir tenir compte de chaque allocation sur le tas.Utilisez les routines de tas de débogage d'exécution C pour vérifier votre tas et en faire un dump.Vous pouvez enregistrer le résultat dans un tableur tel que Microsoft Excel et utiliser des tables pivots pour afficher les résultats.Notez le nombre total, la taille et la distribution des allocations.Comparez-les à la taille des jeux de travail.Examinez également le « clustering » des objets de taille connexe.
Vous pouvez aussi utiliser les compteurs de performance pour surveiller l'utilisation de la mémoire.
Threads
Pour les tâches de fond, une gestion d'inactivité effective des événements peut être plus rapide que l'utilisation des threads.Il est plus facile de comprendre la localité de référence dans un programme monothread.
En règle générale, il vaut mieux utiliser un thread uniquement si une notification du système d'exploitation sur laquelle vous êtes bloqué se trouve à la racine de votre travail d'arrière-plan.Les threads représentent la meilleure solution dans pareil cas, car il n'est pas pratique de bloquer un thread principal sur un événement.
Les threads présentent également des problèmes de communication.Vous devez manager la liaison de communication entre les threads, avec une liste de messages ou en allouant et en utilisant une mémoire partagée.La gestion de la liaison de communication passe généralement par une synchronisation afin d'éviter les conditions de concurrence critique et les problèmes d'interblocage.Cette complexité peut aisément se transformer en bogues et en problèmes de performance.
Pour plus d'informations, consultez Traitement de la boucle d'inactivité et Multithreading.
Petit jeu de travail
Les petits jeux de travail signifient une meilleure localité de référence, moins d'erreurs de page et plus d'accès au cache avec résultat.Le jeu de travail du processus est la mesure quantitative la plus proche que le système d'exploitation fournit directement pour la mesure de la localité de référence.
Pour définir les limites supérieure et inférieure du jeu de travail, utilisez SetProcessWorkingSetSize.
Pour obtenir les limites supérieure et inférieure du jeu de travail, utilisez GetProcessWorkingSetSize.
Pour afficher la taille du jeu de travail, utilisez Spy++.