Nous travaillons sur une base de code C ++ de taille moyenne (10Mloc) qui, grâce à nos efforts d'optimisation, devient uniformément lente .
Cette base de code est un ensemble de bibliothèques que nous combinons pour les mettre au travail. Lorsque le cadre général de la façon dont ces bibliothèques communiquent a été développé, l'accent a été mis sur les performances et plus tard, lorsque davantage de parties ont été ajoutées, le cadre général n'a pas beaucoup changé. L'optimisation a été effectuée en cas de besoin et à mesure que notre matériel évoluait. Cela a rendu la décision précoce coûteuse apparente seulement beaucoup plus tard. Nous sommes maintenant à un point où de nouvelles optimisations sont beaucoup plus coûteuses car elles nécessiteraient la réécriture de grandes parties de la base de code. Nous nous trouvons à l'approche d'un minimum local indésirable car nous savons qu'en principe le code devrait pouvoir s'exécuter beaucoup plus rapidement.
Existe-t-il des méthodologies réussies qui aident à décider des virages pour prendre en charge l'évolution d'une base de code vers une solution globale et optimisée qui ne soit pas facilement confondue par des opportunités d'optimisation faciles?
ÉDITER
Pour répondre à la question de savoir comment nous profilons actuellement:
Nous n'avons vraiment que 2 scénarios différents sur la façon dont ce code peut être utilisé, tous deux parallèlement embarrassants. Le profilage se fait à la fois avec une horloge murale moyenne sur un grand échantillon d'entrées et des exécutions plus détaillées (coûts d'instruction, erreurs de prédiction de branche et problèmes de mise en cache). Cela fonctionne bien puisque nous fonctionnons exclusivement sur nos machines extrêmement homogènes (un cluster de quelques milliers de machines identiques). Comme nous gardons généralement toutes nos machines occupées la plupart du temps plus rapidement, nous pouvons examiner de nouvelles choses supplémentaires. Le problème est bien sûr que lorsque de nouvelles variations d'entrée apparaissent, elles peuvent être pénalisées pour retard dans la mesure où nous avons supprimé les micro-inefficacités les plus évidentes pour les autres cas d'utilisation, réduisant ainsi éventuellement le nombre de scénarios "fonctionnant de manière optimale".
la source
sloc
. Je l'ai appelé "de taille moyenne" parce que je n'ai aucune idée de ce qui est considéré comme "gros" ici.Réponses:
Je ne connais pas d'approche générale à ce problème, mais deux approches quelque peu liées ont bien fonctionné pour moi dans le passé: faute de meilleures conditions, je les ai appelées groupage et optimisation horizontale .
L'approche groupée est une tentative de remplacer un grand nombre d'opérations courtes et rapides par une opération unique, plus lente et hautement spécialisée qui produit finalement le même résultat.
Exemple: Après avoir profilé une opération particulièrement lente de notre éditeur de règles visuelles, nous n'avons découvert aucun "fruit bas": il n'y avait pas une seule opération qui prenait plus de 2% du temps d'exécution, mais l'opération dans son ensemble semblait lente. Cependant, nous avons découvert que l'éditeur envoyait un grand nombre de petites requêtes au serveur. Même si l'éditeur traitait rapidement les réponses individuelles, le nombre d'interactions demande / réponse a eu un effet multiplicateur, donc le temps global de l'opération a été de plusieurs secondes. Après avoir soigneusement catalogué les interactions de l'éditeur au cours de cette opération de longue durée, nous avons ajouté une nouvelle commande à l'interface du serveur. La commande supplémentaire était plus spécialisée, car elle acceptait les données nécessaires pour effectuer un sous-ensemble d'opérations courtes, a exploré les dépendances des données pour comprendre l'ensemble final de données à renvoyer et a fourni une réponse contenant les informations nécessaires pour effectuer toutes les petites opérations individuelles en un seul voyage vers le serveur. Cela n'a pas réduit le temps de traitement dans notre code, mais il a réduit une quantité très importante de latence en raison de la suppression de plusieurs allers-retours client-serveur coûteux.
L'optimisation horizontale est une technique connexe lorsque vous éliminez la «lenteur» qui est finement répartie entre plusieurs composants de votre système à l'aide d'une fonction particulière de votre environnement d'exécution.
Exemple: Après le profilage d'une opération de longue durée, nous avons découvert que nous faisons beaucoup d'appels à travers la limite du domaine d'application (ceci est très spécifique à .NET). Nous n'avons pu éliminer aucun des appels et nous ne pouvions pas les regrouper: ils provenaient à des moments différents de sections très différentes de notre système, et les choses qu'ils demandaient dépendaient des résultats renvoyés des demandes précédentes. Chaque appel a nécessité la sérialisation et la désérialisation d'une quantité relativement faible de données. Encore une fois, les appels individuels étaient de courte durée, mais très nombreux. Nous avons fini par concevoir un schéma qui évitait presque entièrement la sérialisation, en le remplaçant par le passage d'un pointeur à travers la limite du domaine de l'application. Ce fut une grande victoire, car de nombreuses demandes de classes entièrement indépendantes sont devenues instantanément beaucoup plus rapides grâce à l'applicationsolution horizontale .
la source
Lorsque vous démarrez cette réécriture, vous devez faire plusieurs choses différemment.
Premier. Et le plus important. Arrêtez "d'optimiser". L '"optimisation" n'a pas beaucoup d'importance. Comme vous l'avez vu, seule la réécriture en gros est importante.
Donc.
Seconde. Comprendre les implications de chaque structure de données et choix d'algorithme.
Troisième. Faire du choix réel de la structure des données et de l'algorithme une question de "liaison tardive". Concevez des interfaces qui peuvent avoir n'importe laquelle de plusieurs implémentations utilisées derrière l'interface.
Ce que vous faites maintenant (réécriture) devrait être beaucoup, beaucoup moins pénible si vous avez défini un ensemble d'interfaces qui vous permet de modifier en gros la structure des données ou l'algorithme.
la source
++
et+=1
sera hors de propos et presque incommensurable. C'est la chose que vous durerez .Une bonne astuce pratique consiste à utiliser votre suite de tests unitaires comme suite de tests de performances .
L'approche suivante a bien fonctionné dans mes bases de code:
Si vous continuez à faire tout cela, alors avec le temps, les performances moyennes de votre base de code devraient s'améliorer de manière organique.
Il serait également possible de suivre les temps de test historiques et de dessiner des graphiques de performances et des régressions ponctuelles au fil du temps dans les performances moyennes. Je ne me suis jamais soucié de cela, principalement parce que c'est un peu difficile de s'assurer que vous comparez comme avec comme lorsque vous changez et ajoutez de nouveaux tests, mais cela pourrait être un exercice intéressant si les performances sont suffisamment importantes pour vous.
la source
La réponse de @dasblinkenlight souligne un problème très courant, en particulier avec les grandes bases de code (selon mon expérience). Il peut y avoir de graves problèmes de performances, mais ils ne sont pas localisés . Si vous exécutez un profileur, aucune routine ne prend suffisamment de temps pour attirer l'attention. (En supposant que vous regardez le pourcentage de temps inclusif qui inclut les callees. Ne vous embêtez même pas avec le «temps libre».)
En fait, dans ce cas, le problème réel n'a pas été trouvé par profilage, mais par chance.
J'ai une étude de cas, pour laquelle il y a un bref diaporama PDF , illustrant ce problème en détail et comment le gérer. Le point fondamental est, puisque le code est beaucoup plus lent qu'il ne pourrait l'être, cela signifie (par définition) que le temps supplémentaire est consacré à faire quelque chose qui pourrait être supprimé.
Si vous deviez regarder quelques échantillons à temps aléatoire de l'état du programme, vous simplement voir ce faire l'activité amovible, en raison du pour cent du temps qu'il faut. Il est tout à fait possible que l'activité amovible ne se limite pas à une seule fonction, voire à plusieurs fonctions. Il ne se localise pas de cette façon.
Ce n'est pas un "point chaud".
C'est une description que vous faites de ce que vous voyez, qui s'avère être vraie un grand pourcentage du temps. Cela le rend simple à découvrir, mais sa simplicité dépend de la quantité de réécriture nécessaire.
(Il y a souvent une critique de cette approche, à savoir que le nombre d'échantillons est trop petit pour la validité statistique. C'est ce qui est répondu sur la diapositive 13 du PDF. Brièvement - oui, il y a une grande incertitude dans la "mesure" des économies potentielles, mais 1) la valeur attendue de ces économies potentielles n'est pratiquement pas affectée, et 2) lorsque les économies potentielles $ x $ sont converties en ratio d'accélération de 1 $ / (1-x) $, elles sont fortement biaisées vers des facteurs élevés (bénéfiques).)
la source