Tout en répondant à - cette question , j'ai commencé à me demander pourquoi tant de développeurs croient qu'un bon design ne devrait pas rendre compte de la performance, car cela affecterait la lisibilité et/ou la maintenabilité.
Je crois qu'un bon design prend également une performance en considération au moment de son écrit et qu'un bon développeur avec un bon design peut écrire un programme efficace sans nuire à la lisibilité ou à la maintenabilité.
Alors que je reconnais qu'il y a des cas extrêmes, Pourquoi de nombreux développeurs insistent-ils un programme/conception efficace entraîneront une faible lisibilité et/ou une mauvaise maintenabilité, et par conséquent que la performance ne devrait pas être une contrepartie de la conception?
Je pense ces vues sont généralement des réactions aux tentatives d'optimisation prématurée (micro), qui est toujours répandue, et fait généralement plus de mal que bien. Quand on essaie de contrer de telles vues, il est facile de tomber dans - ou du moins à ressembler - l'autre extrême.
Il est néanmoins vrai qu'avec l'énorme développement des ressources matérielles au cours des dernières décennies, pour la plupart des programmes écrits aujourd'hui, la performance a cessé d'être un facteur limitant majeur. Bien entendu, il convient de prendre en compte les performances attendues et réalisables pendant la phase de conception, afin de identifier les cas lorsque la performance peut être (venir) un problème majeur. Et puis il est en effet important de concevoir une performance depuis le début. Cependant, la simplicité globale, la lisibilité et la maintenabilité sont encore plus importantes. Comme on l'a noté, le code optimisé de la performance est plus complexe, plus difficile à lire et à entretenir, et plus de bug-coudé que la solution de travail la plus simple. Ainsi, tout effort consacré à l'optimisation doit être prouvé - pas seulement croyait - Pour apporter de vrais avantages, tout en dégradant la maintenabilité à long terme du programme le moins possible. Ainsi, A bon design isole les parties complexes et intensives de performance du reste du code, qui est conservée aussi simple et propre que possible.
Venir à votre question du côté d'un développeur qui travaille sur le code hautes performances, il y a plusieurs éléments à considérer dans la conception.
Obtenez-le bien, faites-le bien, obtenez-le rapidement. Dans cet ordre.
Si je peux supposer que "emprunter" @ Greengit's Nice Diagram et faire une petite addition:
|
P
E
R
F
O * X <- a program as first written
R *
M *
A *
N *
C * * * * *
E
|
O -- R E A D A B I L I T Y --
Nous avons tous été "enseignés" qu'il y a des courbes de compromis. De plus, nous avons tous supposé que nous sommes des programmeurs aussi optimaux qu'un programme donné que nous écrivions est si serré c'est sur la courbe . Si un programme est sur la courbe, toute amélioration d'une dimension entraîne nécessairement un coût dans l'autre dimension.
Dans mon expérience, les programmes ne se rapprochent que de toute courbe en étant accordés, ajustés, martelés, cirés et en général transformés en "code golf". La plupart des programmes ont beaucoup d'espace pour améliorer toutes les dimensions. Voici ce que je veux dire.
Ce n'est pas tellement que ces choses ne peuvent pas coexister. Le problème est que le code de chacun est lent, illisible et stimulable sur la première itération. Le reste du temps est consacré à l'amélioration de ce qui est le plus important. Si c'est la performance, alors allez-y. N'écris pas du code terriblement terriblement terriblement, mais s'il doit juste être x rapide, alors faites-le x rapidement. Je crois que la performance et la propreté sont fondamentalement non corrélées. Le code performant ne provoque pas de code laid. Cependant, si vous passez votre temps à régler chaque morceau de code pour être rapide, devinez ce que vous n'avez pas passé votre temps à faire? Rendre votre code propre et maintenu.
[.____] | [.____] p E R F R * M * A * N * [.____] C * * * * * [.____] E [.____] | [.] O - Lisibilité -
Comme tu peux le voir...
Donc, la performance et la lisibilité sont mais liées modestement - et dans la plupart des cas, il n'y a pas de véritables grandes incitations préférant les anciens au-dessus de ces dernières. Et je parle ici des langues de haut niveau.
Je pense qu'il est difficile d'atteindre les trois. Je pense que je pense être réalisable. Par exemple, je pense qu'il est possible d'assurer une efficacité et une lisibilité dans certains cas, mais la maintenabilité pourrait être difficile avec le code micro-syntonisé. Le code le plus efficace de la planète manquera généralement à la fois la maintenabilité et la lisibilité, comme cela est probablement évident pour la plupart, à moins que vous soyez le genre qui puisse comprendre la main Code SIMD multithreadisé SOA-vectorisé que Intel écrit avec un ensemble inliné, ou les algorithmes de pointe utilisés dans l'industrie avec des journaux mathématiques de 40 pages publiés il y a seulement 2 mois et 12 bibliothèques d'une valeur de code pour une structure de données incroyablement complexe.
micro-efficacité
Une chose que je suggère que cela pourrait être contraire à l'opinion populaire est que le code algorithmique plus intelligent est souvent plus difficile à maintenir que l'algorithme simple le plus à réglage. Cette idée selon laquelle les améliorations de l'évolutivité donnent plus de bang pour l'argent sur le code micro-syntonisé (ex: modèles d'accès respectueux de la cache, multithreading, SIMD, etc.) est quelque chose que je pourrais contester, au moins avoir travaillé dans une industrie remplie d'une industrie extrêmement complexe. Structures de données et algorithmes (l'industrie Visual FX), en particulier dans des domaines tels que le traitement des mailles, car le bang pourrait être important, mais le mâle est extrêmement coûteux lorsque vous introduisez de nouveaux algorithmes et des structures de données, personne n'a jamais entendu parler d'auparavant depuis leur marque Nouveau. En outre, j'ai réussi à battre de tels algorithmes et structures de données avec un code plus simple sur des connaissances générales sur l'informatique et l'architecture informatique, et non des algorithmes industriels de pointe publiés par des assistants mathématiques, qui "théoriquement" ne se sont pas aussi complètement échoués ( Ex: linearithmic vs. linéaire) mais a été micro-syntonisée et ma version ne nécessitait que environ 1/100ème du code, la solution évolutive requise tout en étant plus rapide pour les entrées plus importantes (relativement plus rapidement, les entrées sont devenues ma version encore plus évolutives. en pratique, même s'il était moins évolutif théoriquement).
Donc, cette idée selon laquelle les optimisations algorithmiques l'emportent toujours, disent que les optimisations liées aux modèles d'accès à la mémoire sont toujours quelque chose que je ne suis pas tout à fait d'accord. Bien sûr, si vous utilisez un tri de bulle, aucune micro-optimisation ne peut vous aider ... mais dans la raison, je ne pense pas que ce soit toujours si clair. Et des optimisations algorithmiques sans doute sont plus difficiles à entretenir que les micro-optimisations. J'aurais beaucoup plus facile de maintenir, disons, Embrere d'Intel qui prend un algorithme de BVH classique et simple et qui a simplement des micro-mélodies la merde de celui-ci que le code OpenVDB de Dreamwork pour des méthodes de pointe d'accélération algorithmique de simulation de fluide. Donc, dans mon industrie au moins, j'aimerais voir plus de personnes familiarisées avec une architecture informatique Micro-optimisant davantage, car Intel a ensuite marché dans la scène, par opposition à la création de milliers et de milliers de nouveaux algorithmes et de nouvelles structures de données. Avec des micro-optimisations efficaces, les personnes pourraient potentiellement trouver de moins en moins de raisons d'inventer de nouveaux algorithmes.
J'ai travaillé dans une base de code héritée avant que presque chaque opération utilisateur avait sa propre structure de données unique et son algorithme de référence (ajoutant des centaines de structures de données exotiques). Et la plupart d'entre eux avaient des caractéristiques de performance très asymétrique, étant très étroitement applicables. Cela aurait été tellement plus facile si le système pouvait tourner autour d'une douzaine de structures de données plus largement applicables, et je pense que cela aurait pu être le cas s'ils étaient beaucoup mieux optimisés. Je mentionne ce cas car la micro-optimisation peut potentiellement améliorer la maintenabilité énormément dans un tel cas si cela signifie la différence entre des centaines de structures de données micro-pessimisées qui ne peuvent même pas être utilisées en toute sécurité pour des fins strictes en lecture seule, qui impliquent des caches manquées à gauche et Droite vs juste des dizaines de structures de données micro-optimisées qui fonctionnent beaucoup plus efficacement tout autour.
Langues fonctionnelles
Pendant ce temps, une partie du code le plus maintenu que j'ai jamais rencontré était raisonnablement efficace mais extrêmement difficile à lire, car ils ont été écrits en langues fonctionnelles. En général, la lisibilité et la maintenabilité de Uber sont des idées contradictoires à mon avis.
Il est vraiment difficile de rendre le code lisible, et de maintenir et efficace tout à la fois. Généralement, vous devez compromettre un peu dans l'une de ces trois, sinon deux, comme compromettre la lisibilité pour la maintenabilité ou compromettre la maintenabilité d'efficacité. C'est généralement la main-d'œuvre qui souffre lorsque vous recherchez une grande partie des deux autres.
lisibilité contre la maintenabilité
Maintenant, comme dit, je crois que la lisibilité et la maintenabilité sont non des concepts harmonieux. Après tout, le code le plus lisible pour la plupart des mortels américains cartes de manière très intuitive aux motifs de pensée humaine et les modèles de pensées humaines sont intrinsèquement exposés à l'erreur: " si cela se produit, faites-le. Si cela se produit. Si cela se produit. Si cela se produit. Si cela se produit. Si cela se produit. , fais cela. Sinon, faites cela. Oups, j'ai oublié quelque chose! Si ces systèmes interagissent les uns avec les autres, cela devrait arriver, de sorte que ce système puisse le faire ... Oh, attendez, qu'en est-il de ce système lorsque cet événement est déclenché? "J'ai oublié la citation exacte mais quelqu'un a dit une fois que si Rome était construite comme des logiciels, il ne prendrait qu'un oiseau atterrissant sur un mur pour l'apporter. Tel est le cas avec la plupart des logiciels. C'est plus fragile que nous nous soucions souvent de penser. Quelques lignes de code apparemment inoffensifs ici et il pourrait s'arrêter sur le point de nous faire reconsidérer toute la conception et que des langues de haut niveau visant à être aussi lisibles que possible ne sont pas des exceptions à de telles erreurs de conception humaine.
Les langages fonctionnels purs sont à peu près aussi proches de l'invulnérable à cela que l'on peut obtenir de manière gastronomique (pas même près d'être invulnérable, mais relativement plus proche que la plupart). Et c'est partiellement parce qu'ils ne correspondent pas intuitivement à la pensée humaine. Ils ne sont pas lisibles. Ils forcent la pensée des modèles sur nous, ce qui nous oblige à résoudre les problèmes avec le moins de cas spéciaux que possible en utilisant la quantité minimale de connaissances possibles et sans causer d'effets secondaires. Ils sont extrêmement orthogonaux, ils permettent au code de changer souvent et de changements sans surprises si EPIC que nous devions repenser la conception sur une planche à dessin, même au point de changer d'avis sur la conception générale, sans réécrire tout. Il ne semble pas être plus facile de maintenir que cela ... mais le code est toujours très difficile à lire et presque nécessairement à être si maintenable (si facile de changer sans problèmes).
Le point est non La lisibilité doit toujours l'emporter sur l'efficacité. Si vous savez du get Go, votre algorithme doit être très efficace, ce sera l'un des facteurs que vous utilisez pour le développer.
La chose est la plupart utilise des cas n'ayant pas besoin d'un code rapide aveugle. Dans de nombreux cas IO ou interaction utilisateur entraîne beaucoup plus de retard, votre exécution d'algorithme provoque des causes de votre algorithme. Le point est que vous ne devriez pas sortir de votre façon de faire quelque chose plus efficace si vous ne savez pas C'est le cou de la bouteille.
L'optimisation du code de performance le rend souvent plus compliqué car cela implique généralement de faire des choses de manière intelligente, au lieu de la plus intuitive. Le code plus compliqué est plus difficile à maintenir et plus difficile pour les autres développeurs à ramasser (les deux sont des coûts qui doivent être pris en compte). Dans le même temps, les compilateurs sont très bons pour optimiser les cas communs. Il est possible que votre tentative d'amélioration d'un cas commun signifie que le compilateur ne reconnaît plus le motif et ne peut donc pas vous aider à rendre votre code rapidement. Il convient de noter que cela ne signifie pas écrire tout ce que vous voulez sans préoccupation de la performance. Vous ne devriez pas faire quelque chose qui est clairement inefficace.
Le but est de ne pas s'inquiéter de petites choses qui peuvent rendre les choses mieux. Utilisez un profileur et voyez que 1) Ce que vous avez maintenant est un problème et 2) ce que vous avez changé pour être une amélioration.
Je pense que la plupart des programmeurs obtiennent cette intestinale, simplement parce que la plupart du temps, le code de performance est un code basé sur beaucoup plus d'informations (sur le contexte, la connaissance matérielle, l'architecture globale) que tout autre code dans les applications. La plupart des codes n'exprimeront que certaines solutions à des problèmes spécifiques qui sont encapsulés dans certaines abstractions de manière modulaire (comme des fonctions telles que les moyens limitant la connaissance du contexte uniquement à ce qui entrave cette encapsulation (comme des paramètres de fonction).
Lorsque vous écrivez pour des performances élevées, après avoir réparer toutes les opticulisations algorithmiques, vous entrez dans des détails qui nécessitent beaucoup plus de connaissances sur le contexte. Cela pourrait naturellement submerger tout programmeur qui ne se sent pas suffisamment concentré pour la tâche.
Parce que le coût du réchauffement climatique (de ces cycles de CPU supplémentaires a été mis à l'échelle par des centaines de millions de PC et des installations de centre de données massives) et une durée de vie médiocre de la batterie (sur les appareils mobiles de l'utilisateur), comme requis pour exécuter leur code mal optimisé, apparaît rarement sur la plupart des Performance du programmeur ou examens par des pairs.
C'est une externalité négative économique, semblable à une forme de pollution ignorée. Ainsi, le ratio coût/avantages de la pensée sur la performance du tout est asymétrique mentalement de la réalité.
Les concepteurs matériels ont fonctionné dur en ajoutant des fonctionnalités de la sauvegarde de puissance et de l'échelle de l'horloge aux derniers processeurs. Il appartient aux programmeurs de laisser le matériel tirer parti de ces capacités plus souvent, en ne mâchant pas chaque cycle d'horloge de la CPU disponible.
AJOUTÉ: De retour dans les temps antiques, le coût de l'un ordinateur était des millions, une optimisation de l'heure de la CPU était très importante. Ensuite, le coût du développement et de la maintenance du Code est devenu supérieur au coût des ordinateurs. L'optimisation a donc chuté de la faveur par rapport à la productivité du programmeur. Cependant, toutefois, un autre coût devient supérieur au coût des ordinateurs, le coût de l'alimentation et le refroidissement de tous ces centres de données deviennent maintenant plus importants que le coût de tous les processeurs à l'intérieur.
Il existe également des pièces célèbres de code hautement optimisé qui plieront la plupart des cerveaux des peuples qui soutiennent le cas que le code hautement optimisé est difficile à lire et à comprendre.
Voici le plus célèbre que je pense. Pris de Quake III Arena et attribué à John Carmak, bien que je pense qu'il y a eu plusieurs itérations de cette fonction et qu'il n'a pas été créé à l'origine par lui ( N'est-ce pas Wikipedia super? ) ==).
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
return y;
}
Un problème est que le temps de développeur fini signifie que tout ce que vous cherchez à optimiser enlève du temps de passer du temps sur les autres problèmes.
Il y a une assez bonne expérience effectuée sur ce code référencé dans le code de Meyer. Différents groupes de développeurs ont été invités à optimiser la vitesse, l'utilisation de la mémoire, la lisibilité, la robustesse et ainsi de suite. Il a été constaté que leurs projets ont marqué de manière élevée dans tout ce qu'ils ont été invités à optimiser, mais plus bas de toutes les autres qualités.
Parce que les programmeurs expérimentés ont appris que c'est vrai.
Nous avons travaillé avec du code qui est maigre et méchant et n'a pas de problèmes de performance.
Nous avons travaillé sur beaucoup de code que, pour résoudre les problèmes de performance, est très complexe.
Un exemple immédiat qui me vient à l'esprit est que mon dernier projet comprenait 8.192 tables SQL faite manuellement. Ceci était nécessaire à cause des problèmes de performance. La configuration à sélectionner à partir de 1 table est beaucoup plus simple que de sélectionner et de maintenir 8 1392 éclats.