OK, vous définissez le problème là où il semblerait qu'il n'y ait pas beaucoup de place à l'amélioration. C'est assez rare, d'après mon expérience. J'ai essayé d'expliquer cela dans un article du Dr Dobbs en novembre 1993, en partant d'un programme non trivial conventionnellement bien conçu sans gaspillage évident et en passant par une série d'optimisations jusqu'à ce que son temps d'horloge murale soit réduit de 48 secondes. à 1,1 seconde, et la taille du code source a été réduite d'un facteur 4. Mon outil de diagnostic était le suivant . La séquence de changements était la suivante:
Le premier problème rencontré a été l'utilisation de clusters de listes (maintenant appelés "itérateurs" et "classes de conteneurs") représentant plus de la moitié du temps. Ceux-ci ont été remplacés par du code assez simple, ce qui a réduit le temps à 20 secondes.
Maintenant, le plus grand preneur de temps est plus de construction de listes. En pourcentage, il n'était pas si important auparavant, mais maintenant c'est parce que le plus gros problème a été supprimé. Je trouve un moyen de l'accélérer et le temps passe à 17 secondes.
Maintenant, il est plus difficile de trouver des coupables évidents, mais il y en a quelques-uns plus petits sur lesquels je peux faire quelque chose, et le temps passe à 13 secondes.
Maintenant, il me semble que j'ai heurté un mur. Les échantillons me disent exactement ce qu'il fait, mais je n'arrive pas à trouver quoi que ce soit que je puisse améliorer. Ensuite, je réfléchis à la conception de base du programme, à sa structure axée sur les transactions, et je demande si toutes les recherches de liste qu'il fait sont réellement imposées par les exigences du problème.
Ensuite, je suis tombé sur une refonte, où le code du programme est réellement généré (via des macros de préprocesseur) à partir d'un ensemble de sources plus petit, et dans lequel le programme ne trouve pas constamment des choses que le programmeur sait être assez prévisibles. En d'autres termes, n'interprétez pas la séquence des choses à faire, ne la "compilez" pas.
- Cette refonte est effectuée, réduisant le code source d'un facteur 4, et le temps est réduit à 10 secondes.
Maintenant, parce que ça devient si rapide, c'est difficile à échantillonner, donc je lui donne 10 fois plus de travail à faire, mais les temps suivants sont basés sur la charge de travail d'origine.
Plus de diagnostic révèle qu'il passe du temps à gérer les files d'attente. L'intégration de ces éléments réduit le temps à 7 secondes.
Maintenant, un grand preneur de temps est l'impression de diagnostic que je faisais. Rincer - 4 secondes.
Maintenant, les plus grands preneurs de temps sont les appels à malloc et gratuits . Recycler les objets - 2,6 secondes.
En continuant à échantillonner, je trouve toujours des opérations qui ne sont pas strictement nécessaires - 1,1 seconde.
Facteur d'accélération total: 43,6
Maintenant, il n'y a pas deux programmes identiques, mais dans les logiciels non-jouets, j'ai toujours vu une progression comme celle-ci. D'abord, vous obtenez les choses faciles, puis les plus difficiles, jusqu'à ce que vous arriviez à un point de rendements décroissants. Ensuite, les informations que vous obtiendrez pourraient bien conduire à une refonte, en commençant une nouvelle série d'accélérations, jusqu'à ce que vous atteigniez à nouveau des rendements décroissants. Maintenant, c'est le point auquel il pourrait être judicieux de se demander si ++i
ou i++
ou for(;;)
ouwhile(1)
sont plus rapides: le genre de questions que je vois si souvent sur Stack Overflow.
PS On peut se demander pourquoi je n'ai pas utilisé de profileur. La réponse est que presque chacun de ces "problèmes" était un site d'appel de fonction, qui empile les échantillons avec précision. Les profileurs, même aujourd'hui, viennent à peine à l'idée que les instructions et les instructions d'appel sont plus importantes à localiser et plus faciles à corriger que des fonctions entières.
J'ai en fait construit un profileur pour le faire, mais pour une réelle intimité avec ce que fait le code, il n'y a pas de substitut pour mettre les doigts dedans. Ce n'est pas un problème que le nombre d'échantillons soit petit, car aucun des problèmes détectés n'est si minuscule qu'ils sont facilement ignorés.
AJOUT: jerryjvl a demandé quelques exemples. Voici le premier problème. Il se compose d'un petit nombre de lignes de code distinctes, prenant ensemble la moitié du temps:
/* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)
Celles-ci utilisaient le cluster de listes ILST (similaire à une classe de liste). Ils sont mis en œuvre de la manière habituelle, la «dissimulation d'informations» signifiant que les utilisateurs de la classe n'étaient pas censés avoir à se soucier de la façon dont ils étaient mis en œuvre. Lorsque ces lignes ont été écrites (sur environ 800 lignes de code), on n'a pas pensé à l'idée qu'elles pouvaient être un "goulot d'étranglement" (je déteste ce mot). Ils sont simplement la façon recommandée de faire les choses. Il est facile de dire avec le recul que cela aurait dû être évité, mais d'après mon expérience, tous les problèmes de performances sont comme ça. En général, il est bon d'essayer d'éviter de créer des problèmes de performances. Il est encore mieux de trouver et de corriger ceux qui sont créés, même s'ils "auraient dû être évités" (avec le recul).
Voici le deuxième problème, sur deux lignes distinctes:
/* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)
Il s'agit de créer des listes en ajoutant des éléments à leurs extrémités. (Le correctif consistait à collecter les éléments dans des tableaux et à créer les listes en une seule fois.) La chose intéressante est que ces instructions ne coûtaient (c.-à-d. Étaient sur la pile des appels) 3/48 du temps d'origine, donc elles n'étaient pas dans fait un gros problème au début . Cependant, après avoir éliminé le premier problème, ils coûtaient 3/20 du temps et étaient donc maintenant un "plus gros poisson". En général, c'est comme ça que ça se passe.
Je pourrais ajouter que ce projet a été distillé d'un vrai projet sur lequel j'ai aidé. Dans ce projet, les problèmes de performances étaient beaucoup plus dramatiques (tout comme les accélérations), comme l'appel d'une routine d'accès à la base de données dans une boucle interne pour voir si une tâche était terminée.
RÉFÉRENCE AJOUTÉE: Le code source, à la fois original et remanié, peut être trouvé sur www.ddj.com , pour 1993, dans le fichier 9311.zip, les fichiers slug.asc et slug.zip.
EDIT 2011/11/26: Il existe maintenant un projet SourceForge contenant le code source dans Visual C ++ et une description détaillée de la façon dont il a été réglé. Il ne passe que par la première moitié du scénario décrit ci-dessus, et il ne suit pas exactement la même séquence, mais obtient toujours une accélération de 2 à 3 ordres de grandeur.