11.2.2.1 : Préparer l'optimisation



Avant d'optimiser un programme, il est nécessaire de vérifier s'il ne contient pas de bogues. Cette tâche est difficile à réaliser dans des analyses importantes mais est grandement facilitée par l'utilisation de tests unitaires noteLes tests unitaires sont des programmes qui vérifient qu'une fonctionnalité se comporte bien comme prévu avec des jeux de paramètres définis. Quand une fonctionnalité ajoutée à un programme est accompagnée de tests unitaires vérifiant son bon fonctionnement, il est possible de les utiliser tout au long de la vie du programme pour vérifier que les mises à jour ne cassent pas cette fonctionnalité. Lors d'un cycle de développement standard, les tests unitaires sont lancés au moins une fois pas jour pour vérifier que le programme en question reste cohérent. tout au long du développement.

Un niveau de vérification supplémentaire peut être mis en place en utilisant le programme valgrind[60]Valgrind pour détecter des opérations illégales au cours de l'exécution du programme qui sont sources de bogues cachés noteLes bogues cachés ne se produisent pas toujours durant l'exécution du programme, ce qui les rend extrêmement difficiles à corriger. (voir cours~[144]Introduction to the Valgrind Debugger/Profiler, Pierre Aubert).

Certains compilateurs fournissent des outils de vérification similaires, appelés "sanitizers", qui ont l'avantage de détecter plus de problèmes que valgrind et de produire moins de faux positifs grâce à une connaissance plus précise du programme étudié. Malheureusement, ces outils ne sont pas disponibles pour tous les langages de programmation, et leur utilisation nécessite d'effectuer une compilation spéciale du programme et de toutes les bibliothèques tierces où l'on souhaite rechercher des erreurs.

Dès lors que tous les bogues (connus) du programme sont corrigés, il est important de vérifier si ce dernier ne produit pas de fuite mémoire. En effet, une fuite mémoire fera augmenter progressivement la quantité de mémoire RAM utilisée par le programme jusqu'à ce que la totalité de la RAM soit saturée. Certaines fuites peuvent passer inaperçues lors de tests sur une quantité de données réduite (des tests unitaires par exemple). Le programme valgrind[60]Valgrind permet aussi un diagnostic de l'utilisation de la mémoire afin de détecter toute fuite mémoire. Il s'avère très efficace d'utiliser valgrind sur les tests unitaires de manière automatique pour prévenir ce genre de problèmes.

Lorsque les bogues et les fuites mémoire ont été corrigés, il est nécessaire d'évaluer quelles fonctions devront être optimisées en priorité. Ce seront généralement les fonctions les plus fréquemment appelées. Là encore, il est important d'utiliser des outils adéquats. Les programmes valgrind[60]Valgrind, maqao[57]Maqao, gprof[58]GProf et perf[59]Perf permettent d'effectuer un profilage du programme afin de déterminer les fonctions les plus coûteuses, dite chaudes. Il faut néanmoins prendre garde à ce que le programme profilé ne s'exécute pas trop rapidement pour que les fonctions d'initialisation ne prennent pas le pas sur les fonctions qui seront effectivement les plus utilisées en production.

Le profilage donne le point de départ de l'optimisation mais quelques réflexes permettent de développer des programmes plus rapides.

Les programmes utilisent des bibliothèques tierces, fournies par d'autres développeurs tels que GNU, Khronos, KitWare, ou Apache. Il peut être difficile pour les développeurs d'une analyse de physique de physique de modifier ces bibliothèques, car elles sont parfois très complexes et un changement pérenne ne peut y être instauré qu'avec l'aide de leurs développeurs, qui ne sont pas toujours ouverts à cette possibilité. Si un programme utilise une fonction lente que ses développeurs ne peuvent pas optimiser, il est possible de contourner le problème en appelant cette fonction le moins souvent possible.

Par exemple, si un tableau est nécessaire pour effectuer un calcul sur un événement. Il est intéressant de ne l'allouer qu'une seule fois pour analyser tous les événements à traiter et de le réutiliser. Les fonctions d'allocations sont lentes, mais l'utilisation de temporaires (voirsection 11.2.2.3.5) permet d'amortir leurs appels. La lisibilité d'un programme peut être améliorée en regroupant tous les temporaires d'une même tâche dans une classe commune.

Dans un autre cas d'utilisation, si le programme doit appeler une base de données, il peut être intéressant de n'effectuer qu'une seule requête complexe plutôt que de nombreuses requêtes simples en stockant le résultat obtenu pour ne pas rappeler la base de données. De plus, les bases de données peuvent subir des latences très importantes. Un programme qui dépend d'une base de données devra donc être conçu pour être affecté le moins possible par ses temps de réponse.

Enfin, un programme peut être ralenti par l'utilisation d'un format de données inadapté pour le HPC. Dans ce cas, le format de données devra être optimisé avant les calculs (voirsection suivante).

Écrire de petites fonctions :
  • Permet de cerner beaucoup plus facilement les calculs qui coûtent le plus cher en performances. Notamment à l'aide de programmes de profilage comme valgrind[60]Valgrind.
  • Facilite le débogage (une fonction de quelques lignes sera plus simple à comprendre qu'une fonction de 1000 lignes).
  • Facilite l'optimisation du compilateur, car les calculs effectués seront plus simples donc plus facilement optimisables.