(Ceci est principalement destiné à ceux qui ont une connaissance spécifique des systèmes à faible latence, pour éviter que les gens répondent simplement avec des opinions non fondées).
Pensez-vous qu'il y a un compromis entre l'écriture de code orienté objet "sympa" et l'écriture de code à faible latence très rapide? Par exemple, éviter les fonctions virtuelles en C ++ / la surcharge du polymorphisme, etc. réécrire du code qui a l'air méchant, mais qui est très rapide, etc.?
Cela va de soi - peu importe si ça a l'air moche (tant que c'est maintenable) - si vous avez besoin de vitesse, vous avez besoin de vitesse?
Je serais intéressé d'entendre des gens qui ont travaillé dans ces domaines.
Réponses:
Oui.
C'est pourquoi l'expression "optimisation prématurée" existe. Il existe pour forcer les développeurs à mesurer leurs performances, et à optimiser uniquement ce code qui fera une différence dans les performances, tout en concevant judicieusement leur architecture d'application dès le début afin qu'elle ne tombe pas sous une lourde charge.
De cette façon, dans la mesure du possible, vous conservez votre code joli, bien architecturé et orienté objet, et vous n'optimisez avec du code laid que les petites portions qui comptent.
la source
Oui, l'exemple que je donne n'est pas C ++ vs Java, mais Assembly vs COBOL car c'est ce que je sais.
Les deux langues sont très rapides, mais même COBOL, une fois compilé, contient beaucoup plus d'instructions qui sont placées dans le jeu d'instructions et qui n'ont pas nécessairement besoin d'être là par rapport à l'écriture de ces instructions vous-même dans Assembly.
La même idée peut être appliquée directement à votre question d'écrire du «code laid» par rapport à l'utilisation de l'héritage / polymorphisme en C ++. Je pense qu'il est nécessaire d'écrire du code laid, si l'utilisateur final a besoin de délais de transaction inférieurs à une seconde, c'est notre travail en tant que programmeurs de leur donner cela, peu importe comment cela se produit.
Cela étant dit, l'utilisation libérale des commentaires augmente considérablement la fonctionnalité et la maintenabilité du programmeur, quelle que soit la laideur du code.
la source
Oui, un compromis existe. Par cela, je veux dire qu'un code plus rapide et plus laid n'est pas nécessaire mieux - les avantages quantitatifs du «code rapide» doivent être pondérés par rapport à la complexité de maintenance des changements de code nécessaires pour atteindre cette vitesse.
Le compromis vient du coût de l'entreprise. Le code qui est plus complexe nécessite des programmeurs plus qualifiés (et des programmeurs avec un ensemble de compétences plus ciblées, tels que ceux avec une architecture CPU et des connaissances en conception), prend plus de temps pour lire et comprendre le code et pour corriger les bogues. Le coût commercial de développement et de maintenance d'un tel code pourrait être de l'ordre de 10x à 100x par rapport au code normalement écrit.
Ce coût de maintenance est justifiable dans certaines industries , dans lesquelles les clients sont prêts à payer une prime très élevée pour un logiciel très rapide.
Certaines optimisations de vitesse permettent un meilleur retour sur investissement (ROI) que d'autres. À savoir, certaines techniques d'optimisation peuvent être appliquées avec un impact moindre sur la maintenabilité du code (préservant la structure de niveau supérieur et la lisibilité de niveau inférieur) par rapport au code normalement écrit.
Ainsi, un propriétaire d'entreprise devrait:
Ces compromis sont très spécifiques aux circonstances.
Ceux-ci ne peuvent être décidés de manière optimale sans la participation des gestionnaires et des propriétaires de produits.
Celles-ci sont très spécifiques aux plates-formes. Par exemple, les processeurs de bureau et mobiles ont des considérations différentes. Les applications serveur et client ont également des considérations différentes.
Oui, il est généralement vrai que le code plus rapide est différent du code normalement écrit. Tout code différent prendra plus de temps à lire. Que cela implique la laideur est aux yeux du spectateur.
Les techniques avec lesquelles j'ai une certaine exposition sont: (sans essayer de revendiquer un quelconque niveau d'expertise) l'optimisation à vecteur court (SIMD), le parallélisme de tâches à grain fin, la pré-allocation de mémoire et la réutilisation d'objets.
SIMD a généralement des impacts graves sur la lisibilité de bas niveau, même s'il ne nécessite généralement pas de modifications structurelles de plus haut niveau (à condition que l'API soit conçue avec la prévention des goulots d'étranglement à l'esprit).
Certains algorithmes peuvent être facilement transformés en SIMD (l'embarquement vectorisable). Certains algorithmes nécessitent plus de réarrangements de calcul pour utiliser SIMD. Dans des cas extrêmes tels que le parallélisme SIMD du front d'onde, des algorithmes entièrement nouveaux (et des implémentations brevetables) doivent être écrits pour en profiter.
La parallélisation de tâches à granularité fine nécessite de réorganiser les algorithmes en graphiques de flux de données et d'appliquer de manière répétée une décomposition fonctionnelle (informatique) à l'algorithme jusqu'à ce qu'aucun autre avantage de marge ne puisse être obtenu. Les étapes décomposées sont généralement enchaînées avec un style de continuation, un concept emprunté à la programmation fonctionnelle.
Par décomposition fonctionnelle (informatique), les algorithmes qui auraient pu être normalement écrits dans une séquence linéaire et conceptuellement claire (lignes de code qui sont exécutables dans le même ordre qu'elles sont écrites) doivent être décomposés en fragments et distribués en plusieurs fonctions. ou des cours. (Voir l'objectivation de l'algorithme ci-dessous.) Cette modification entravera considérablement les autres programmeurs qui ne sont pas familiers avec le processus de conception de décomposition qui a donné naissance à un tel code.
Pour rendre ce code maintenable, les auteurs de ce code doivent écrire des documentations élaborées de l'algorithme - bien au-delà du type de commentaire de code ou des diagrammes UML effectués pour du code normalement écrit. Ceci est similaire à la façon dont les chercheurs rédigent leurs articles universitaires.
Non, il n'est pas nécessaire que le code rapide soit en contradiction avec l'orientation orientée objet.
Autrement dit, il est possible d'implémenter un logiciel très rapide, toujours orienté objet. Cependant, vers l'extrémité inférieure de cette implémentation (au niveau des écrous et des boulons où la majorité du calcul se produit), la conception d'objet peut s'écarter considérablement des conceptions obtenues à partir de la conception orientée objet (OOD). La conception de niveau inférieur est orientée vers l'objectivation de l'algorithme.
Quelques avantages de la programmation orientée objet (POO), tels que l'encapsulation, le polymorphisme et la composition, peuvent toujours être tirés de l'objectivation d'algorithme de bas niveau. C'est la principale justification de l'utilisation de la POO à ce niveau.
La plupart des avantages de la conception orientée objet (OOD) sont perdus. Plus important encore, il n'y a pas d'intuitivité dans la conception de bas niveau. Un collègue programmeur ne peut pas apprendre à travailler avec le code de niveau inférieur sans d'abord comprendre pleinement comment l'algorithme a été transformé et décomposé en premier lieu, et cette compréhension n'est pas disponible à partir du code résultant.
la source
Oui, le code doit parfois être "moche" pour le faire fonctionner dans le temps requis, tout le code ne doit pas être moche cependant. Les performances doivent être testées et profilées avant de trouver les bits de code qui doivent être "moches" et ces sections doivent être notées avec un commentaire afin que les futurs développeurs sachent ce qui est délibérément laid et ce qui est juste de la paresse. Si quelqu'un écrit beaucoup de code mal conçu invoquant des raisons de performances, faites-le le prouver.
La vitesse est tout aussi importante que toute autre exigence d'un programme, apporter de mauvaises corrections à un missile guidé équivaut à fournir les bonnes corrections après l'impact. La maintenabilité est toujours une préoccupation secondaire pour le code de travail.
la source
Certaines des études dont j'ai vu des extraits indiquent que le code propre et facile à lire est souvent plus rapide que le code plus difficile à lire. Cela est dû en partie à la conception des optimiseurs. Ils ont tendance à être bien meilleurs pour optimiser une variable dans un registre que de faire de même avec un résultat intermédiaire de calcul. De longues séquences d'affectations utilisant un seul opérateur conduisant au résultat final peuvent être mieux optimisées qu'une longue équation compliquée. Les nouveaux optimiseurs ont peut-être réduit la différence entre un code propre et compliqué, mais je doute qu'ils l'aient éliminé.
D'autres optimisations telles que le déroulement de la boucle peuvent être ajoutées de manière nette si nécessaire.
Toute optimisation ajoutée pour améliorer les performances doit être accompagnée d'un commentaire approprié. Cela devrait inclure une déclaration selon laquelle il a été ajouté en tant qu'optimisation, de préférence avec des mesures des performances avant et après.
J'ai trouvé que la règle 80/20 s'applique au code que j'ai optimisé. En règle générale, je n'optimise rien qui ne prenne pas au moins 80% du temps. Je vise ensuite (et j'atteins généralement) une augmentation des performances de 10 fois. Cela améliore les performances d'environ 4 fois. La plupart des optimisations que j'ai mises en œuvre n'ont pas rendu le code beaucoup moins "beau". Votre kilométrage peut varier.
la source
Si par laid, vous voulez dire difficile à lire / comprendre au niveau où d'autres développeurs le réutiliseront ou auront besoin de le comprendre, alors je dirais qu'un code élégant et facile à lire vous permettra presque toujours de vous gain de performances à long terme dans une application que vous devez maintenir.
Sinon, il y a parfois suffisamment de gains de performances pour que cela vaille la peine d'être laid dans une belle boîte avec une interface de tueur, mais d'après mon expérience, c'est un dilemme assez rare.
Pensez à éviter le travail de base au fur et à mesure. Enregistrez les astuces mystérieuses pour savoir quand un problème de performances se présente réellement. Et si vous devez écrire quelque chose que quelqu'un ne pourrait comprendre que grâce à la familiarisation avec l'optimisation spécifique, faites ce que vous pouvez pour rendre au moins le laid facile à comprendre à partir d'une réutilisation de votre point de vue de code. Un code qui fonctionne misérablement le fait rarement parce que les développeurs réfléchissaient trop à ce que le prochain gars allait hériter, mais si les changements fréquents sont la seule constante d'une application (la plupart des applications Web selon mon expérience), un code rigide / rigide qui est difficile à modifier, c'est pratiquement la mendicité pour que les dégâts paniqués commencent à apparaître partout dans votre base de code. Propre et maigre est meilleur pour les performances à long terme.
la source
Complexe et laid n'est pas la même chose. Le code qui a de nombreux cas particuliers, qui est optimisé pour effacer chaque dernière baisse de performances, et qui ressemble d'abord à un enchevêtrement de connexions et de dépendances peut en fait être très soigneusement conçu et assez beau une fois que vous le comprenez. En effet, si les performances (qu'elles soient mesurées en termes de latence ou autre) sont suffisamment importantes pour justifier un code très complexe, alors le code doit être bien conçu. Si ce n'est pas le cas, vous ne pouvez pas être sûr que toute cette complexité est vraiment meilleure qu'une solution plus simple.
Le code laid, pour moi, est un code bâclé, mal considéré et / ou inutilement compliqué. Je ne pense pas que vous souhaitiez l'une de ces fonctionnalités dans le code qui doit fonctionner.
la source
Je travaille dans un domaine qui est un peu plus axé sur le débit que sur la latence, mais c'est très critique pour les performances, et je dirais "sorta" .
Pourtant, un problème est que tant de gens se trompent complètement sur leurs notions de performance. Les novices se trompent souvent à peu près tout et leur modèle conceptuel entier de «coût de calcul» a besoin d'être retravaillé, avec seulement la complexité algorithmique étant la seule chose qu'ils peuvent obtenir correctement. Les intermédiaires se trompent sur beaucoup de choses. Les experts se trompent.
Mesurer avec des outils précis qui peuvent fournir des mesures telles que les erreurs de cache et les erreurs de prédiction de branche est ce qui garde toutes les personnes de tout niveau d'expertise dans le domaine sous contrôle.
La mesure est également ce qui indique ce qu'il ne faut pas optimiser . Les experts passent souvent moins de temps à optimiser que les novices, car ils optimisent les véritables points chauds mesurés et n'essaient pas d'optimiser les coups sauvages dans l'obscurité en fonction de intuitions sur ce qui pourrait être lent (ce qui, sous une forme extrême, pourrait inciter à une micro-optimisation juste sur toutes les autres lignes de la base de code).
Concevoir pour la performance
Avec cela mis à part, la clé de la conception pour la performance vient de la partie conception , comme dans la conception d'interface. L'un des problèmes liés à l'inexpérience est qu'il y a généralement un changement précoce des mesures de mise en œuvre absolues, comme le coût d'un appel de fonction indirect dans un contexte généralisé, comme si le coût (qui est mieux compris dans un sens immédiat du point de vue d'un optimiseur). plutôt qu'un point de vue de branchement) est une raison pour l'éviter dans toute la base de code.
Les coûts sont relatifs . Bien qu'il y ait un coût pour un appel de fonction indirect, par exemple, tous les coûts sont relatifs. Si vous payez ce coût une fois pour appeler une fonction qui passe par des millions d'éléments, s'inquiéter de ce coût, c'est comme passer des heures à marchander des sous pour acheter un produit d'un milliard de dollars, pour finalement conclure à ne pas acheter ce produit car il était un sou trop cher.
Conception d'interface plus grossière
L' aspect de conception d' interface de la performance cherche souvent plus tôt à pousser ces coûts à un niveau plus grossier. Au lieu de payer les coûts d'abstraction à l'exécution pour une seule particule, par exemple, nous pourrions pousser ce coût au niveau du système de particules / émetteur, transformant efficacement une particule en détail d'implémentation et / ou simplement des données brutes de cette collection de particules.
La conception orientée objet ne doit donc pas être incompatible avec la conception axée sur les performances (que ce soit la latence ou le débit), mais il peut y avoir des tentations dans un langage qui se concentre sur celui-ci pour modéliser des objets granulaires de plus en plus minuscules, et là, le dernier optimiseur ne peut pas Aidez-moi. Il ne peut pas faire des choses comme fusionner une classe représentant un seul point d'une manière qui donne une représentation SoA efficace pour les modèles d'accès à la mémoire du logiciel. Une collection de points avec une conception d'interface modélisée au niveau de grossièreté offre cette opportunité et permet d'itérer vers des solutions de plus en plus optimales au besoin. Une telle conception est conçue pour la mémoire en masse *.
De nombreuses conceptions critiques en termes de performances peuvent en fait être très compatibles avec la notion de conceptions d'interface de haut niveau qui sont faciles à comprendre et à utiliser pour les humains. La différence est que le «haut niveau» dans ce contexte concernerait l'agrégation de masse de la mémoire, une interface modélisée pour des collections de données potentiellement volumineuses, et avec une implémentation sous le capot qui peut être assez bas. Une analogie visuelle pourrait être une voiture vraiment confortable et facile à conduire et à manipuler et très sûre tout en allant à la vitesse du son, mais si vous ouvrez le capot, il y a peu de démons cracheurs de feu à l'intérieur.
Avec une conception plus grossière, il est également plus facile de fournir des modèles de verrouillage plus efficaces et d'exploiter le parallélisme dans le code (le multithreading est un sujet exhaustif que je vais en quelque sorte ignorer ici).
Pool de mémoire
Un aspect critique de la programmation à faible latence va probablement être un contrôle très explicite de la mémoire pour améliorer la localité de référence ainsi que la vitesse générale d'allocation et de désallocation de la mémoire. Un allocateur personnalisé regroupant la mémoire fait en fait écho au même type de mentalité de conception que nous avons décrit. Il est conçu pour le vrac ; il est conçu à un niveau grossier. Il préalloue la mémoire en gros blocs et regroupe la mémoire déjà allouée en petits morceaux.
L'idée est exactement la même de pousser des choses coûteuses (allouer un morceau de mémoire contre un allocateur à usage général, par exemple) à un niveau plus grossier et plus grossier. Un pool de mémoire est conçu pour gérer la mémoire en masse .
Type Systèmes Mémoire séparée
L'une des difficultés de la conception granulaire orientée objet dans n'importe quel langage est qu'elle veut souvent introduire de nombreux types et structures de données définis par l'utilisateur. Ces types peuvent alors vouloir être alloués en petits morceaux minuscules s'ils sont alloués dynamiquement.
Un exemple courant en C ++ serait pour les cas où le polymorphisme est requis, où la tentation naturelle est d'allouer chaque instance d'une sous-classe contre un allocateur de mémoire à usage général.
Cela finit par décomposer les dispositions de mémoire éventuellement contiguës en petits morceaux et morceaux dispersés à travers la plage d'adressage, ce qui se traduit par davantage de défauts de page et d'échecs de cache.
Les domaines qui exigent la réponse déterministe la plus faible latence, sans bégaiement sont probablement le seul endroit où les points chauds ne se résument pas toujours à un seul goulot d'étranglement, où de minuscules inefficacités peuvent réellement réellement "s'accumuler" (quelque chose que beaucoup de gens imaginent ne se passe pas correctement avec un profileur pour les garder sous contrôle, mais dans les domaines liés à la latence, il peut en fait y avoir de rares cas où de minuscules inefficacités s'accumulent). Et beaucoup des raisons les plus courantes d'une telle accumulation peuvent être les suivantes: l'allocation excessive de minuscules morceaux de mémoire partout.
Dans des langages comme Java, il peut être utile d'utiliser plus de tableaux d'anciens types de données simples lorsque cela est possible pour les zones goulot d'étranglement (zones traitées en boucles serrées) telles qu'un tableau de
int
(mais toujours derrière une interface de haut niveau volumineuse) au lieu de, par exemple , unArrayList
desInteger
objets définis par l'utilisateur . Cela évite la ségrégation mémoire qui accompagnerait typiquement cette dernière. En C ++, nous n'avons pas autant à dégrader la structure si nos modèles d'allocation de mémoire sont efficaces, car les types définis par l'utilisateur peuvent y être alloués de manière contiguë et même dans le contexte d'un conteneur générique.Fusionner la mémoire ensemble
Une solution ici consiste à rechercher un allocateur personnalisé pour les types de données homogènes, et peut-être même entre les types de données homogènes. Lorsque de minuscules types de données et structures de données sont aplatis en bits et octets en mémoire, ils prennent une nature homogène (mais avec des exigences d'alignement variables). Lorsque nous ne les considérons pas dans un état d'esprit centré sur la mémoire, le système de types de langages de programmation "veut" diviser / séparer les régions de mémoire potentiellement contiguës en petits morceaux dispersés.
La pile utilise cette concentration centrée sur la mémoire pour éviter cela et potentiellement stocker à l'intérieur de celle-ci toute combinaison mixte possible d'instances de type définies par l'utilisateur. Utiliser davantage la pile est une excellente idée lorsque cela est possible car le haut de celle-ci est presque toujours assis dans une ligne de cache, mais nous pouvons également concevoir des allocateurs de mémoire qui imitent certaines de ces caractéristiques sans modèle LIFO, fusionnant la mémoire entre des types de données disparates en contigus morceaux même pour des modèles d'allocation et de désallocation de mémoire plus complexes.
Le matériel moderne est conçu pour être à son apogée lors du traitement de blocs de mémoire contigus (accéder à plusieurs reprises à la même ligne de cache, à la même page, par exemple). Le mot-clé y est contigu, car cela n'est bénéfique que s'il y a des données environnantes d'intérêt. Ainsi, une grande partie (mais aussi la difficulté) des performances consiste à fusionner à nouveau des morceaux de mémoire séparés en blocs contigus auxquels on accède dans leur intégralité (toutes les données environnantes étant pertinentes) avant l'expulsion. Le système de type riche de types spécialement définis par l'utilisateur dans les langages de programmation peut être le plus grand obstacle ici, mais nous pouvons toujours atteindre et résoudre le problème via un allocateur personnalisé et / ou des conceptions plus volumineuses, le cas échéant.
Laid
"Ugly" est difficile à dire. C'est une métrique subjective, et quelqu'un qui travaille dans un domaine très critique en termes de performances commencera à changer son idée de la «beauté» en une idée beaucoup plus orientée données et se concentrant sur les interfaces qui traitent les choses en vrac.
Dangereux
"Dangereux" pourrait être plus facile. En général, les performances ont tendance à vouloir atteindre un code de niveau inférieur. La mise en œuvre d'un allocateur de mémoire, par exemple, est impossible sans atteindre sous les types de données et travailler au niveau dangereux des bits et octets bruts. En conséquence, cela peut aider à mettre davantage l'accent sur une procédure de test minutieuse dans ces sous-systèmes critiques pour les performances, en adaptant la rigueur des tests avec le niveau d'optimisation appliqué.
Beauté
Pourtant, tout cela se ferait au niveau des détails de mise en œuvre. Dans un état d'esprit à grande échelle et critique pour les performances, la «beauté» a tendance à se tourner vers les conceptions d'interface plutôt que vers les détails de mise en œuvre. Il devient de plus en plus prioritaire de rechercher des interfaces «belles», utilisables, sûres et efficaces plutôt que des implémentations en raison des ruptures de couplage et de cascade qui peuvent survenir face à un changement de conception d'interface. Les implémentations peuvent être échangées à tout moment. Nous itérons généralement vers les performances selon les besoins et comme indiqué par les mesures. La clé de la conception de l'interface est de modéliser à un niveau suffisamment grossier pour laisser de la place à de telles itérations sans casser tout le système.
En fait, je dirais que l'accent mis par un vétéran sur le développement essentiel à la performance aura souvent tendance à mettre l'accent sur la sécurité, les tests, la maintenabilité, juste le disciple de SE en général, car une base de code à grande échelle qui a un certain nombre de performances -les sous-systèmes critiques (systèmes de particules, algorithmes de traitement d'image, traitement vidéo, rétroaction audio, raytracers, moteurs maillés, etc.) devront prêter une attention particulière à l'ingénierie logicielle pour éviter de se noyer dans un cauchemar de maintenance. Ce n'est pas par pure coïncidence que souvent les produits les plus étonnamment efficaces peuvent également contenir le moins de bogues.
TL; DR
Quoi qu'il en soit, c'est mon point de vue sur le sujet, qui va des priorités dans des domaines véritablement critiques aux performances, ce qui peut réduire la latence et provoquer de minuscules inefficacités, et ce qui constitue réellement la «beauté» (lorsque l'on regarde les choses de la manière la plus productive).
la source
Pour ne pas être différent, mais voici ce que je fais:
Écrivez-le propre et maintenable.
Faites un diagnostic des performances et corrigez les problèmes qu'il vous indique, pas ceux que vous devinez. Garantis, ils seront différents de ce que vous attendez.
Vous pouvez effectuer ces correctifs de manière claire et maintenable, mais vous devrez ajouter des commentaires pour que les personnes qui consultent le code sachent pourquoi vous l'avez fait de cette façon. Sinon, ils l'annuleront.
Y a-t-il donc un compromis? Je ne le pense pas vraiment.
la source
Vous pouvez écrire du code laid qui est très rapide et vous pouvez également écrire du beau code aussi rapide que votre code laid. Le goulot d'étranglement ne sera pas dans la beauté / organisation / structure de votre code mais dans les techniques que vous aurez choisies. Par exemple, utilisez-vous des sockets non bloquants? Utilisez-vous une conception à filetage unique? Utilisez-vous une file d'attente sans verrouillage pour la communication entre les threads? Produisez-vous des déchets pour le GC? Effectuez-vous des opérations d'E / S de blocage dans le thread critique? Comme vous pouvez le voir, cela n'a rien à voir avec la beauté.
la source
Qu'est-ce qui compte pour l'utilisateur final?
Cas 1: mauvais code optimisé
Cas 2: bon code non optimisé
Solution?
Facile, optimisez les morceaux de code critiques pour les performances
par exemple:
Un programme qui se compose de 5 méthodes , 3 d'entre elles sont pour la gestion des données, 1 pour la lecture du disque, l'autre pour l'écriture sur le disque
Ces 3 méthodes de gestion des données utilisent les deux méthodes d'E / S et en dépendent
Nous optimiserions les méthodes d'E / S.
Raison: les méthodes d'E / S sont moins susceptibles d'être modifiées, ni affectent la conception de l'application, et dans l'ensemble, tout dans ce programme en dépend, et donc elles semblent essentielles aux performances, nous utiliserions n'importe quel code pour les optimiser .
Cela signifie que nous obtenons un bon code et une conception gérable du programme tout en le gardant rapide en optimisant certaines parties du code
Je suis en train de penser..
Je pense qu'un mauvais code rend difficile pour les humains d'optimiser le polissage et que de petites erreurs peuvent l'aggraver, donc un bon code pour un novice / débutant serait mieux s'il était bien écrit ce code laid.
la source